<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>lingyi</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blog.280303.xyz/</id>
  <link href="https://blog.280303.xyz/" rel="alternate"/>
  <link href="https://blog.280303.xyz/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, lingyi</rights>
  <subtitle>AI开发与创业实践</subtitle>
  <title>零壹技术博客</title>
  <updated>2026-06-02T07:49:27.291Z</updated>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="技术科普" scheme="https://blog.280303.xyz/categories/%E6%8A%80%E6%9C%AF%E7%A7%91%E6%99%AE/"/>
    <category term="运维" scheme="https://blog.280303.xyz/tags/%E8%BF%90%E7%BB%B4/"/>
    <category term="SSL" scheme="https://blog.280303.xyz/tags/SSL/"/>
    <category term="HTTPS" scheme="https://blog.280303.xyz/tags/HTTPS/"/>
    <category term="网络安全" scheme="https://blog.280303.xyz/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <content>
      <![CDATA[<p>上周有个朋友问我：”我想给网站上 HTTPS，一看证书价格，有免费的，有几十块的，还有上千上万的——这不都是加密吗？区别在哪？”</p><p>这个问题问到了点子上。作为一个踩过各种证书坑的开发者，今天就把 SSL 证书的定价逻辑掰开揉碎讲清楚。</p><p><img src="/images/ssl-cert-types/ssl-price-comparison.svg" alt="不同 SSL 证书类型及其价格差异"></p><blockquote><p><strong>一句话剧透</strong>：DV、OV、EV 三种证书的核心加密能力完全一样——都是 256 位 AES 加密。你多花的钱，买的是”验证深度”和”品牌信任”，不是更安全的密码学。</p></blockquote><hr><h2 id="先搞清楚：SSL-证书到底在干什么？"><a href="#先搞清楚：SSL-证书到底在干什么？" class="headerlink" title="先搞清楚：SSL 证书到底在干什么？"></a>先搞清楚：SSL 证书到底在干什么？</h2><p>SSL&#x2F;TLS 证书的核心作用其实就两件事：</p><ol><li><strong>加密通信</strong> —— 让浏览器和服务器之间的数据不被窃听或篡改</li><li><strong>身份证明</strong> —— 证明你正在访问的网站确实是它声称的那个实体</li></ol><p>其中第 1 点，所有证书做的一样好。差异全在第 2 点——<strong>验证你”是谁”的深入程度</strong>。</p><p>这就像办身份证：同样是一张卡片，街道办颁发的临时证明和公安局颁发的正式身份证，背后验证的严格程度完全不同。但在刷卡过闸机这个动作上，它们的效果是一样的。</p><hr><h2 id="三大证书类型：DV、OV、EV"><a href="#三大证书类型：DV、OV、EV" class="headerlink" title="三大证书类型：DV、OV、EV"></a>三大证书类型：DV、OV、EV</h2><p>业内把 SSL 证书按验证深度分为三个等级，我来逐一拆解。</p><p><img src="/images/ssl-cert-types/ssl-validation-depth.svg" alt="DV、OV、EV 的核心区别在于身份验证深度，而不是加密强度"></p><h3 id="DV-证书（域名验证型）——-免费到几十块"><a href="#DV-证书（域名验证型）——-免费到几十块" class="headerlink" title="DV 证书（域名验证型）—— 免费到几十块"></a>DV 证书（域名验证型）—— 免费到几十块</h3><p><strong>验证方式</strong>：只要证明你控制了该域名的管理权即可。通常通过以下方式之一验证：</p><ul><li>在域名 DNS 记录中添加特定 TXT 记录</li><li>在网站根目录放一个指定文件</li><li>通过域名注册邮箱（如 <a href="mailto:&#97;&#x64;&#x6d;&#105;&#110;&#64;&#x79;&#111;&#x75;&#x72;&#x64;&#x6f;&#x6d;&#x61;&#105;&#x6e;&#46;&#x63;&#x6f;&#109;">&#97;&#x64;&#x6d;&#105;&#110;&#64;&#x79;&#111;&#x75;&#x72;&#x64;&#x6f;&#x6d;&#x61;&#105;&#x6e;&#46;&#x63;&#x6f;&#109;</a>）回复确认邮件</li></ul><p>整个过程<strong>完全自动化</strong>，没有人工介入，几分钟就能签发。</p><p><strong>浏览器显示</strong>：地址栏出现小锁图标，HTTPS 前缀。</p><p><strong>适用场景</strong>：</p><ul><li>个人博客、作品集网站</li><li>内部系统、测试环境</li><li>对身份验证要求不高的信息展示型网站</li></ul><p><strong>价格范围</strong>：</p><table><thead><tr><th>类型</th><th>价格</th><th>典型提供商</th></tr></thead><tbody><tr><td>免费 DV</td><td><strong>¥0</strong></td><td>Let’s Encrypt, ZeroSSL</td></tr><tr><td>付费 DV</td><td><strong>¥30 - ¥150&#x2F;年</strong></td><td>Sectigo PositiveSSL, Comodo</td></tr><tr><td>通配符 DV</td><td><strong>¥300 - ¥600&#x2F;年</strong></td><td>保护 *.yoursite.com</td></tr></tbody></table><h3 id="OV-证书（组织验证型）——-几百到一千多"><a href="#OV-证书（组织验证型）——-几百到一千多" class="headerlink" title="OV 证书（组织验证型）—— 几百到一千多"></a>OV 证书（组织验证型）—— 几百到一千多</h3><p><strong>验证方式</strong>：除了验证域名所有权，还需<strong>核实企业或组织的真实身份</strong>：</p><ul><li>查验营业执照或组织机构代码</li><li>通过企业电话人工回访确认</li><li>核实申请人的授权身份</li></ul><p>这个过程不再是自动化，需要 CA（证书颁发机构）的审核团队<strong>人工参与</strong>，通常需要 1-3 个工作日。</p><p><strong>浏览器显示</strong>：小锁 + HTTPS，点击锁图标可以看到企业名称。</p><p><strong>适用场景</strong>：</p><ul><li>企业官网、电商平台</li><li>需要向访客展示”这是一个真实企业”的场景</li><li>会员登录系统、企业门户</li></ul><p><strong>价格范围</strong>：</p><table><thead><tr><th>产品</th><th>年费</th></tr></thead><tbody><tr><td>Sectigo OV（通过分销商）</td><td><strong>¥250 - ¥400&#x2F;年</strong></td></tr><tr><td>Namecheap PositiveSSL OV</td><td><strong>¥350&#x2F;年</strong> 左右</td></tr><tr><td>GoDaddy OV</td><td><strong>¥1,000+&#x2F;年</strong></td></tr><tr><td>DigiCert OV</td><td><strong>¥1,500&#x2F;年</strong> 左右</td></tr></tbody></table><h3 id="EV-证书（扩展验证型）——-一千到上万"><a href="#EV-证书（扩展验证型）——-一千到上万" class="headerlink" title="EV 证书（扩展验证型）—— 一千到上万"></a>EV 证书（扩展验证型）—— 一千到上万</h3><p><strong>验证方式</strong>：这是最高级别的验证，审核流程极其严苛：</p><ul><li>验证企业的法律存在（营业执照、公章）</li><li>核实企业的实际运营地址</li><li>确认申请人的授权代表身份</li><li>部分 CA 还会电话联系企业法人</li><li>整个过程通常需要 3-7 个工作日</li></ul><p><strong>浏览器显示</strong>：小锁 + HTTPS，部分浏览器（尤其是移动端和 Chrome 后续版本）曾经会在地址栏<strong>绿色显示企业名称</strong>。不过 2019 年后 Chrome 移除了 EV 的绿色地址栏标识，但这并不意味着 EV 没了价值——它的验证级别仍然是最高标准。</p><p><strong>适用场景</strong>：</p><ul><li>银行、证券、支付平台</li><li>大型电商平台（京东、淘宝早期都用 EV）</li><li>对安全合规有硬性要求的行业</li><li>需要对抗高精度钓鱼攻击的场景</li></ul><p><strong>价格范围</strong>：</p><table><thead><tr><th>产品</th><th>年费</th></tr></thead><tbody><tr><td>入门级 EV（通过分销商）</td><td><strong>¥500 - ¥1,000&#x2F;年</strong></td></tr><tr><td>DigiCert Basic EV</td><td><strong>¥2,500&#x2F;年</strong></td></tr><tr><td>DigiCert Secure Site EV</td><td><strong>¥7,000&#x2F;年</strong></td></tr><tr><td>DigiCert Secure Site Pro EV</td><td><strong>¥10,000+&#x2F;年</strong></td></tr></tbody></table><hr><h2 id="为什么价格差这么多？五个核心因素"><a href="#为什么价格差这么多？五个核心因素" class="headerlink" title="为什么价格差这么多？五个核心因素"></a>为什么价格差这么多？五个核心因素</h2><h3 id="1-验证成本——最根本的原因"><a href="#1-验证成本——最根本的原因" class="headerlink" title="1. 验证成本——最根本的原因"></a>1. 验证成本——最根本的原因</h3><p>这是价格差异的<strong>最大构成项</strong>。</p><p>DV 证书的验证是完全自动化的：CA 发一个挑战值给你，你在 DNS 或服务器上放一下，系统自动检查通过就签发。整个过程服务器跑一遍，<strong>边际成本趋近于零</strong>。这也是 Let’s Encrypt 敢免费送的底气——它是非盈利机构，ISRG 资助，自动化签发，没有人工成本。</p><p>而 OV 和 EV 需要<strong>人工审核团队</strong>：</p><ul><li>审核员需要接受专业培训并持有资质</li><li>每条验证记录需存档备查（合规要求）</li><li>EV 审核涉及多层复核，每一层都在烧人力成本</li></ul><p>一个做 CA 的朋友告诉我，<strong>签发一张 EV 证书的人工成本约在 30-50 美元</strong>，这就是为什么 EV 证书不可能免费。</p><h3 id="2-保险与赔付——买的是兜底"><a href="#2-保险与赔付——买的是兜底" class="headerlink" title="2. 保险与赔付——买的是兜底"></a>2. 保险与赔付——买的是兜底</h3><p>付费 SSL 证书（特别是 EV）都附带<strong>保险条款</strong>。如果因 CA 的失误导致证书错误签发，进而造成用户损失，CA 会进行赔偿：</p><table><thead><tr><th>证书类型</th><th>典型保额</th></tr></thead><tbody><tr><td>免费 DV</td><td><strong>无</strong>或极低（通常不超过几千美元）</td></tr><tr><td>付费 OV</td><td><strong>$10,000 - $50,000</strong></td></tr><tr><td>高端 EV</td><td><strong>$250,000 - $2,000,000</strong></td></tr></tbody></table><p>这笔保险费用，当然也摊进了售价里。</p><blockquote><p>不过说实话，真正触发赔付的案例极少——CA 签错证书的几率远小于航空公司丢行李的概率。与其说保险是实用性保障，不如说它是”信任成本的具象化”。</p></blockquote><h3 id="3-品牌溢价——买的是认知"><a href="#3-品牌溢价——买的是认知" class="headerlink" title="3. 品牌溢价——买的是认知"></a>3. 品牌溢价——买的是认知</h3><p>这一点很微妙但很真实。</p><p><strong>DigiCert</strong> 和 <strong>GlobalSign</strong> 这些老牌 CA 在业界有几十年的品牌积累。当一家银行选择 DigiCert EV 时，它买的不仅仅是证书，还有 DigiCert 的品牌背书——万一出事，审计师问”为什么选这家 CA”，回答”DigiCert”比回答”某个没听说过的分销商”要安全得多。</p><p>而 <code>Sectigo</code>（原 Comodo）等品牌的证书通过大量分销商（Namecheap、SSLs.com、国产厂商）销售，渠道成本低，价格自然下来了。</p><h3 id="4-通配符与多域名——按覆盖范围溢价"><a href="#4-通配符与多域名——按覆盖范围溢价" class="headerlink" title="4. 通配符与多域名——按覆盖范围溢价"></a>4. 通配符与多域名——按覆盖范围溢价</h3><p>这是最容易理解的定价因素：</p><ul><li><strong>单域名证书</strong>：只保护 <code>yoursite.com</code></li><li><strong>通配符证书</strong>：保护 <code>yoursite.com</code> 及所有一级子域名（<code>*.yoursite.com</code>）</li><li><strong>多域名证书</strong>（SAN&#x2F;UCC）：一张证书保护多个不同的域名</li></ul><p>通配符和多域名的管理工作量比单域名证书大得多，价格自然也水涨船高。</p><blockquote><p>一些小技巧：如果你有多个子域名，但可以接受分别管理，用免费 DV + 自动化脚本续期，总成本为零。反之，如果想省事，一张通配符证书更划算。</p></blockquote><h3 id="5-有效期与续期管理"><a href="#5-有效期与续期管理" class="headerlink" title="5. 有效期与续期管理"></a>5. 有效期与续期管理</h3><p>Let’s Encrypt 的免费证书<strong>有效期 90 天</strong>，需要每 90 天续期一次。虽然可以自动续期（用 acme.sh 或 Certbot），但仍有翻车的可能——证书过期导致网站打不开，这种事故我见过不止一次。</p><p>付费证书的有效期通常为 <strong>1-2 年</strong>（行业标准，最长不超过 398 天，CA&#x2F;B 论坛有规定），管理压力小很多。</p><p>有些高价证书还附带<strong>部署监控、到期提醒、技术支持</strong>等增值服务，这些也都是成本。</p><p><img src="/images/ssl-cert-types/ssl-pricing-factors.svg" alt="影响 SSL 证书价格的五个核心因素：验证成本、保险、品牌、覆盖范围和生命周期服务"></p><hr><h2 id="一张表看清所有区别"><a href="#一张表看清所有区别" class="headerlink" title="一张表看清所有区别"></a>一张表看清所有区别</h2><table><thead><tr><th>维度</th><th>免费 DV</th><th>付费 DV</th><th>OV</th><th>EV</th></tr></thead><tbody><tr><td><strong>价格（&#x2F;年）</strong></td><td>¥0</td><td>¥30 - ¥150</td><td>¥250 - ¥1,500</td><td>¥500 - ¥10,000+</td></tr><tr><td><strong>验证内容</strong></td><td>域名所有权</td><td>域名所有权</td><td>域名 + 企业身份</td><td>域名 + 最严格的企业审核</td></tr><tr><td><strong>审核方式</strong></td><td>全自动</td><td>全自动</td><td>人工审核</td><td>多层人工+法律核查</td></tr><tr><td><strong>签发速度</strong></td><td>几分钟</td><td>几分钟</td><td>1-3 天</td><td>3-7 天</td></tr><tr><td><strong>浏览器显示</strong></td><td>🔒 HTTPS</td><td>🔒 HTTPS</td><td>🔒 + 企业名称</td><td>🔒 + 完整企业信息</td></tr><tr><td><strong>保额</strong></td><td>无</td><td>低</td><td>中</td><td>高</td></tr><tr><td><strong>适用对象</strong></td><td>个人博客、测试</td><td>个人站点、小型项目</td><td>企业官网、电商</td><td>金融、支付、大型平台</td></tr></tbody></table><hr><h2 id="2026-年，我怎么选？"><a href="#2026-年，我怎么选？" class="headerlink" title="2026 年，我怎么选？"></a>2026 年，我怎么选？</h2><p>这里给几条实际建议，按场景划分：</p><p><img src="/images/ssl-cert-types/ssl-selection-matrix.svg" alt="按业务场景选择 SSL 证书：博客选 DV，企业官网偏 OV，金融支付用 EV"></p><p><strong>个人博客 &#x2F; 技术文档 &#x2F; 展示型站点</strong><br>→ <strong>Let’s Encrypt 免费 DV</strong> 就够。搭配 acme.sh 做自动续期，零成本、零维护。如果嫌 90 天续期麻烦，花几十块买个 1 年的 DV 省心。</p><p><strong>小型创业团队 &#x2F; 内部系统</strong><br>→ 付费 <strong>DV 或 OV</strong>。如果面向开发者、不涉及资金交易，DV 完全够用。如果客户能看到你的品牌名（SaaS 界面），OV 更稳妥。</p><p><strong>企业官网 &#x2F; B2B 平台</strong><br>→ <strong>OV 证书</strong>。访客能看到你的企业经过认证，对商务场景尤其重要。Namecheap 或国内分销商（如 亚洲诚信 TrustAsia）的 OV 性价比很高。</p><p><strong>金融 &#x2F; 支付 &#x2F; 大型电商</strong><br>→ <strong>EV 证书</strong>。虽然 Chrome 不再显示绿色地址栏，但 EV 的审核严格程度仍是最高级别的，在合规审计和商务信任上不可替代。</p><blockquote><p>一个容易被忽略的点：<strong>Google 曾明确表示 HTTPS（无论证书类型）是搜索排名信号</strong>。所以即使你的网站不需要交易，为 SEO 考虑，HTTPS 也应该是标配——免费 DV 就能满足这个需求。</p></blockquote><hr><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>SSL 证书市场的定价逻辑，拆开来看其实不复杂：</p><ul><li>加密能力 &#x3D; <strong>都一样</strong>（所有证书都用 TLS 协议）</li><li>验证深度 &#x3D; <strong>付多少钱就验证到多深</strong></li><li>品牌溢价 &#x3D; <strong>大型 CA 的品牌背书值钱</strong></li><li>保险兜底 &#x3D; <strong>买的是出事时的赔偿承诺</strong></li></ul><p>对普通开发者来说，明确定位、选对类型就行，完全没必要为用不上的服务买单。<strong>最低门槛是免费 DV，中等需求选 OV，硬性合规上 EV</strong>——就这么简单。</p><p>如果你现在还在用 HTTP，花五分钟配个 Let’s Encrypt，给自己的域名加把锁，这是 2026 年最划算的投入了。</p><hr><p><em>本文数据基于 2026 年 6 月市场行情整理，具体价格请以各 CA 官网及分销商报价为准。</em></p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/ssl-certificate-types-dv-ov-ev-price-differences/</id>
    <link href="https://blog.280303.xyz/posts/ssl-certificate-types-dv-ov-ev-price-differences/"/>
    <published>2026-06-02T03:36:00.000Z</published>
    <summary>DV、OV、EV 三种证书，价格从 0 到上万不等。核心加密能力完全一样，差异藏在验证流程、商业保障和品牌溢价里。这篇文章帮你一次看透 SSL 证书的定价逻辑。</summary>
    <title>SSL 证书价格之谜：为什么有的免费、有的几十块、有的上千？</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="开源" scheme="https://blog.280303.xyz/categories/%E5%BC%80%E6%BA%90/"/>
    <category term="开源协议" scheme="https://blog.280303.xyz/tags/%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE/"/>
    <category term="MIT" scheme="https://blog.280303.xyz/tags/MIT/"/>
    <category term="GPL" scheme="https://blog.280303.xyz/tags/GPL/"/>
    <category term="Apache" scheme="https://blog.280303.xyz/tags/Apache/"/>
    <category term="License" scheme="https://blog.280303.xyz/tags/License/"/>
    <content>
      <![CDATA[<p>每次在 GitHub 上新建仓库，到了 “Choose a license” 那一步，很多人就卡住了。MIT、GPL、Apache、BSD…… 一堆名字摆在那，看着都差不多，选错了又怕出事。</p><p>其实没你想的那么复杂。开源协议的核心问题就两个：</p><ol><li><strong>别人能拿你的代码卖钱吗？</strong></li><li><strong>别人改完代码，需不需要把改动也公开？</strong></li></ol><p>搞懂这两个问题，选协议就跟点菜一样简单。</p><h2 id="MIT-—-最简单的选择，没有之一"><a href="#MIT-—-最简单的选择，没有之一" class="headerlink" title="MIT — 最简单的选择，没有之一"></a>MIT — 最简单的选择，没有之一</h2><p>MIT 协议短到什么程度？原文就二十来行，核心意思一句话：<strong>爱怎么用怎么用，出事别找我，署个名就行</strong>。</p><p>你想抄、想改、想塞进闭源商业软件卖钱——都可以。唯一的要求就是保留原作者的版权声明。</p><p>所以大厂最爱 MIT。React 是 MIT、jQuery 是 MIT、Node.js 早期也是 MIT。无拘无束，不用请律师审条款。</p><p><strong>适合谁</strong>：想让代码传播得最广、不想管别人怎么用、追求省事。大部分人选 MIT 都不会错。</p><p><strong>代价是什么</strong>：你写了个爆款库，Facebook 拿去塞进 React Native 闭源卖钱，你一分拿不到，也说不了什么。MIT 就是这样——给了别人最大的自由。</p><h2 id="BSD-—-和-MIT-差不多，多了个防碰瓷条款"><a href="#BSD-—-和-MIT-差不多，多了个防碰瓷条款" class="headerlink" title="BSD — 和 MIT 差不多，多了个防碰瓷条款"></a>BSD — 和 MIT 差不多，多了个防碰瓷条款</h2><p>BSD 有两个版本：</p><ul><li><strong>BSD 2-Clause</strong>：跟 MIT 几乎一样，随便用，署名就行</li><li><strong>BSD 3-Clause</strong>：多了一条 “别拿我的名字给你的产品打广告”</li></ul><p>3-Clause 那条很有用。有人拿你的代码做了个产品，满世界宣传 “基于 XXX 核心技术”，结果出 bug 了用户跑来骂你——有了这条，你可以理直气壮说关我屁事。</p><p>Go 语言用的 BSD、Nginx 用的 BSD、Redis 用的也是 BSD。</p><h2 id="Apache-2-0-—-MIT-的升级版，大厂最爱"><a href="#Apache-2-0-—-MIT-的升级版，大厂最爱" class="headerlink" title="Apache 2.0 — MIT 的升级版，大厂最爱"></a>Apache 2.0 — MIT 的升级版，大厂最爱</h2><p>Apache 2.0 跟 MIT 一样宽松——可以闭源商用、可以随便改——但它多了两个 MIT 没有的东西：</p><p><strong>专利授权</strong>：你用 MIT 协议的代码，理论上贡献者哪天不高兴了可以告你专利侵权。Apache 2.0 明确说 “我不会告你”，这对公司来说太重要了。</p><p><strong>修改声明</strong>：你改了我的代码，不能假装就是你写的。得告诉别人你改了什么。</p><p>Google 的 Android、Kubernetes、Spring 全家桶、Hadoop 全线都是 Apache 2.0。大厂选它不是因为更严格，而是因为专利条款让法务部睡得着觉。</p><h2 id="GPL-—-Copyleft-的灵魂"><a href="#GPL-—-Copyleft-的灵魂" class="headerlink" title="GPL — Copyleft 的灵魂"></a>GPL — Copyleft 的灵魂</h2><p>Richard Stallman 当年写 GPL 的时候想法很简单：<strong>你用我的代码，你的代码也得开源</strong>。</p><p>这个叫 Copyleft。不是说你不能商用——你可以卖钱，但你卖的时候必须把源码一起给客户，客户拿到源码后也可以继续传播。</p><p>关键是<strong>传染性</strong>。你写了段 GPL 代码，别人在你的代码基础上做了个新软件，那新软件也得 GPL。不管你是直接复制还是链接引用——沾上了就甩不掉。</p><p>Linux 内核是 GPL v2，Git 也是 GPL v2，WordPress 也是。为什么这些项目选 GPL？因为它们不想被闭源分叉吃掉。你今天 fork 了 Linux 加了一堆闭源特性卖钱？不行，你得开源。</p><p><strong>GPL v2 vs v3</strong>：v2 短小精悍，Linux 内核一直用 v2。v3 加了专利保护和一个叫 “反 Tivoization” 的东西——有些厂商虽然开源了代码，但用硬件签名锁死，你改了代码也跑不了。v3 把这条路也堵了。但 v3 更复杂，很多人不太愿意碰。</p><p><strong>⚠️ 实话说</strong>：GPL 在商业世界口碑不太好。很多公司法务听到 GPL 就头大，合规审查极其麻烦。你选 GPL 基本等于告诉大公司 “别用我的代码”。</p><h2 id="LGPL-—-专门给库准备的"><a href="#LGPL-—-专门给库准备的" class="headerlink" title="LGPL — 专门给库准备的"></a>LGPL — 专门给库准备的</h2><p>GPL 的传染性有个问题：如果我用的是 C 语言的 glibc，只要链接了它我的软件就得 GPL？那所有 Linux 软件都得开源了。</p><p>所以 GNU 搞了个 LGPL（Lesser GPL），规则变成：你的程序<strong>动态链接</strong>这个库，不需要开源。但如果<strong>修改了库本身</strong>，修改部分必须开源。</p><p>Glibc 是 LGPL、FFmpeg 有 LGPL 版本、Qt 早期也是 LGPL。</p><p><strong>适合谁</strong>：你写了个底层库，希望大家都来用（包括闭源项目），但不想库本身被人改了不还回来。</p><h2 id="AGPL-—-你跑在服务器上也得开源"><a href="#AGPL-—-你跑在服务器上也得开源" class="headerlink" title="AGPL — 你跑在服务器上也得开源"></a>AGPL — 你跑在服务器上也得开源</h2><p>传统 GPL 有个巨大的漏洞：<strong>我搭了个网站用你的 GPL 代码，用户只是通过浏览器访问，算不算 “分发” 软件？不算。那我的修改就不用公开。</strong></p><p>AGPL 直接把这条补死了。只要有人通过网络使用你的服务，你就得提供源码。</p><p>MongoDB 早期用的就是 AGPL（后来改成了更严格的 SSPL），Nextcloud 也用 AGPL。做 SaaS 的朋友对这个协议最有感触——你辛辛苦苦写了个服务，别人拿去改改就部署上线赚钱，还不用开源——AGPL 就是防这个的。</p><p>当然，AGPL 比 GPL 还让公司害怕。很多开源项目的 README 里直接写 “We’re AGPL，commercial license available”，意思很明白：不想开源就付钱。</p><h2 id="MPL-—-折中方案"><a href="#MPL-—-折中方案" class="headerlink" title="MPL — 折中方案"></a>MPL — 折中方案</h2><p>MPL（Mozilla Public License）想解决一个问题：GPL 太激进，MIT 太松，有没有中间选项？</p><p>答案是有。MPL 是<strong>文件级别的传染</strong>。你改了 <code>foo.js</code>，那这个 <code>foo.js</code> 必须保持 MPL 开源。但项目的其他文件可以闭源。</p><p>Firefox 就是用 MPL 的。</p><p><strong>适合谁</strong>：你想要一定的保护，但项目里有部分私有代码不想公开。MPL 给你划了一条清晰的线。</p><h2 id="实战选择指南"><a href="#实战选择指南" class="headerlink" title="实战选择指南"></a>实战选择指南</h2><p>现在回过头来看，选协议其实就是选项目的性格：</p><p>你的项目是个<strong>工具库</strong>，希望被全世界引用 —— MIT 或 Apache 2.0。不用想太多，选这俩永远不会错。</p><p>你在<strong>大厂工作</strong>，或者项目可能被大厂用 —— Apache 2.0。专利条款能免去跟法务部扯皮的麻烦。</p><p>你<strong>不想被闭源白嫖</strong> —— GPL。代价是商业公司大概率绕着你走。</p><p>你做了个<strong>SaaS 产品</strong>，怕别人抄 —— AGPL。但也别指望它被广泛采用。</p><p>你写了<strong>底层基础设施</strong>，希望生态繁荣 —— LGPL 或 MIT。</p><p>你想要<strong>中间路线</strong>，部分开源部分闭源 —— MPL。</p><h2 id="几个容易踩的坑"><a href="#几个容易踩的坑" class="headerlink" title="几个容易踩的坑"></a>几个容易踩的坑</h2><p><strong>没有协议 &#x3D; 没有授权</strong>。GitHub 上很多项目不放 License 文件，这不等于 “大家都来用”——法律上恰恰相反，默认保留所有权利，别人什么都不能做。</p><p><strong>协议不兼容是个大问题</strong>。GPL v2 和 Apache 2.0 就互不兼容——专利条款有冲突。你混用了这两个协议的代码，基本算违规。选之前去 choosealicense.com 查一下兼容性。</p><p><strong>别自己写协议</strong>。网上有人喜欢自己写个 “XX 开源协议”，看着很酷，但法律上没人认。OSI 批准的协议很多年了，经历了大量判例检验，选现成的比什么都强。</p><p><strong>双许可很常见</strong>。MySQL 就是 GPL + 商业许可两条路。你个人用、开源项目用，走 GPL 免费。公司想闭源集成，付钱买商业许可。Qt、MongoDB 都这么玩。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>开源协议没有最好，只有最合适。搞清楚你到底在意什么——是传播广度、商业友好、还是衍生作品必须开源——答案自然就有了。</p><p>如果真的拿不准，选 MIT。大多数时候够用了。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/open-source-license-guide/</id>
    <link href="https://blog.280303.xyz/posts/open-source-license-guide/"/>
    <published>2026-06-01T23:31:13.000Z</published>
    <summary>
      <![CDATA[<p>每次在 GitHub 上新建仓库，到了 “Choose a license” 那一步，很多人就卡住了。MIT、GPL、Apache、BSD…… 一堆名字摆在那，看着都差不多，选错了又怕出事。</p>
<p>其实没你想的那么复杂。开源协议的核心问题就两个：</p>
<ol>]]>
    </summary>
    <title>开源协议怎么选？MIT、GPL、Apache 到底有什么区别</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="工程实践" scheme="https://blog.280303.xyz/categories/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <category term="Git" scheme="https://blog.280303.xyz/tags/Git/"/>
    <category term="SVN" scheme="https://blog.280303.xyz/tags/SVN/"/>
    <category term="分支管理" scheme="https://blog.280303.xyz/tags/%E5%88%86%E6%94%AF%E7%AE%A1%E7%90%86/"/>
    <category term="团队协作" scheme="https://blog.280303.xyz/tags/%E5%9B%A2%E9%98%9F%E5%8D%8F%E4%BD%9C/"/>
    <content>
      <![CDATA[<p><img src="/images/svn-to-git-branch-workflow/svn-git-release-hero.png" alt="发版前的分支梳理现场"></p><p>公司现在的代码管理还是 SVN。严格说，SVN 不是不能用，它稳定、集中、权限直观，在一些传统项目里也跑了很多年。问题在于，我们现在的开发方式已经不是当年那种“少数人、少量改动、按计划提交”的节奏了。</p><p>现在更常见的场景是：很多人同时在同一个分支上开发。上午你改订单，我改库存，另一个同事改登录，大家都在一条线上施工。平时还好，一到发版前就开始紧张：有人说“我这个功能还没改完，不能提交”；有人说“我现在提交会报错”；还有人下班前才把一天的代码一次性提交上来，冲突像攒了一天的账单，晚上统一结算。</p><p>这类问题表面看是个人习惯不好，实际上更像是工具和流程一起把人推到了尴尬位置。</p><h2 id="同一个分支，所有人都没有缓冲区"><a href="#同一个分支，所有人都没有缓冲区" class="headerlink" title="同一个分支，所有人都没有缓冲区"></a>同一个分支，所有人都没有缓冲区</h2><p>在 SVN 单分支协作里，主干既是开发区，又是集成区，很多时候还临时承担了准发布区的角色。这个设计一旦碰上多人并行开发，就会天然产生几个矛盾。</p><p>第一个矛盾是“代码没写完，但又需要保存进度”。开发人员本地改了一堆文件，功能还没完成，提交上去可能影响别人，不提交又只能在自己机器上扛着。时间一长，本地工作区越来越脏，合并别人的代码也越来越胆战心惊。</p><p>第二个矛盾是“发版要稳定，但主干一直在变”。发版当天大家都盯着同一条分支，有人要修 bug，有人功能还差一点，有人已经提交了一半。最后发布负责人只能一边催，一边问：“现在这个版本到底能不能打包？”</p><p>第三个矛盾是“冲突被推迟了”。早上开始写代码，中间不怎么提交，下班前一次性提交一天的改动，看似省事，其实只是把冲突、编译错误、逻辑冲突都往后拖。等到真正提交时，冲突已经不是几行代码的问题，而是一整天上下文的碰撞。</p><p><img src="/images/svn-to-git-branch-workflow/daily-commit-conflict.png" alt="一天代码攒到下班才提交，冲突只会变成一大坨"></p><p>我见过不少团队把这种行为归因成“开发不规范”。当然，习惯要改，但也要承认：如果一个工具链让大家很难创建分支、很难切换分支、很难舒服地合并，那最后大家自然会选择最省操作的方式，哪怕这个方式长期看很痛。</p><h2 id="SVN-也有分支，为什么大家还是不用"><a href="#SVN-也有分支，为什么大家还是不用" class="headerlink" title="SVN 也有分支，为什么大家还是不用"></a>SVN 也有分支，为什么大家还是不用</h2><p>SVN 当然可以创建分支。理论上，<code>trunk</code>、<code>branches</code>、<code>tags</code> 也能组织出不错的流程。但落到日常开发里，很多团队还是很少用 SVN 分支，原因通常不是“不知道有这个功能”，而是“不好用、不敢用、嫌麻烦”。</p><p>尤其是依赖 GUI 工具时，创建分支、切换分支、合并回主干这些操作，经常显得重。分支在 SVN 里更像是服务器上的一个目录副本，合并历史和冲突处理也容易让人没有安全感。对于不常操作分支的同事来说，每次合并都像一次小型发布。</p><p>于是团队最后形成了一个很典型的惯性：既然分支麻烦，那就都在主干上改；既然主干不能随便坏，那就等功能差不多了再提交；既然大家都晚点提交，那冲突就集中在下班前和发版前爆出来。</p><p>这不是某个人偷懒，而是流程在奖励“大提交”和“晚集成”。</p><h2 id="Git-分支真正解决的是什么"><a href="#Git-分支真正解决的是什么" class="headerlink" title="Git 分支真正解决的是什么"></a>Git 分支真正解决的是什么</h2><p>Git 最大的变化不是命令更酷，也不是大家可以在终端里多敲几行命令。它真正改变的是：分支变得足够便宜，便宜到你愿意每天用。</p><p>在 Git 里，创建一个功能分支通常就是一秒钟的事情：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git checkout -b feature/order-refund<br></code></pre></td></tr></table></figure><p>这意味着每个功能都可以有自己的工作空间。功能没写完，没有关系，先提交到自己的分支；代码还会报错，也不要紧，只要别合进主分支；需要同步别人的改动，就从 <code>main</code> 拉一下再 rebase 或 merge。主分支不再承担所有人的半成品，它只接收经过检查的变更。</p><p>一个更健康的日常节奏大概是这样：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash">git checkout -b feature/order-refund<br>git add .<br>git commit -m <span class="hljs-string">&quot;feat: add refund order status&quot;</span><br>git fetch origin<br>git rebase origin/main<br>git push -u origin feature/order-refund<br></code></pre></td></tr></table></figure><p>这里的重点不是命令本身，而是工作方式变了。你可以一天提交多次，每次只提交一个明确的小改动。提交不再等于“我要影响所有人”，而是“我把当前阶段的工作保存下来”。真正影响主分支的动作，发生在 Pull Request 或 Merge Request 被合并的时候。</p><p><img src="/images/svn-to-git-branch-workflow/svn-single-branch-vs-git-flow.png" alt="SVN 单线协作和 Git 分支协作的区别"></p><h2 id="发版时，未完成的功能不应该挡住所有人"><a href="#发版时，未完成的功能不应该挡住所有人" class="headerlink" title="发版时，未完成的功能不应该挡住所有人"></a>发版时，未完成的功能不应该挡住所有人</h2><p>发版最怕一句话：“我这个功能还没改完，不能提交。”</p><p>在 SVN 单分支模式下，这句话很致命。因为大家都在同一条线上，某个人的半成品可能真的会影响整个发布节奏。但在 Git 分支模式下，这句话应该变成：“这个功能还没完成，所以这次不合进发布分支。”</p><p>常见做法是：</p><ul><li><code>main</code> 保持随时可运行；</li><li>每个需求从 <code>main</code> 拉出 <code>feature/*</code> 分支；</li><li>发版前从稳定的 <code>main</code> 拉出 <code>release/*</code> 分支；</li><li>发布分支只接受明确的 bugfix；</li><li>没做完的功能继续留在自己的功能分支；</li><li>线上紧急问题从 <code>main</code> 或发布 tag 拉出 <code>hotfix/*</code> 分支。</li></ul><p>比如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">git checkout -b release/2026-06-01 origin/main<br>git cherry-pick &lt;fix-commit&gt;<br></code></pre></td></tr></table></figure><p>这样发版就不再是“等所有人都刚好写完”，而是“把已经完成、已经验证的内容发布出去”。未完成的功能不会消失，也不会被粗暴打断，只是不会混进这次发布。</p><h2 id="分支不是按人分，而是按事情分"><a href="#分支不是按人分，而是按事情分" class="headerlink" title="分支不是按人分，而是按事情分"></a>分支不是按人分，而是按事情分</h2><p>有些团队刚切到 Git 时，会把 SVN 的思维原封不动搬过来：每个人一个长期分支，月底再合一次。这样做很容易把 Git 也用成另一种形式的单分支痛苦。</p><p>更推荐的方式是按任务建分支，而不是按人建分支：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">feature/order-refund<br>feature/invoice-export<br>bugfix/login-timeout<br>hotfix/payment-callback<br>release/2026-06-01<br></code></pre></td></tr></table></figure><p>一个分支对应一件清晰的事情。事情完成了，代码评审通过了，自动化检查过了，就合回主分支，然后删除这个分支。分支活得短，风险就小；分支拖得久，冲突和认知成本都会上升。</p><h2 id="Git-不是万能药，但它给团队留出了操作空间"><a href="#Git-不是万能药，但它给团队留出了操作空间" class="headerlink" title="Git 不是万能药，但它给团队留出了操作空间"></a>Git 不是万能药，但它给团队留出了操作空间</h2><p>换 Git 之后，问题不会自动消失。大提交还是可以大提交，长期分支还是可以长期不合，主分支也还是可能被提交坏。工具只能提供更好的可能性，真正让协作变顺的，是团队愿意一起调整规则。</p><p>我会建议先从几条简单规则开始：</p><ul><li>主分支必须保持可编译、可启动、可测试；</li><li>一个需求一个功能分支，不在主分支直接开发大功能；</li><li>每天多次小提交，不把一天代码攒到最后；</li><li>合并前必须同步主分支，解决冲突后再提合并请求；</li><li>合并请求里要有人看代码，至少看风险点和影响范围；</li><li>CI 不通过不合并；</li><li>发布分支只收修复，不收临时加塞的大功能。</li></ul><p>这些规则看起来朴素，但它们能把“发版前集体焦虑”拆成每天可处理的小问题。</p><h2 id="迁移时别一口吃太满"><a href="#迁移时别一口吃太满" class="headerlink" title="迁移时别一口吃太满"></a>迁移时别一口吃太满</h2><p>从 SVN 切到 Git，最怕一上来就讲一堆复杂模型：Git Flow、Trunk Based Development、rebase、squash、cherry-pick、tag 策略、权限策略、CI&#x2F;CD 全家桶。概念越多，团队越容易觉得这是另一套负担。</p><p>比较稳的方式是先小范围试点：</p><ol><li>选一个活跃但风险可控的项目迁到 Git；</li><li>约定 <code>main</code>、<code>feature/*</code>、<code>release/*</code>、<code>hotfix/*</code> 四类分支；</li><li>让大家先熟悉创建分支、提交、推送、提合并请求；</li><li>给主分支加最基本的构建检查；</li><li>发一次小版本，复盘冲突、回滚、修复流程；</li><li>再逐步补充代码评审规范和 CI 规则。</li></ol><p>不要试图第一天就把所有流程设计到完美。团队真正需要的不是一份漂亮流程图，而是每个人第二天上班真的愿意照着做。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>SVN 单分支协作最大的问题，不是它不能提交代码，而是它让“提交”这件事变得太沉重：没写完不敢交，怕影响别人不敢交，发版前更不敢交。于是大家都把风险留在本地，等到某个时间点一起爆出来。</p><p>Git 分支管理的价值，是把半成品、已完成、待发布、线上修复这些状态分开，让每一种代码都有合适的位置。它不是为了让流程显得高级，而是为了让开发者可以更早提交、更小步集成、更从容地发版。</p><p>工具该服务人，而不是让人每天靠胆量维护主干。对于多人并行开发的团队来说，从 SVN 单分支走向 Git 分支管理，不只是换一个仓库地址，更是把协作方式从“大家挤在一条路上抢时间”，改成“每件事有自己的车道，最后有秩序地汇入主路”。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/from-svn-single-branch-to-git-branch-workflow/</id>
    <link href="https://blog.280303.xyz/posts/from-svn-single-branch-to-git-branch-workflow/"/>
    <published>2026-06-01T09:00:00.000Z</published>
    <summary>当所有人都挤在 SVN 同一个分支上开发，冲突、发版阻塞和“我还不能提交”就不是偶发现象，而是协作模型本身在提醒团队该升级了。</summary>
    <title>从 SVN 单分支协作到 Git 分支管理：别再把一天的代码攒到下班提交了</title>
    <updated>2026-06-01T08:49:12.981Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="GIS" scheme="https://blog.280303.xyz/categories/GIS/"/>
    <category term="Cesium" scheme="https://blog.280303.xyz/tags/Cesium/"/>
    <category term="天地图" scheme="https://blog.280303.xyz/tags/%E5%A4%A9%E5%9C%B0%E5%9B%BE/"/>
    <category term="WebGIS" scheme="https://blog.280303.xyz/tags/WebGIS/"/>
    <category term="三维地球" scheme="https://blog.280303.xyz/tags/%E4%B8%89%E7%BB%B4%E5%9C%B0%E7%90%83/"/>
    <category term="踩坑记录" scheme="https://blog.280303.xyz/tags/%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/"/>
    <content>
      <![CDATA[<h2 id="一个看似简单的需求"><a href="#一个看似简单的需求" class="headerlink" title="一个看似简单的需求"></a>一个看似简单的需求</h2><p>事情开始得很普通。</p><p>项目需要在 Cesium 三维地球上显示国内城市的卫星影像底图。默认的 Bing Maps 影像在国内加载慢，而且行政边界和地名标注不符合国内项目的要求。</p><p>方案很明确——换成天地图。国家地理信息公共服务平台，数据权威，国内访问快，免费。</p><p>然后我花了一天时间，在四个地方卡住。</p><p>这篇文章不是什么”三分钟快速接入”教程。是把我踩过的坑、排查的思路、最终能稳定运行的技术方案，一次说清楚。如果你也在做 Cesium + 天地图的集成，应该能帮你省掉大半天的时间。</p><span id="more"></span><h2 id="场景：为什么非要用天地图"><a href="#场景：为什么非要用天地图" class="headerlink" title="场景：为什么非要用天地图"></a>场景：为什么非要用天地图</h2><p>先交代一下项目背景，方便你判断后面的内容是否对你有用。</p><p>这是一个智慧城市管理平台，Cesium 作为三维底座，上面叠加了建筑白模、实时传感器数据和规划红线。需求很明确：</p><ul><li>底图必须是国内权威地图源，满足政务项目数据合规要求</li><li>影像要清晰，更新不能太旧（Bing Maps 在一些区域是两三年前的影像）</li><li>要有地名注记，但不喧宾夺主</li><li>加载速度要快，不能拖慢整个场景的初始化</li></ul><p>天地图天然满足前两个条件。但接入过程中，几个技术细节坑了不少人。</p><h2 id="坑一：img-w-还是-img-c——投影的陷阱"><a href="#坑一：img-w-还是-img-c——投影的陷阱" class="headerlink" title="坑一：img_w 还是 img_c——投影的陷阱"></a>坑一：img_w 还是 img_c——投影的陷阱</h2><p>这是第一个坑，也是最容易踩的。</p><p>天地图的在线服务分两套投影：</p><table><thead><tr><th>后缀</th><th>投影</th><th>EPSG</th><th align="center">Cesium 默认支持</th></tr></thead><tbody><tr><td><code>_w</code></td><td>Web Mercator</td><td>EPSG:3857</td><td align="center">✅ 直接支持</td></tr><tr><td><code>_c</code></td><td>经纬度 (Lat&#x2F;Lon)</td><td>EPSG:4326</td><td align="center">⚠️ 需额外配置</td></tr></tbody></table><p>两种投影天地图都提供，对应不同的瓦片组织方式。</p><h3 id="踩坑现场"><a href="#踩坑现场" class="headerlink" title="踩坑现场"></a>踩坑现场</h3><p>我第一次接入时，在网上随便找了一段代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">const</span> provider = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">UrlTemplateImageryProvider</span>(&#123;<br>  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t0.tianditu.gov.cn/DataServer?T=img_c&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27;</span> + token,<br>  <span class="hljs-attr">tilingScheme</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">WebMercatorTilingScheme</span>(),<br>  <span class="hljs-attr">maximumLevel</span>: <span class="hljs-number">18</span><br>&#125;);<br></code></pre></td></tr></table></figure><p>结果——地图能出来，但位置偏了。瓦片偏移，在三维球上明显错位。</p><p>原因是 <code>img_c</code> 用的是经纬度投影（EPSG:4326），而我却告诉 Cesium 这是 Web Mercator 投影。投影不匹配，瓦片坐标就全错了。</p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p><strong>方法一：用 <code>img_w</code>（推荐）</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">const</span> provider = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">UrlTemplateImageryProvider</span>(&#123;<br>  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t&#123;s&#125;.tianditu.gov.cn/DataServer?T=img_w&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27;</span> + token,<br>  <span class="hljs-attr">subdomains</span>: [<span class="hljs-string">&#x27;0&#x27;</span>, <span class="hljs-string">&#x27;1&#x27;</span>, <span class="hljs-string">&#x27;2&#x27;</span>, <span class="hljs-string">&#x27;3&#x27;</span>, <span class="hljs-string">&#x27;4&#x27;</span>, <span class="hljs-string">&#x27;5&#x27;</span>, <span class="hljs-string">&#x27;6&#x27;</span>, <span class="hljs-string">&#x27;7&#x27;</span>],<br>  <span class="hljs-attr">tilingScheme</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">WebMercatorTilingScheme</span>(),<br>  <span class="hljs-attr">maximumLevel</span>: <span class="hljs-number">18</span><br>&#125;);<br></code></pre></td></tr></table></figure><p><code>img_w</code> 是 Web Mercator 投影，与 Cesium 默认的 <code>WebMercatorTilingScheme</code> 天然匹配。一行不改，直接跑。</p><p><strong>方法二：用 <code>img_c</code> + 匹配的 TilingScheme</strong></p><p>如果项目确实需要经纬度投影（比如需要精确的经纬度坐标对齐），把 TilingScheme 换成 <code>GeographicTilingScheme</code>：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">const</span> provider = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">UrlTemplateImageryProvider</span>(&#123;<br>  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t&#123;s&#125;.tianditu.gov.cn/DataServer?T=img_c&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27;</span> + token,<br>  <span class="hljs-attr">subdomains</span>: [<span class="hljs-string">&#x27;0&#x27;</span>, <span class="hljs-string">&#x27;1&#x27;</span>, <span class="hljs-string">&#x27;2&#x27;</span>, <span class="hljs-string">&#x27;3&#x27;</span>, <span class="hljs-string">&#x27;4&#x27;</span>, <span class="hljs-string">&#x27;5&#x27;</span>, <span class="hljs-string">&#x27;6&#x27;</span>, <span class="hljs-string">&#x27;7&#x27;</span>],<br>  <span class="hljs-attr">tilingScheme</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">GeographicTilingScheme</span>(),<br>  <span class="hljs-attr">maximumLevel</span>: <span class="hljs-number">18</span><br>&#125;);<br></code></pre></td></tr></table></figure><p>两种投影都能用，关键是<strong>你的 TilingScheme 必须和瓦片服务保持一致</strong>，不是随便抄代码就能跑通的。</p><p>这个坑的根本原因：很多人不知道天地图同时提供两套投影的服务，网上代码混着抄，抄到 <code>_c</code> 的 URL 配了 <code>_w</code> 的投影，或者反过来。</p><h2 id="坑二：tileMatrixSetID——WMTS-方式的隐藏参数"><a href="#坑二：tileMatrixSetID——WMTS-方式的隐藏参数" class="headerlink" title="坑二：tileMatrixSetID——WMTS 方式的隐藏参数"></a>坑二：tileMatrixSetID——WMTS 方式的隐藏参数</h2><p>如果你用 <code>UrlTemplateImageryProvider</code>（上面那种方式），不会碰到这个问题。但如果你用标准 WMTS 协议接入——比如用 <code>WebMapTileServiceImageryProvider</code>——<code>tileMatrixSetID</code> 就是一个必填且极易配错的参数。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// WMTS 方式接入天地图影像</span><br><span class="hljs-keyword">const</span> provider = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">WebMapTileServiceImageryProvider</span>(&#123;<br>  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t0.tianditu.gov.cn/img_w/wmts?tk=&#x27;</span> + token,<br>  <span class="hljs-attr">layer</span>: <span class="hljs-string">&#x27;img&#x27;</span>,<br>  <span class="hljs-attr">style</span>: <span class="hljs-string">&#x27;default&#x27;</span>,<br>  <span class="hljs-attr">format</span>: <span class="hljs-string">&#x27;tiles&#x27;</span>,<br>  <span class="hljs-attr">tileMatrixSetID</span>: <span class="hljs-string">&#x27;w&#x27;</span>,    <span class="hljs-comment">// ← 这个值容易配错</span><br>  <span class="hljs-attr">maximumLevel</span>: <span class="hljs-number">18</span><br>&#125;);<br></code></pre></td></tr></table></figure><h3 id="踩坑现场-1"><a href="#踩坑现场-1" class="headerlink" title="踩坑现场"></a>踩坑现场</h3><p>tileMatrixSetID 的取值规则其实很简单：</p><ul><li><code>_w</code> 系列 → <code>tileMatrixSetID: &#39;w&#39;</code></li><li><code>_c</code> 系列 → <code>tileMatrixSetID: &#39;c&#39;</code></li></ul><p>但很多人写成了 <code>EPSG:3857</code>、<code>GoogleMapsCompatible</code>、<code>WGS84</code> 之类的值，结果瓦片加载不出来，控制台报 404。</p><p>原因是：<strong>天地图的 WMTS 服务使用的 tileMatrixSet 名称就是单字母 <code>w</code> 或 <code>c</code>，不是 OGC 标准中常见的完整命名。</strong> 你用标准 OGC 名称去请求，天地图不认识。</p><p>另外注意，<code>url</code> 后缀和 <code>tileMatrixSetID</code> 必须配套：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// ✅ 正确配对</span><br><span class="hljs-comment">// _w 后缀 + tileMatrixSetID: &#x27;w&#x27;</span><br><span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t0.tianditu.gov.cn/img_w/wmts?tk=&#x27;</span> + token,<br><span class="hljs-attr">tileMatrixSetID</span>: <span class="hljs-string">&#x27;w&#x27;</span><br><br><span class="hljs-comment">// ✅ 正确配对  </span><br><span class="hljs-comment">// _c 后缀 + tileMatrixSetID: &#x27;c&#x27;</span><br><span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t0.tianditu.gov.cn/img_c/wmts?tk=&#x27;</span> + token,<br><span class="hljs-attr">tileMatrixSetID</span>: <span class="hljs-string">&#x27;c&#x27;</span><br><br><span class="hljs-comment">// ❌ 错误：_w 的 URL 配了 c</span><br><span class="hljs-attr">tileMatrixSetID</span>: <span class="hljs-string">&#x27;c&#x27;</span><br><span class="hljs-comment">// → 服务返回 404 或空瓦片</span><br></code></pre></td></tr></table></figure><h3 id="什么场景需要-WMTS-方式"><a href="#什么场景需要-WMTS-方式" class="headerlink" title="什么场景需要 WMTS 方式"></a>什么场景需要 WMTS 方式</h3><p><code>UrlTemplateImageryProvider</code> 简单粗暴，能跑。但 WMTS 方式更标准，在某些场景下更有优势：</p><ul><li>需要对接标准 OGC WMTS 客户端</li><li>需要精确控制瓦片矩阵参数（如分辨率、比例尺）</li><li>需要通过 GetCapabilities 元数据自动发现服务信息</li><li>对 Cesium 内部瓦片请求机制的兼容性要求更高</li></ul><p>大多数项目用 URL Template 方式就够了。只有遇到具体的兼容性问题时，才需要考虑切到 WMTS 方式。</p><h2 id="坑三：注记图层的叠加问题"><a href="#坑三：注记图层的叠加问题" class="headerlink" title="坑三：注记图层的叠加问题"></a>坑三：注记图层的叠加问题</h2><p>天地图的影像底图（卫星照片）本身没有地名标注。要显示地名、道路名称，必须叠加一个<strong>注记图层</strong>。</p><p>技术上是两个独立的 WMTS 服务，叠加显示：</p><figure class="highlight asciidoc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs asciidoc">底层：img<span class="hljs-emphasis">_w  （卫星影像）</span><br><span class="hljs-emphasis">上层：cia_</span>w  （影像注记）<br></code></pre></td></tr></table></figure><h3 id="踩坑现场-2"><a href="#踩坑现场-2" class="headerlink" title="踩坑现场"></a>踩坑现场</h3><p><strong>坑 3.1：图层顺序反了</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs javascript">viewer.<span class="hljs-property">imageryLayers</span>.<span class="hljs-title function_">addImageryProvider</span>(imgProvider);   <span class="hljs-comment">// 影像</span><br>viewer.<span class="hljs-property">imageryLayers</span>.<span class="hljs-title function_">addImageryProvider</span>(ciaProvider);    <span class="hljs-comment">// 注记</span><br></code></pre></td></tr></table></figure><p>Cesium 的图层渲染顺序是从底到顶：先加的在下层，后加的在上层。</p><p>如果你先加影像再加注记，注记显示在影像上面——这是对的。但如果你切换了添加顺序，或者中途用 <code>addImageryProvider</code> 和 <code>addImageryLayer</code> 混用，顺序就可能乱。</p><p>注记被影像盖住，什么都看不见。</p><p><strong>坑 3.2：投影不匹配</strong></p><p>影像用 <code>img_w</code>（Web Mercator），注记用 <code>cia_w</code>（也是 Web Mercator），这没问题。</p><p>但有些人影像用了 <code>img_w</code>，注记用了 <code>cia_c</code>，投影不一致，导致注记位置偏移。</p><p>记住一条原则：<strong>影像和注记的投影必须一致。</strong> <code>img_w</code> + <code>cia_w</code>，或者 <code>img_c</code> + <code>cia_c</code>，不要混搭。</p><p><strong>坑 3.3：跨域与白图</strong></p><p>有些版本的 Cesium 加载注记图层时，特定缩放级别下会出现白图或空瓦片。原因比较复杂，可能是天地图服务端的切片边界与 Cesium 请求的 tile 坐标存在微小偏差。解决方案是确认 <code>minimumLevel</code> 和 <code>maximumLevel</code> 设置正确，天地图注记一般从 1 到 18 级都可用。</p><h3 id="正确的叠层方式"><a href="#正确的叠层方式" class="headerlink" title="正确的叠层方式"></a>正确的叠层方式</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">// 1. 影像底图</span><br><span class="hljs-keyword">const</span> imgLayer = viewer.<span class="hljs-property">imageryLayers</span>.<span class="hljs-title function_">addImageryProvider</span>(<br>  <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">UrlTemplateImageryProvider</span>(&#123;<br>    <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t&#123;s&#125;.tianditu.gov.cn/DataServer?T=img_w&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27;</span> + token,<br>    <span class="hljs-attr">subdomains</span>: [<span class="hljs-string">&#x27;0&#x27;</span>, <span class="hljs-string">&#x27;1&#x27;</span>, <span class="hljs-string">&#x27;2&#x27;</span>, <span class="hljs-string">&#x27;3&#x27;</span>, <span class="hljs-string">&#x27;4&#x27;</span>, <span class="hljs-string">&#x27;5&#x27;</span>, <span class="hljs-string">&#x27;6&#x27;</span>, <span class="hljs-string">&#x27;7&#x27;</span>],<br>    <span class="hljs-attr">tilingScheme</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">WebMercatorTilingScheme</span>(),<br>    <span class="hljs-attr">maximumLevel</span>: <span class="hljs-number">18</span><br>  &#125;)<br>);<br><br><span class="hljs-comment">// 2. 注记图层（在影像之上）</span><br><span class="hljs-keyword">const</span> annoLayer = viewer.<span class="hljs-property">imageryLayers</span>.<span class="hljs-title function_">addImageryProvider</span>(<br>  <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">UrlTemplateImageryProvider</span>(&#123;<br>    <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://t&#123;s&#125;.tianditu.gov.cn/DataServer?T=cia_w&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27;</span> + token,<br>    <span class="hljs-attr">subdomains</span>: [<span class="hljs-string">&#x27;0&#x27;</span>, <span class="hljs-string">&#x27;1&#x27;</span>, <span class="hljs-string">&#x27;2&#x27;</span>, <span class="hljs-string">&#x27;3&#x27;</span>, <span class="hljs-string">&#x27;4&#x27;</span>, <span class="hljs-string">&#x27;5&#x27;</span>, <span class="hljs-string">&#x27;6&#x27;</span>, <span class="hljs-string">&#x27;7&#x27;</span>],<br>    <span class="hljs-attr">tilingScheme</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Cesium</span>.<span class="hljs-title class_">WebMercatorTilingScheme</span>(),<br>    <span class="hljs-attr">maximumLevel</span>: <span class="hljs-number">18</span><br>  &#125;)<br>);<br><br><span class="hljs-comment">// 3. （可选）调整注记透明度</span><br>annoLayer.<span class="hljs-property">alpha</span> = <span class="hljs-number">0.8</span>; <span class="hljs-comment">// 让注记不那么生硬地贴在影像上</span><br></code></pre></td></tr></table></figure><h2 id="坑四：Token-配置——从申请到调用的所有细节"><a href="#坑四：Token-配置——从申请到调用的所有细节" class="headerlink" title="坑四：Token 配置——从申请到调用的所有细节"></a>坑四：Token 配置——从申请到调用的所有细节</h2><p>这是最基础但也最容易出问题的一环。</p><h3 id="4-1-应用类型必须选”浏览器端”"><a href="#4-1-应用类型必须选”浏览器端”" class="headerlink" title="4.1 应用类型必须选”浏览器端”"></a>4.1 应用类型必须选”浏览器端”</h3><p>去 <a href="http://www.tianditu.gov.cn/">天地图官网</a> 注册 → 控制台 → 创建应用。最关键的一步：</p><p><strong>应用类型必须选”浏览器端”。</strong></p><p>天地图现在严格区分应用类型：</p><table><thead><tr><th>应用类型</th><th align="center">Referer 校验</th><th>适用场景</th></tr></thead><tbody><tr><td>浏览器端</td><td align="center">✅ 校验 Referer</td><td>前端 JS 直接调用（Cesium、Leaflet、OpenLayers）</td></tr><tr><td>服务端</td><td align="center">❌ 不校验</td><td>后端服务代理请求</td></tr><tr><td>Android&#x2F;iOS</td><td align="center">平台绑定</td><td>移动端原生应用</td></tr></tbody></table><p>如果你选了”服务端”的 token 放到 Cesium 前端代码里，请求会返回 403——因为天地图服务端会校验 <code>Referer</code> 头，服务端 token 和浏览器端的 Referer 模式不匹配。</p><h3 id="4-2-IP-白名单和域名白名单"><a href="#4-2-IP-白名单和域名白名单" class="headerlink" title="4.2 IP 白名单和域名白名单"></a>4.2 IP 白名单和域名白名单</h3><p>创建应用时可以配置”IP 白名单”和”Referer 白名单”。</p><ul><li><strong>IP 白名单：</strong> 只有列表里的 IP 能调用。对前端应用不适用（用户端 IP 不固定）</li><li><strong>Referer 白名单：</strong> 只有指定域名来源的请求能通过。建议开发阶段填 <code>*</code>，上线前换成你的具体域名</li></ul><p>如果配置了 Referer 白名单但忘填 <code>localhost</code>，本地开发直接 403。这是本地调试最常见的翻车点。</p><h3 id="4-3-HTTP-和-HTTPS"><a href="#4-3-HTTP-和-HTTPS" class="headerlink" title="4.3 HTTP 和 HTTPS"></a>4.3 HTTP 和 HTTPS</h3><p>天地图现在支持 HTTPS 访问，URL 统一用 <code>https://t&#123;s&#125;.tianditu.gov.cn/...</code>。</p><p>如果你的站点是 HTTPS，但加载天地图用了 HTTP 地址，浏览器会报混合内容警告（Mixed Content），瓦片可能被浏览器拦截。直接用 HTTPS 地址就能解决。</p><h3 id="4-4-Token-泄露风险"><a href="#4-4-Token-泄露风险" class="headerlink" title="4.4 Token 泄露风险"></a>4.4 Token 泄露风险</h3><p>Token 直接写在前端代码里，等于公开了。天地图的 token 目前只做调用量限制和来源校验，不涉及敏感权限，风险相对可控。但如果你有调用量限制的担忧，建议：</p><ul><li>开发环境用自己的 token</li><li>生产环境通过后端代理转发，不要在前端暴露 token</li><li>定期更换 token</li></ul><h3 id="4-5-完整的-token-配置检查清单"><a href="#4-5-完整的-token-配置检查清单" class="headerlink" title="4.5 完整的 token 配置检查清单"></a>4.5 完整的 token 配置检查清单</h3><p>如果天地图加载不出来，按这个顺序排查：</p><figure class="highlight lasso"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs lasso">□ 应用类型是否选了<span class="hljs-string">&quot;浏览器端&quot;</span>？<br>□ <span class="hljs-keyword">Referer</span> 白名单是否包含当前域名？<br>□ 域名写的是 https 还是 http？<br>□ 当前浏览器是否限制了 <span class="hljs-keyword">Referer</span> 头（少数隐私模式会）？<br>□ Token 字符串是否完整复制（注意不要漏掉末尾字符）？<br>□ MaximumLevel 是否设置正确？<br></code></pre></td></tr></table></figure><h2 id="完整代码：能直接跑的方案"><a href="#完整代码：能直接跑的方案" class="headerlink" title="完整代码：能直接跑的方案"></a>完整代码：能直接跑的方案</h2><p>最后贴一个完整的、经过验证的代码片段，Vue 3 + Cesium：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><code class="hljs vue">&lt;template&gt;<br>  &lt;div id=&quot;cesiumContainer&quot; ref=&quot;container&quot;&gt;&lt;/div&gt;<br>&lt;/template&gt;<br><br>&lt;script setup&gt;<br>import * as Cesium from &#x27;cesium&#x27;;<br>import &#123; onMounted, ref &#125; from &#x27;vue&#x27;;<br><br>const container = ref(null);<br>const TDT_TOKEN = &#x27;你的天地图浏览器端Key&#x27;;<br><br>onMounted(() =&gt; &#123;<br>  const viewer = new Cesium.Viewer(container.value, &#123;<br>    baseLayerPicker: false,<br>    geocoder: false,<br>    homeButton: true,<br>    sceneModePicker: true,<br>    navigationHelpButton: false,<br>    animation: false,<br>    timeline: false,<br>    fullscreenButton: false,<br>    infoBox: false<br>  &#125;);<br><br>  // 移除 Cesium 默认底图<br>  viewer.imageryLayers.removeAll();<br><br>  // 天地图影像底图<br>  const baseLayer = viewer.imageryLayers.addImageryProvider(<br>    new Cesium.UrlTemplateImageryProvider(&#123;<br>      url: &#x27;https://t&#123;s&#125;.tianditu.gov.cn/DataServer?T=img_w&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27; + TDT_TOKEN,<br>      subdomains: [&#x27;0&#x27;, &#x27;1&#x27;, &#x27;2&#x27;, &#x27;3&#x27;, &#x27;4&#x27;, &#x27;5&#x27;, &#x27;6&#x27;, &#x27;7&#x27;],<br>      tilingScheme: new Cesium.WebMercatorTilingScheme(),<br>      maximumLevel: 18,<br>      minimumLevel: 1<br>    &#125;)<br>  );<br><br>  // 天地图注记图层<br>  const annoLayer = viewer.imageryLayers.addImageryProvider(<br>    new Cesium.UrlTemplateImageryProvider(&#123;<br>      url: &#x27;https://t&#123;s&#125;.tianditu.gov.cn/DataServer?T=cia_w&amp;x=&#123;x&#125;&amp;y=&#123;y&#125;&amp;l=&#123;z&#125;&amp;tk=&#x27; + TDT_TOKEN,<br>      subdomains: [&#x27;0&#x27;, &#x27;1&#x27;, &#x27;2&#x27;, &#x27;3&#x27;, &#x27;4&#x27;, &#x27;5&#x27;, &#x27;6&#x27;, &#x27;7&#x27;],<br>      tilingScheme: new Cesium.WebMercatorTilingScheme(),<br>      maximumLevel: 18,<br>      minimumLevel: 1<br>    &#125;)<br>  );<br>  annoLayer.alpha = 0.85;<br><br>  // 定位到广州<br>  viewer.camera.setView(&#123;<br>    destination: Cesium.Cartesian3.fromDegrees(113.264, 23.129, 50000)<br>  &#125;);<br>&#125;);<br>&lt;/script&gt;<br><br>&lt;style&gt;<br>* &#123; margin: 0; padding: 0; &#125;<br>#cesiumContainer &#123; width: 100vw; height: 100vh; &#125;<br>&lt;/style&gt;<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>天地图作为国内 GIS 项目的标配底图源，接入本身不复杂。但对 Cesium 不熟悉的人，容易在这四个点上卡住：</p><ol><li><strong><code>img_w</code> vs <code>img_c</code></strong> ——投影决定一切。Cesium 默认 Web Mercator，用 <code>img_w</code> 少填一个坑</li><li><strong><code>tileMatrixSetID</code></strong> ——WMTS 方式接入时，值是单字母 <code>w</code> 或 <code>c</code>，不是标准命名</li><li><strong>注记图层</strong> ——与底图投影一致、图层顺序正确、透明度调好，效果就不差</li><li><strong>Token 配置</strong> ——浏览器端、Referer 白名单、HTTPS，十分钟能搞定但查错可能花一小时</li></ol><p>这些坑的共性是：<strong>代码不多，但错了一个参数就全盘翻车。</strong> 排查的时候，从参数校验入手往往比从代码逻辑入手更快。</p><p>希望这份记录能让你在集成天地图时少走点弯路。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/cesium-tianditu-integration-pitfalls/</id>
    <link href="https://blog.280303.xyz/posts/cesium-tianditu-integration-pitfalls/"/>
    <published>2026-06-01T07:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="一个看似简单的需求"><a href="#一个看似简单的需求" class="headerlink" title="一个看似简单的需求"></a>一个看似简单的需求</h2><p>事情开始得很普通。</p>
<p>项目需要在 Cesium 三维地球上显示国内城市的卫星影像底图。默认的 Bing Maps 影像在国内加载慢，而且行政边界和地名标注不符合国内项目的要求。</p>
<p>方案很明确——换成天地图。国家地理信息公共服务平台，数据权威，国内访问快，免费。</p>
<p>然后我花了一天时间，在四个地方卡住。</p>
<p>这篇文章不是什么”三分钟快速接入”教程。是把我踩过的坑、排查的思路、最终能稳定运行的技术方案，一次说清楚。如果你也在做 Cesium + 天地图的集成，应该能帮你省掉大半天的时间。</p>]]>
    </summary>
    <title>Cesium 接入天地图影像的几个坑：img_w、tileMatrixSetID、注记图层和 token 配置</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="AI" scheme="https://blog.280303.xyz/categories/AI/"/>
    <category term="AI中转站" scheme="https://blog.280303.xyz/tags/AI%E4%B8%AD%E8%BD%AC%E7%AB%99/"/>
    <category term="API代理" scheme="https://blog.280303.xyz/tags/API%E4%BB%A3%E7%90%86/"/>
    <category term="法律风险" scheme="https://blog.280303.xyz/tags/%E6%B3%95%E5%BE%8B%E9%A3%8E%E9%99%A9/"/>
    <category term="灰产" scheme="https://blog.280303.xyz/tags/%E7%81%B0%E4%BA%A7/"/>
    <content>
      <![CDATA[<h2 id="一个炸裂的消息"><a href="#一个炸裂的消息" class="headerlink" title="一个炸裂的消息"></a>一个炸裂的消息</h2><p>2026 年 5 月，AI 圈被一则声明炸开了锅。</p><p>一位上海的 AI 中转站站长公开发帖，说自己因为运营中转站被上海警方刑事拘留了 37 天，目前取保候审出来。他在声明里直接说了一句话——“将来肯定会被判刑。”</p><p>他已经退赔退赃，接下来还要缴纳罚金。用户充值的钱，他无力赔偿。</p><p>这个在很多人眼里”躺赚”的生意，第一次有人付出了坐牢的代价。</p><p>这件事在从业者圈子里震动很大。去年开始，AI 中转站像雨后春笋一样冒出来，到处都有人喊着”API 中转是 2026 年最赚钱的项目”。但到底这个生意是怎么做的？风险在哪里？被抓的这位到底踩了什么雷？</p><p>这篇文章把技术原理、商业模式和法律风险一次性说清楚。</p><span id="more"></span><h2 id="AI-中转站到底是什么"><a href="#AI-中转站到底是什么" class="headerlink" title="AI 中转站到底是什么"></a>AI 中转站到底是什么</h2><p>简单说，AI 中转站就是一个** API 聚合代理平台**。</p><p>架构长这样：</p><figure class="highlight nix"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs nix">用户 → 中转站 API → 中转站服务器 → OpenAI <span class="hljs-symbol">/</span> Claude <span class="hljs-symbol">/</span> Gemini 等境外模型<br>                         ↓<br>                返回结果给用户<br></code></pre></td></tr></table></figure><p>用户不需要科学上网，不需要注册 OpenAI 账号，不需要外币信用卡。所有后台对接由中转站搞定，用户只管调用。</p><p>技术实现上，中转站做的事情不复杂：</p><ul><li><strong>请求转发。</strong> 接收用户的 HTTP 请求，转换成对应模型的 API 调用格式，发出去</li><li><strong>密钥管理。</strong> 背后维护一堆上游 API Key，做负载均衡和限流</li><li><strong>计费扣量。</strong> 记录用户消耗了多少 Token，按自己的定价扣费</li><li><strong>结果缓存。</strong> 高频相同请求，直接返回缓存结果，省掉上游调用成本</li></ul><p>整条链路的技术栈很标准：Nginx 做反向代理 + Go 或 Node.js 写业务层 + Redis 做缓存 + MySQL&#x2F;PostgreSQL 记账。启动成本低得惊人——一台低配云服务器加一个域名就能开业。</p><p>这本质上就是一门”API 二房东”生意。</p><h2 id="价格低到离谱，中转站怎么赚钱"><a href="#价格低到离谱，中转站怎么赚钱" class="headerlink" title="价格低到离谱，中转站怎么赚钱"></a>价格低到离谱，中转站怎么赚钱</h2><p>打开淘宝、闲鱼、小红书，到处是”100 元 &#x3D; 全模型无限调用”的帖子，价格低到离谱。</p><p>但中间商不赚差价是不可能的。他们的盈利手段，远比表面看起来丰富。</p><h3 id="1-免费额度套利（核心利润来源）"><a href="#1-免费额度套利（核心利润来源）" class="headerlink" title="1. 免费额度套利（核心利润来源）"></a>1. 免费额度套利（核心利润来源）</h3><p>ChatGPT、Claude 这些平台注册新账号都有免费额度。中转站背后的号商，用脚本批量注册大量账号，薅走全部免费额度。</p><p>然后用技术手段把这些网页端接口逆向工程成标准 API 格式，打包卖给用户。</p><p>成本 ≈ 0。</p><p>这是最多中转站赖以生存的根本模式。</p><h3 id="2-退款套利"><a href="#2-退款套利" class="headerlink" title="2. 退款套利"></a>2. 退款套利</h3><p>批量注册账号 → 充值 → 调用 API。账号被封了？直接申请退款。大多数情况下，预充的钱能要回来。</p><p>相当于：用你的钱调 API，账号被封了再找官方退款把成本吃回来。两头赚。</p><h3 id="3-虚报-Token-消耗"><a href="#3-虚报-Token-消耗" class="headerlink" title="3. 虚报 Token 消耗"></a>3. 虚报 Token 消耗</h3><p>官方 API 按 Token 计费，1 个汉字约 1.5 到 2 个 Token。但中转站的计费系统是站长自己写的。</p><p>后台把倍率调高 1 倍，你的 1 个汉字就被扣成 3 到 4 个 Token 的钱。用户没办法核实——返回的数据里只有回答内容，没有明细账单。</p><h3 id="4-模型掉包"><a href="#4-模型掉包" class="headerlink" title="4. 模型掉包"></a>4. 模型掉包</h3><p>你买的是 Claude Opus 4.7，十块钱。但中转站后端实际调的可能是一个开源小模型，成本两毛钱。</p><p>这就是很多用户说的”用中转站感觉被降智了”的真实原因——不是错觉，是你花的钱根本就没到你以为的模型那里。</p><h3 id="5-数据转卖"><a href="#5-数据转卖" class="headerlink" title="5. 数据转卖"></a>5. 数据转卖</h3><p>这是最隐蔽也最值钱的一环。</p><p>用户往模型里发的提示词、代码片段、商业文档，全部经过中转站服务器。这些数据——尤其是编程类的高质量对话——是模型厂商拿来训练下一代模型的金矿。</p><p>部分中转站把用户的对话记录打包卖给第三方，一个数据包几千到几万不等。而用户对此一无所知。</p><h2 id="三条红线，踩了就进去"><a href="#三条红线，踩了就进去" class="headerlink" title="三条红线，踩了就进去"></a>三条红线，踩了就进去</h2><p>回到被抓的那个站长。他声明里写的是”非法逆向爬取、倒卖低价 AI 接口资源”，但如果仔细深究，真实案由大概率不止一条。</p><h3 id="红线一：无证经营电信增值业务"><a href="#红线一：无证经营电信增值业务" class="headerlink" title="红线一：无证经营电信增值业务"></a>红线一：无证经营电信增值业务</h3><p>AI 中转站提供信息中转和数据处理服务，在法律上属于增值电信业务。</p><p>依据《中华人民共和国电信条例》，经营此类业务必须取得 ICP 许可证。跨地区经营还需要跨地区增值电信业务经营许可证。</p><p>目前市面上几乎没有任何一家中转站持有这些资质。</p><p>另外，《生成式人工智能服务管理暂行办法》明确规定，所有向境内公众提供服务的 AI 模型都必须完成备案。中转站把境外未备案的模型直接卖给国内用户，这一条也踩死了。</p><p>这就是<strong>非法经营罪</strong>的入罪逻辑。</p><h3 id="红线二：数据安全义务完全缺失"><a href="#红线二：数据安全义务完全缺失" class="headerlink" title="红线二：数据安全义务完全缺失"></a>红线二：数据安全义务完全缺失</h3><p>中转站每天处理海量用户提示词、代码、商业文件。作为实际的数据经手方，依法必须承担数据安全管理责任。</p><p>但现实情况是：绝大多数中转站没有任何数据安全制度。数据存在哪个服务器？谁有权限访问？安全防护怎么做的？全是空白。</p><p>一旦发生数据泄露（不管是外部攻击还是内部人干的），中转站作为网络服务提供者，直接面临<strong>拒不履行信息网络安全管理义务罪</strong>的追诉。</p><p>更致命的是<strong>数据出境</strong>问题。用户请求经过中转站发到境外模型，这已经构成了数据跨境传输。按照《数据安全法》《个人信息保护法》以及《数据出境安全评估办法》，必须完成数据出境安全评估。没有任何中转站做了这件事。</p><h3 id="红线三：违规收集和出售用户数据"><a href="#红线三：违规收集和出售用户数据" class="headerlink" title="红线三：违规收集和出售用户数据"></a>红线三：违规收集和出售用户数据</h3><p>用户的对话内容，有大量个人信息和商业秘密。中转站收集这些数据，没有告知，更没有取得用户同意。</p><p>未经同意把数据卖给第三方，情节严重的构成<strong>侵犯公民个人信息罪</strong>。</p><p>这个罪名的入罪门槛不高：</p><ul><li>通信内容（聊天记录）50 条以上就够追诉标准</li><li>一般个人信息 500 条以上也够</li></ul><p>以中转站的日均数据量，达到这个门槛太容易了。</p><hr><p><strong>三个红线叠加在一起：非法经营罪 + 拒不履行信息网络安全管理义务罪 + 侵犯公民个人信息罪。</strong></p><p>数罪并罚，所以被抓的那位站长才会说”将来肯定会被判刑”——不是危言耸听，是律师看完案由后给的结论。</p><h2 id="用户也要小心"><a href="#用户也要小心" class="headerlink" title="用户也要小心"></a>用户也要小心</h2><p>中转站的用户不是没有风险，只是风险不容易被人看到。</p><p><strong>数据泄露。</strong> 你的代码、商业计划、API Key、数据库连接串……全部经过第三方服务器。中转站没有任何数据安全保障意识，你的敏感信息就在裸奔。</p><p><strong>服务跑路。</strong> 中转站本身就在灰色地带，站长随时可能被端。站长端了，你的余额、你的数据，全没了。这次被抓的站长已经在声明里说了——“无力赔偿用户的充值款”。</p><p><strong>法律连带。</strong> 如果你用中转站的 API 接入了自己的业务系统，一旦中转站被认定违法，你的业务信息也在警方的取证范围里。企业客户用了不合规的中转站，数据合规的连带责任同样存在。</p><h2 id="为什么这个生意一定会被打击"><a href="#为什么这个生意一定会被打击" class="headerlink" title="为什么这个生意一定会被打击"></a>为什么这个生意一定会被打击</h2><p>从趋势上看，这不是个例，而是信号。</p><p>字节跳动、阿里云、腾讯云都在加大 AI API 的商业化力度。国内大模型厂商的接口质量和价格越来越有竞争力。中转站的存在，在几个层面都是巨大的破坏：</p><ul><li><strong>破坏市场定价。</strong> 当免费额度套利和模型掉包成为主流模式，正规渠道的定价体系会被架空虚置</li><li><strong>消耗厂商成本。</strong> 厂商不得不投入大量资源去做风控对抗（识别批量注册、异常访问等），这些成本最终转嫁给正常用户</li><li><strong>扭曲行业认知。</strong> 用户会潜意识认为 AI 能力就应该是白菜价，这对需要正向现金流的模型厂商是结构性伤害</li></ul><p>厂商被逼到一定程度，拿起法律武器维权是必然的。</p><p>那位被抓的站长，很可能就是第一张倒下的多米诺骨牌。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>AI 中转站这个生意，技术门槛确实低，启动成本确实小，市场需求确实大——但这些加在一起，也不等于”可以做”。</p><p>不要看到有人赚了钱就以为是机会。那个被抓的站长，当初也是这么想的。</p><p>如果一定要在这个领域做点事情，合规路线是唯一的出路：</p><ul><li>持有 ICP 许可证和跨地区增值电信业务经营许可证</li><li>通过正规渠道采购 API 额度，有上游合同和发票</li><li>建立数据安全管理制度，处理数据出境合规</li><li>不收集、不出售用户数据</li><li>不使用批量注册、逆向破解等非法手段获取上游资源</li></ul><p>合规的成本确实高，但至少晚上睡得着觉。</p><hr><p><em>本文根据公开报道和法律分析整理，不构成法律意见。涉及具体业务的合规问题，请咨询专业律师。</em></p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/ai-relay-station-risks/</id>
    <link href="https://blog.280303.xyz/posts/ai-relay-station-risks/"/>
    <published>2026-06-01T06:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="一个炸裂的消息"><a href="#一个炸裂的消息" class="headerlink" title="一个炸裂的消息"></a>一个炸裂的消息</h2><p>2026 年 5 月，AI 圈被一则声明炸开了锅。</p>
<p>一位上海的 AI 中转站站长公开发帖，说自己因为运营中转站被上海警方刑事拘留了 37 天，目前取保候审出来。他在声明里直接说了一句话——“将来肯定会被判刑。”</p>
<p>他已经退赔退赃，接下来还要缴纳罚金。用户充值的钱，他无力赔偿。</p>
<p>这个在很多人眼里”躺赚”的生意，第一次有人付出了坐牢的代价。</p>
<p>这件事在从业者圈子里震动很大。去年开始，AI 中转站像雨后春笋一样冒出来，到处都有人喊着”API 中转是 2026 年最赚钱的项目”。但到底这个生意是怎么做的？风险在哪里？被抓的这位到底踩了什么雷？</p>
<p>这篇文章把技术原理、商业模式和法律风险一次性说清楚。</p>]]>
    </summary>
    <title>AI 中转站是怎么运作的？站长被刑拘 37 天，哪些红线不能碰</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="AI" scheme="https://blog.280303.xyz/categories/AI/"/>
    <category term="OpenClaw" scheme="https://blog.280303.xyz/tags/OpenClaw/"/>
    <category term="AI Agent" scheme="https://blog.280303.xyz/tags/AI-Agent/"/>
    <category term="自部署" scheme="https://blog.280303.xyz/tags/%E8%87%AA%E9%83%A8%E7%BD%B2/"/>
    <category term="教程" scheme="https://blog.280303.xyz/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="为什么是-OpenClaw"><a href="#为什么是-OpenClaw" class="headerlink" title="为什么是 OpenClaw"></a>为什么是 OpenClaw</h2><p>市面上的 AI 助手产品很多，但大部分都是 SaaS——数据在别人手里，模型按调用收费，功能边界由厂商决定。</p><p>OpenClaw 走的是另一条路：自部署、多渠道、Agent 原生。你自己买服务器，自己配模型 API，自己决定用哪个模型、开哪些工具、连什么聊天软件。</p><p>它不是又一个 ChatGPT 网页版。它是一个<strong>网关</strong>——把你用的聊天软件（Telegram、Discord、Signal、飞书……）和一个 AI Agent 连起来，让你在口袋里随时有一个能写代码、能查资料、能操作服务器的 AI 助手。</p><p>这篇文章从零开始，把安装、配置、渠道对接、写 Agent 技能、实际使用场景和踩过的坑全串起来。</p><span id="more"></span><h2 id="第一步：安装"><a href="#第一步：安装" class="headerlink" title="第一步：安装"></a>第一步：安装</h2><h3 id="环境要求"><a href="#环境要求" class="headerlink" title="环境要求"></a>环境要求</h3><p>OpenClaw 需要 Node.js 24（推荐）或 Node.js 22.19+。装好 Node 后：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -fsSL https://openclaw.ai/install.sh | bash<br></code></pre></td></tr></table></figure><p>Windows 用户用 PowerShell：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs powershell"><span class="hljs-built_in">iwr</span> <span class="hljs-literal">-useb</span> https://openclaw.ai/install.ps1 | <span class="hljs-built_in">iex</span><br></code></pre></td></tr></table></figure><p>安装完后运行 <code>openclaw --version</code> 确认成功。如果提示找不到命令，检查 Node 的全局 bin 目录是否在 PATH 里。</p><h3 id="运行-onboarding"><a href="#运行-onboarding" class="headerlink" title="运行 onboarding"></a>运行 onboarding</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">openclaw onboard --install-daemon<br></code></pre></td></tr></table></figure><p>这一步是交互式的，它会问：</p><ul><li>用哪个模型提供商（Anthropic &#x2F; OpenAI &#x2F; 自定义）</li><li>输入 API Key</li><li>是否开机自启</li></ul><p>跑完后 Gateway 就已经在后台运行了。验证一下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">openclaw gateway status<br></code></pre></td></tr></table></figure><p>看到 Gateway 在监听 18789 端口，就说明好了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">openclaw dashboard<br></code></pre></td></tr></table></figure><p>浏览器打开控制面板，你就可以直接在 Web 上发消息了。</p><h3 id="踩坑：安装脚本挂代理"><a href="#踩坑：安装脚本挂代理" class="headerlink" title="踩坑：安装脚本挂代理"></a>踩坑：安装脚本挂代理</h3><p>如果你在国内服务器上安装，curl 脚本可能下载失败。解决办法是在安装前先配好环境变量：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">export</span> http_proxy=http://your-proxy:port<br><span class="hljs-built_in">export</span> https_proxy=http://your-proxy:port<br>curl -fsSL https://openclaw.ai/install.sh | bash<br></code></pre></td></tr></table></figure><p>或者用 npm 手动安装（适合网络受限环境）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install -g openclaw<br></code></pre></td></tr></table></figure><h3 id="踩坑：systemd-daemon-注册失败"><a href="#踩坑：systemd-daemon-注册失败" class="headerlink" title="踩坑：systemd daemon 注册失败"></a>踩坑：systemd daemon 注册失败</h3><p><code>--install-daemon</code> 会在 Linux 上注册 systemd user service。如果你的系统没有 systemd（比如 Docker 容器），会报错。这时候用 <code>openclaw start</code> 前台运行就行，或者自己在容器里配进程管理。</p><hr><h2 id="第二步：理解工作区"><a href="#第二步：理解工作区" class="headerlink" title="第二步：理解工作区"></a>第二步：理解工作区</h2><p>OpenClaw 的核心概念是<strong>工作区（workspace）</strong>，默认在 <code>~/.openclaw/workspace</code>。</p><p>这是 Agent 的家。它看到的文件、读的配置、存的记忆——全在这里。</p><figure class="highlight nix"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs nix">~<span class="hljs-operator">/</span>.openclaw<span class="hljs-operator">/</span>workspace<span class="hljs-symbol">/</span><br>├── AGENTS.md       <span class="hljs-comment"># Agent 的行为指南</span><br>├── SOUL.md         <span class="hljs-comment"># 人格设定（语气、风格、边界）</span><br>├── USER.md         <span class="hljs-comment"># 关于你本人的信息</span><br>├── TOOLS.md        <span class="hljs-comment"># 工具相关的本地备注</span><br>├── MEMORY.md       <span class="hljs-comment"># 长期记忆（重要事件、决策、偏好）</span><br>├── HEARTBEAT.md    <span class="hljs-comment"># 心跳任务清单</span><br>├── BOOTSTRAP.md    <span class="hljs-comment"># 首次启动脚本（用完后删除）</span><br>└── memory<span class="hljs-symbol">/</span>         <span class="hljs-comment"># 每日日志文件</span><br>    └── <span class="hljs-number">202</span>6-<span class="hljs-number">0</span>6-<span class="hljs-number">01</span>.md<br></code></pre></td></tr></table></figure><h3 id="AGENTS-md-和-SOUL-md-是灵魂"><a href="#AGENTS-md-和-SOUL-md-是灵魂" class="headerlink" title="AGENTS.md 和 SOUL.md 是灵魂"></a>AGENTS.md 和 SOUL.md 是灵魂</h3><p>这两个文件决定了 Agent 的行为方式。</p><p><strong>AGENTS.md</strong> 定义 Agent 的工作规则：怎么使用工具、什么情况下需要问你、安全红线是什么。</p><p><strong>SOUL.md</strong> 定义 Agent 的性格：是正式还是随意，是话痨还是简洁，有没有幽默感。</p><p>我第一次用的时候没太在意这两个文件，结果 Agent 回消息特别”AI 味”——客套、冗长、每句话都带礼貌用语。后来把 SOUL.md 改成”别废话，直接回答”，世界清静了。</p><h3 id="记忆系统"><a href="#记忆系统" class="headerlink" title="记忆系统"></a>记忆系统</h3><p><code>MEMORY.md</code> 是长期记忆，Agent 每次启动都会读。<code>memory/</code> 下面按日期存日志文件。</p><p>实际用下来，记忆系统最大的价值是：</p><ul><li>你不用反复告诉它你的偏好</li><li>跨会话上下文能保持</li><li>决策记录可回溯</li></ul><p>但要注意——记忆不是无限的。写多了会被截断。建议只记真正重要的东西，不要事无巨细全往里塞。</p><h3 id="踩坑：工作区文件权限"><a href="#踩坑：工作区文件权限" class="headerlink" title="踩坑：工作区文件权限"></a>踩坑：工作区文件权限</h3><p>OpenClaw 用文件系统读写的，如果工作区权限不对，Agent 写文件会失败。特别是把工作区放在需要 sudo 的路径下。建议：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">chmod</span> -R 755 ~/.openclaw/workspace<br></code></pre></td></tr></table></figure><p>确保运行 Gateway 的用户有完整权限。</p><hr><h2 id="第三步：配置渠道"><a href="#第三步：配置渠道" class="headerlink" title="第三步：配置渠道"></a>第三步：配置渠道</h2><p>这是 OpenClaw 最核心的能力——一个 Agent，对接多个聊天软件。</p><h3 id="Telegram（最简单）"><a href="#Telegram（最简单）" class="headerlink" title="Telegram（最简单）"></a>Telegram（最简单）</h3><p>在 @BotFather 创建一个机器人，拿到 token，然后配到 <code>~/.openclaw/openclaw.json</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;channels&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;telegram&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;enabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;botToken&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;123456:ABC-DEF1234...&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;dmPolicy&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;pairing&quot;</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>重启 Gateway，去 Telegram 给你的 bot 发消息，配对后就能用了。全程不到五分钟。</p><h3 id="Discord"><a href="#Discord" class="headerlink" title="Discord"></a>Discord</h3><p>创建 Discord 应用，拿到 bot token，开通 Message Content Intent：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;channels&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;discord&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;enabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;botToken&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;你的token&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;dmPolicy&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;pairing&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;groupPolicy&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;allowlist&quot;</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><h3 id="飞书（Feishu）"><a href="#飞书（Feishu）" class="headerlink" title="飞书（Feishu）"></a>飞书（Feishu）</h3><p>飞书接入稍微复杂一点，需要：</p><ul><li>在飞书开放平台创建应用</li><li>配置事件订阅（接收消息）</li><li>获取 app_id 和 app_secret</li></ul><p>配置好后，飞书上的消息就直接路由到 OpenClaw 的 Agent 了。</p><h3 id="踩坑：配对码过期"><a href="#踩坑：配对码过期" class="headerlink" title="踩坑：配对码过期"></a>踩坑：配对码过期</h3><p>默认 DM 策略是 <code>pairing</code>，未知发送者会收到一个配对码，一小时过期。如果你刚配好渠道、发消息没反应，检查一下配对码是不是过期了。可以把 <code>dmPolicy</code> 临时改成 <code>open</code> 调试，确认通了再切回 <code>pairing</code>。</p><h3 id="踩坑：多渠道的会话隔离"><a href="#踩坑：多渠道的会话隔离" class="headerlink" title="踩坑：多渠道的会话隔离"></a>踩坑：多渠道的会话隔离</h3><p>OpenClaw 的会话是按渠道+用户隔离的。你在 Telegram 上说的话，Discord 上看不到。这是设计如此——但如果你想让不同渠道共享上下文，需要用 <code>/session</code> 命令手动绑定到同一个会话。</p><hr><h2 id="第四步：写技能（Skills）"><a href="#第四步：写技能（Skills）" class="headerlink" title="第四步：写技能（Skills）"></a>第四步：写技能（Skills）</h2><p>Skills 是 OpenClaw 最强大的扩展机制。一个 Skill &#x3D; 一个 <code>SKILL.md</code> + 脚本&#x2F;工具。</p><h3 id="Skill-的结构"><a href="#Skill-的结构" class="headerlink" title="Skill 的结构"></a>Skill 的结构</h3><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs applescript">~/.openclaw/workspace/skills/<span class="hljs-keyword">my</span>-skill/<br>├── SKILL.md     <span class="hljs-comment"># 技能说明（何时触发、怎么用）</span><br>└── <span class="hljs-keyword">script</span>.sh    <span class="hljs-comment"># 可选：配套脚本</span><br></code></pre></td></tr></table></figure><p><code>SKILL.md</code> 的头部定义触发条件：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs markdown">---<br>name: my-website-deployer<br>description: 部署网站到服务器<br><span class="hljs-section">trigger: 用户说要部署</span><br><span class="hljs-section">---</span><br><br><span class="hljs-section"># 我的部署技能</span><br><br>当用户说&quot;部署&quot;时，执行以下步骤：<br><span class="hljs-bullet">1.</span> 从 GitHub 拉取最新代码<br><span class="hljs-bullet">2.</span> 运行构建<br><span class="hljs-bullet">3.</span> 同步到服务器<br></code></pre></td></tr></table></figure><h3 id="内建-Skills"><a href="#内建-Skills" class="headerlink" title="内建 Skills"></a>内建 Skills</h3><p>OpenClaw 自带很多实用的 skill：</p><ul><li><strong>weather</strong> — 查天气</li><li><strong>diagram-maker</strong> — 画架构图&#x2F;流程图</li><li><strong>meme-maker</strong> — 做表情包</li><li><strong>notion</strong> — 操作 Notion 页面</li><li><strong>taskflow</strong> — 管理多步骤任务</li></ul><h3 id="实际场景：我写的一个部署-Skill"><a href="#实际场景：我写的一个部署-Skill" class="headerlink" title="实际场景：我写的一个部署 Skill"></a>实际场景：我写的一个部署 Skill</h3><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs markdown">---<br>name: deploy-blog<br><span class="hljs-section">description: 博客部署流程</span><br><span class="hljs-section">---</span><br><br>当用户说&quot;发布博客&quot;或&quot;部署博客&quot;时：<br><br><span class="hljs-bullet">1.</span> 在 /root/blogs 下检查是否有新的 Markdown 文件<br><span class="hljs-bullet">2.</span> 执行 hexo generate<br><span class="hljs-bullet">3.</span> 执行 git add + commit + push<br><span class="hljs-bullet">4.</span> 返回部署结果<br></code></pre></td></tr></table></figure><p>有了这个 skill，我说一句”发博客”，Agent 会自动跑完整个流程。省掉了打开终端、切换目录、敲命令的动作。</p><h3 id="踩坑：Skill-匹配优先级"><a href="#踩坑：Skill-匹配优先级" class="headerlink" title="踩坑：Skill 匹配优先级"></a>踩坑：Skill 匹配优先级</h3><p>多个 skill 的 description 可能互相覆盖。比如一个 skill 说”用户请求帮助时触发”，另一个说”用户说帮忙时触发”，Agent 可能两个都匹配。</p><p>解决办法是：description 写得越具体越好。不要用模糊的触发描述，加上明确的场景限制。</p><h3 id="踩坑：MCP-工具配置"><a href="#踩坑：MCP-工具配置" class="headerlink" title="踩坑：MCP 工具配置"></a>踩坑：MCP 工具配置</h3><p>如果你需要让 Agent 调用外部服务（如 GitHub、Jira、数据库），得配 MCP 服务器。配置在 <code>mcp.servers</code> 下。常见的坑是：</p><ul><li>环境变量没传进去（用 <code>env</code> 字段显式传递）</li><li>端口冲突（检查本地服务端口）</li><li>工具名加上了 <code>mcp-</code> 前缀，对应的工具策略忘了开</li></ul><hr><h2 id="第五步：实际应用场景"><a href="#第五步：实际应用场景" class="headerlink" title="第五步：实际应用场景"></a>第五步：实际应用场景</h2><h3 id="场景一：移动端-AI-编程助手"><a href="#场景一：移动端-AI-编程助手" class="headerlink" title="场景一：移动端 AI 编程助手"></a>场景一：移动端 AI 编程助手</h3><p>这是我用得最多的场景。在 Telegram 上给 Agent 发消息：</p><blockquote><p>“帮我检查线上服务器的 Nginx 配置，看看有没有明显的问题”</p></blockquote><p>Agent 会用 SSH 连上服务器，读配置，分析问题，然后把结果发到我的手机。全程不用开电脑。</p><h3 id="场景二：自动化运维"><a href="#场景二：自动化运维" class="headerlink" title="场景二：自动化运维"></a>场景二：自动化运维</h3><p>定时任务（cron） + Agent：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">openclaw cron add \<br>  --schedule <span class="hljs-string">&quot;0 9 * * 1&quot;</span> \<br>  --task <span class="hljs-string">&quot;检查服务器磁盘和内存状态，如果有异常发给我&quot;</span><br></code></pre></td></tr></table></figure><p>每周一早上九点，Agent 自动检查服务器健康状态，有问题直接推送。</p><h3 id="场景三：知识库问答"><a href="#场景三：知识库问答" class="headerlink" title="场景三：知识库问答"></a>场景三：知识库问答</h3><p>把团队文档放到工作区，Agent 就可以基于这些文档回答问题。不需要额外训练模型，不需要搭 RAG 流水线——文件放那里，Agent 自然会读。</p><h3 id="场景四：多-Agent-协作"><a href="#场景四：多-Agent-协作" class="headerlink" title="场景四：多 Agent 协作"></a>场景四：多 Agent 协作</h3><p>OpenClaw 支持 <code>sessions_spawn</code>——在一个任务里派生子 Agent 去做独立工作。比如：</p><ul><li>主 Agent 负责和用户对话</li><li>子 Agent 去查资料、生成代码、执行测试</li><li>结果返回来汇总结论</li></ul><p>这个架构适合复杂任务的并行处理，但目前对模型能力要求比较高。</p><h3 id="踩坑：Agent-做复杂任务时的-Token-消耗"><a href="#踩坑：Agent-做复杂任务时的-Token-消耗" class="headerlink" title="踩坑：Agent 做复杂任务时的 Token 消耗"></a>踩坑：Agent 做复杂任务时的 Token 消耗</h3><p>一个需要调用多个工具、来回几次对话才能完成的任务，Token 消耗比你想象的大。特别是用便宜模型的时候，问题可能不是钱，而是上下文窗口满了被截断。</p><p>解决方案：</p><ul><li>复杂任务拆成子 Agent 做，主 Agent 只做调度</li><li>定期用 <code>/prune</code> 压缩会话</li><li>不要在一个会话里堆太多无关对话</li></ul><hr><h2 id="第六步：安全措施"><a href="#第六步：安全措施" class="headerlink" title="第六步：安全措施"></a>第六步：安全措施</h2><h3 id="工具策略"><a href="#工具策略" class="headerlink" title="工具策略"></a>工具策略</h3><p>默认情况下 Agent 是有 shell 执行权限的。如果你只需要对话功能，把工具集缩小：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;tools&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;profile&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;minimal&quot;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p><code>minimal</code> 只保留 <code>session_status</code>。<code>coding</code> 模式有文件读写、exec、网络访问。<code>full</code> 不做任何限制。</p><h3 id="沙箱"><a href="#沙箱" class="headerlink" title="沙箱"></a>沙箱</h3><p>对安全性要求高的场景，可以开启 sandbox：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;agents&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;defaults&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;sandbox&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;non-main&quot;</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>Sandbox 模式下 Agent 的文件操作被限制在一个隔离目录里，不会影响到宿主机。</p><h3 id="踩坑：Agent-暴露在公网"><a href="#踩坑：Agent-暴露在公网" class="headerlink" title="踩坑：Agent 暴露在公网"></a>踩坑：Agent 暴露在公网</h3><p>Gateway 默认只监听 <code>127.0.0.1:18789</code>。如果你想让手机在外面也能访问控制台，不要直接把端口暴露出去。建议通过 Tailscale &#x2F; WireGuard 组网，或者用反向代理加认证。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>OpenClaw 最大的价值不在于它”能做什么”，而在于它把选择权交给了你。</p><ul><li>数据在你自己的服务器上</li><li>模型你可以随便换</li><li>渠道可以随意接</li><li>行为可以精确控制</li></ul><p>坏处也很明显——自部署意味着你要自己维护、自己排错、自己操心安全。不像 SaaS 产品那样即开即用。</p><p>但从”用别人的工具”到”拥有自己的 Agent”，这一步跨过去之后的自由度，是 SaaS 给不了的。</p><p>如果你已经在自部署的路上了，OpenClaw 是目前把”多渠道 + Agent + 工具链”整合得最好的一套方案。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/openclaw-guide-from-setup-to-practice/</id>
    <link href="https://blog.280303.xyz/posts/openclaw-guide-from-setup-to-practice/"/>
    <published>2026-06-01T05:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="为什么是-OpenClaw"><a href="#为什么是-OpenClaw" class="headerlink" title="为什么是 OpenClaw"></a>为什么是 OpenClaw</h2><p>市面上的 AI 助手产品很多，但大部分都是 SaaS——数据在别人手里，模型按调用收费，功能边界由厂商决定。</p>
<p>OpenClaw 走的是另一条路：自部署、多渠道、Agent 原生。你自己买服务器，自己配模型 API，自己决定用哪个模型、开哪些工具、连什么聊天软件。</p>
<p>它不是又一个 ChatGPT 网页版。它是一个<strong>网关</strong>——把你用的聊天软件（Telegram、Discord、Signal、飞书……）和一个 AI Agent 连起来，让你在口袋里随时有一个能写代码、能查资料、能操作服务器的 AI 助手。</p>
<p>这篇文章从零开始，把安装、配置、渠道对接、写 Agent 技能、实际使用场景和踩过的坑全串起来。</p>]]>
    </summary>
    <title>OpenClaw 从入门到落地：安装、配置、渠道接入与避坑指南</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="AI" scheme="https://blog.280303.xyz/categories/AI/"/>
    <category term="阿里云百炼" scheme="https://blog.280303.xyz/tags/%E9%98%BF%E9%87%8C%E4%BA%91%E7%99%BE%E7%82%BC/"/>
    <category term="Coding Plan" scheme="https://blog.280303.xyz/tags/Coding-Plan/"/>
    <category term="Token Plan" scheme="https://blog.280303.xyz/tags/Token-Plan/"/>
    <category term="AI编程" scheme="https://blog.280303.xyz/tags/AI%E7%BC%96%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="这件事是怎么开始的"><a href="#这件事是怎么开始的" class="headerlink" title="这件事是怎么开始的"></a>这件事是怎么开始的</h2><p>阿里云百炼的套餐体系，最近发生了一些变化。</p><p>最早就是 Coding Plan，面向个人开发者，按请求次数限制来计费。说白了就是你一个月能调用多少次模型，分 5 小时、周、月三个粒度去卡你。价格不高，门槛低，当时很多开发者都在用。</p><p>后来阿里云百炼推出了 Token Plan 团队版。这次换了个思路——按 Credits 计费，Credits 本质上就是 Token 消耗量。模型调用多少 Token 就扣多少 Credits，你用得精就省，用得多就多付。同时还支持团队多席位、共用 Credits 池、文本+图像多模态模型。</p><p>再后来，情况变了。</p><p>Coding Plan 上的模型池停止了更新。新的模型全都跑在 Token Plan 那边。Coding Plan 原地踏步，只剩老模型撑着。</p><p>按常理，一个不更新模型的套餐应该慢慢凉掉才对。</p><p>但现实恰恰相反。</p><p>Coding Plan 一放货就被抢空，二手市场的溢价比首发价还高。很多人蹲点都抢不到。</p><p>这就很有意思了。</p><span id="more"></span><h2 id="为什么一个”过时”的套餐还在被疯抢"><a href="#为什么一个”过时”的套餐还在被疯抢" class="headerlink" title="为什么一个”过时”的套餐还在被疯抢"></a>为什么一个”过时”的套餐还在被疯抢</h2><p>核心原因不是模型，是成本和心理预期。</p><h3 id="1-价格锚点太低了"><a href="#1-价格锚点太低了" class="headerlink" title="1. 价格锚点太低了"></a>1. 价格锚点太低了</h3><p>Coding Plan Pro 两百块一个月，买断制。你一个月写一万行代码是两百，写一百行也是两百。对于个人开发者来说，这就是一个死线清晰的固定支出。多了不心疼，少了不亏本。</p><p>Token Plan 最低一个席位 198 元&#x2F;月起，乍一看没差多少。但 Credits 是按用量走的。你今天多问了几个上下文，明天多跑了几轮 Agent，月底一看——超了。是每个人都能坦然接受这种”上不封顶”的支出模式。</p><p>对于个人开发者来说，固定价格的安心感，是 Token Plan 给不了的。</p><h3 id="2-老模型真的不能用吗"><a href="#2-老模型真的不能用吗" class="headerlink" title="2. 老模型真的不能用吗"></a>2. 老模型真的不能用吗</h3><p>这才是关键问题。</p><p>Coding Plan 上的模型是”旧”，但不是”废”。代码补全、行内建议、基础问答——这些场景对模型能力的要求其实没那么高。老模型跑得好好的，稳定，快，不抽风。</p><p>模型更新这件事，对重度 Agent 用户是刚需。对日常写代码的人来说，边际收益没那么大。就像一个用了三年的编辑器，不更新也不妨碍你每天用它写代码。</p><p>所以”模型不更新”这个缺点，对很多人的实际体验，打击有限。</p><h3 id="3-心理账户在作祟"><a href="#3-心理账户在作祟" class="headerlink" title="3. 心理账户在作祟"></a>3. 心理账户在作祟</h3><p>Token Plan 按量计费，每次调用都有”烧钱感”。哪怕是 Credits 没用完，你也会下意识去想”这波调用值不值”。</p><p>Coding Plan 没有这个问题。钱已经付了，不调用就是浪费，调多了反而觉得自己赚了。</p><p>这种心理上的舒适区，不是价格能简单衡量的。</p><h3 id="4-抢购的稀缺效应"><a href="#4-抢购的稀缺效应" class="headerlink" title="4. 抢购的稀缺效应"></a>4. 抢购的稀缺效应</h3><p>Coding Plan 限量供应，不是想买就能买。稀缺本身就制造了需求。</p><p>再加上二手市场有人囤货加价卖，更加剧了”不抢就亏了”的氛围。很多人跟风抢，抢完了其实自己也不知道能用多少。</p><p>但消费者的心理就是这样——你越限，他越想要。</p><h2 id="Coding-Plan-真正的优势"><a href="#Coding-Plan-真正的优势" class="headerlink" title="Coding Plan 真正的优势"></a>Coding Plan 真正的优势</h2><p><strong>价格锁定。</strong> 适合用量稳定的个人开发者。没有意外支出。</p><p><strong>使用自由。</strong> 不用算账，不用算 Credits，不用想”这个请求值不值”。想用就用，随心所欲。</p><p><strong>门槛低。</strong> 不需要团队账号，不需要企业资质，一个人就能买。而 Token Plan 团队版虽然有一人公司也能买，但整个产品定位和流程都是面向团队的。</p><p><strong>老模型够用。</strong> 代码场景没那么吃模型版本。稳定比新重要。</p><h2 id="Token-Plan-真正的优势"><a href="#Token-Plan-真正的优势" class="headerlink" title="Token Plan 真正的优势"></a>Token Plan 真正的优势</h2><p><strong>没有频次限制。</strong> 你不受 5 小时、周、月配额的限制。用量大的时候不会突然被卡住。这是最痛的点。</p><p><strong>模型更多。</strong> Qwen3.6-plus、GLM-5、DeepSeek-v3.2 都姓 Token。如果你做 Agent 开发、复杂推理、图像生成，Token Plan 是唯一选项。</p><p><strong>团队友好。</strong> 多人共享 Credits 池，管理起来比每个人单独买 Coding Plan 方便得多。</p><p><strong>计费更精细。</strong> 用多少花多少，不会出现”我这个月就用了两次却付了两百块”的情况。对于使用量波动大的人，Token Plan 其实更省钱。</p><p><strong>多模态。</strong> 不仅写代码，还能生成图片。如果你做的是带 UI 的原型项目，这一个点就足够让你选 Token Plan。</p><h2 id="到底怎么选"><a href="#到底怎么选" class="headerlink" title="到底怎么选"></a>到底怎么选</h2><p>这个问题其实被复杂化了。选哪个完全取决于你的使用画像。</p><p><strong>选 Coding Plan，如果你：</strong></p><ul><li>主要是个人写代码，用量不大也不小</li><li>偏好固定支出，不想操心用量监控</li><li>不需要最新的模型，老模型够用</li><li>对价格敏感，想要成本可控</li></ul><p><strong>选 Token Plan，如果你：</strong></p><ul><li>重度使用 AI 编程，每天大量交互</li><li>做 Agent 开发、复杂推理、多步骤任务</li><li>需要图像生成能力</li><li>团队使用，需要多人共用配额</li><li>用量波动大，想按实际消耗付费</li></ul><h2 id="关于未来"><a href="#关于未来" class="headerlink" title="关于未来"></a>关于未来</h2><p>从趋势上看，Token Plan 取代 Coding Plan 是迟早的事。</p><p>Coding Plan 不更新模型本身就是个信号——厂商在把资源往 Token Plan 倾斜。老的按请求计费模式，在 AI 服务成本越来越透明化的今天，确实越来越不适应了。</p><p>但取代归取代，不代表 Coding Plan 现在就不值得买。</p><p>说白了，选套餐这件事，没有绝对的”好”与”坏”，只有”适不适合你”。模型不在多，够用就行。钱不在少，花得值就行。</p><p>别被抢购潮带跑了节奏，想清楚自己真正需要什么，再下手。</p><hr><p><em>这篇文章基于个人经验和社区反馈整理，观点仅供参考。实际套餐政策和价格以阿里云百炼官网实时页面为准。</em></p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/coding-plan-vs-token-plan/</id>
    <link href="https://blog.280303.xyz/posts/coding-plan-vs-token-plan/"/>
    <published>2026-06-01T04:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="这件事是怎么开始的"><a href="#这件事是怎么开始的" class="headerlink" title="这件事是怎么开始的"></a>这件事是怎么开始的</h2><p>阿里云百炼的套餐体系，最近发生了一些变化。</p>
<p>最早就是 Coding Plan，面向个人开发者，按请求次数限制来计费。说白了就是你一个月能调用多少次模型，分 5 小时、周、月三个粒度去卡你。价格不高，门槛低，当时很多开发者都在用。</p>
<p>后来阿里云百炼推出了 Token Plan 团队版。这次换了个思路——按 Credits 计费，Credits 本质上就是 Token 消耗量。模型调用多少 Token 就扣多少 Credits，你用得精就省，用得多就多付。同时还支持团队多席位、共用 Credits 池、文本+图像多模态模型。</p>
<p>再后来，情况变了。</p>
<p>Coding Plan 上的模型池停止了更新。新的模型全都跑在 Token Plan 那边。Coding Plan 原地踏步，只剩老模型撑着。</p>
<p>按常理，一个不更新模型的套餐应该慢慢凉掉才对。</p>
<p>但现实恰恰相反。</p>
<p>Coding Plan 一放货就被抢空，二手市场的溢价比首发价还高。很多人蹲点都抢不到。</p>
<p>这就很有意思了。</p>]]>
    </summary>
    <title>Coding Plan 和 Token Plan 到底怎么选？旧模型为啥还抢破头</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="AI" scheme="https://blog.280303.xyz/categories/AI/"/>
    <category term="RAG" scheme="https://blog.280303.xyz/tags/RAG/"/>
    <category term="AI客服" scheme="https://blog.280303.xyz/tags/AI%E5%AE%A2%E6%9C%8D/"/>
    <category term="知识库" scheme="https://blog.280303.xyz/tags/%E7%9F%A5%E8%AF%86%E5%BA%93/"/>
    <category term="大模型" scheme="https://blog.280303.xyz/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/"/>
    <category term="DeepSeek" scheme="https://blog.280303.xyz/tags/DeepSeek/"/>
    <content>
      <![CDATA[<p><img src="/images/rag-enterprise-ai-customer-service/rag-customer-service-architecture.png" alt="企业级 RAG AI 智能客服整体架构：多端接入、知识库检索与大模型生成"></p><h2 id="为什么传统客服机器人效果那么差"><a href="#为什么传统客服机器人效果那么差" class="headerlink" title="为什么传统客服机器人效果那么差"></a>为什么传统客服机器人效果那么差</h2><p>传统客服机器人本质上是在做<strong>文本匹配</strong>，而不是<strong>理解</strong>。这是所有问题的根源。</p><h3 id="关键词匹配：形似神不似"><a href="#关键词匹配：形似神不似" class="headerlink" title="关键词匹配：形似神不似"></a>关键词匹配：形似神不似</h3><p>关键词匹配 + 同义词库的方案，看起来能覆盖常见问题，实际上脆弱得离谱。</p><p>用户问「怎么退货」，机器人匹配到「退款流程」的 FAQ。换个说法「我不想要了怎么办」，匹配失败，直接跳人工。同一个意思，措辞稍微变一下就匹配不到。</p><p>更糟的是<strong>误匹配</strong>。用户问「这个订单还能退吗」，机器人把「退」匹配到了「退差价」的 FAQ，答非所问。用户骂一句「什么垃圾」，流失了。</p><p>关键词匹配本质上就是正则 + 同义词库，没有任何语义理解能力。稍微复杂一点的句子，或者包含多个意图的问题，直接抓瞎。</p><h3 id="FAQ-配置：成本黑洞"><a href="#FAQ-配置：成本黑洞" class="headerlink" title="FAQ 配置：成本黑洞"></a>FAQ 配置：成本黑洞</h3><p>传统客服的知识库是靠人工一条条配 FAQ。每条要写标准问题、答案、关键词、扩展问法。</p><p>10 条还好，1000 条呢？我们一个电商售后项目，整理了三个月，配置了 2000 多条 FAQ，每天还在加。</p><p>而且业务变化快。产品线调整、促销规则变更、物流政策更新——每改一次，FAQ 就得跟着改一轮。维护成本高得离谱，而且永远跟不上业务变化的速度。</p><h3 id="没有上下文：对话树是死的"><a href="#没有上下文：对话树是死的" class="headerlink" title="没有上下文：对话树是死的"></a>没有上下文：对话树是死的</h3><p>多轮对话树的设计思路是「用户按剧本走」。但实际用户根本不按剧本来：</p><figure class="highlight dns"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs dns">用户：我想查一下快递<br>机器人：请提供订单号<br>用户：KF<span class="hljs-number">20240315001</span><br>机器人：您的快递正在配送中，预计明天到达<br>用户：那个红色的呢？<br></code></pre></td></tr></table></figure><p>最后一句「那个红色的呢」，正常人类都知道是在问「红色那件商品的快递情况」，但传统机器人理解不了。它没有对话记忆，也不会做指代消解。</p><p>结果就是：这种对话只能转人工。准确率常年卡在 60% 左右，客户不满意，项目被砍。</p><h2 id="RAG-是什么"><a href="#RAG-是什么" class="headerlink" title="RAG 是什么"></a>RAG 是什么</h2><p>RAG — Retrieval Augmented Generation，翻译过来是<strong>检索增强生成</strong>。</p><p>但说人话就是：<strong>让大模型在回答之前，先去查一下你的知识库，然后根据查到的内容来回答。</strong></p><p>它不是训练模型，也不是微调。模型本身的参数和权重没有任何变化。它只是在推理流程中间插了一步——从外部知识库检索相关的内容，然后拼到 prompt 里喂给大模型。</p><p>RAG 的工作流程：</p><pre><code class=" mermaid">flowchart TD    A[用户提问] --&gt; B[问题改写]    B --&gt; C&#123;向量数据库&#125;    B --&gt; D&#123;关键词检索&#125;    C --&gt; E[Hybrid Search]    D --&gt; E    E --&gt; F[Rerank重排序]    F --&gt; G[上下文组装]    G --&gt; H[大模型生成答案]    H --&gt; I[返回给用户]</code></pre><p>这个流程看着简单，但每一步拆开都有很多坑。</p><h2 id="企业-AI-客服整体架构"><a href="#企业-AI-客服整体架构" class="headerlink" title="企业 AI 客服整体架构"></a>企业 AI 客服整体架构</h2><p>先看完整的架构：</p><pre><code class=" mermaid">flowchart LR    subgraph 前端接入        A[微信公众号/H5]         B[UniApp多端]        C[PC Web - Vue3]    end    A --&gt; D[API Gateway - Go]    B --&gt; D    C --&gt; D    D --&gt; E[会话服务]    D --&gt; F[知识库管理服务]    D --&gt; G[用户管理/鉴权]    E --&gt; H[RAG Service]    F --&gt; H    H --&gt; I[(PostgreSQL 业务数据)]    H --&gt; J[(Redis 缓存/会话)]    H --&gt; K[(Milvus 向量库)]    H --&gt; L[LLM API - DeepSeek/Qwen3]</code></pre><h3 id="技术栈选型"><a href="#技术栈选型" class="headerlink" title="技术栈选型"></a>技术栈选型</h3><table><thead><tr><th>组件</th><th>选型</th><th>为什么</th></tr></thead><tbody><tr><td>前端</td><td>Vue3 + UniApp</td><td>一套代码跑多端，省人力</td></tr><tr><td>后端</td><td>Golang</td><td>并发好，Goroutine 处理 SSE 流式输出省心</td></tr><tr><td>业务数据库</td><td>PostgreSQL</td><td>性能稳定，支持 JSON 字段</td></tr><tr><td>缓存</td><td>Redis</td><td>会话管理、热点问题缓存、限流</td></tr><tr><td>向量库</td><td>Milvus</td><td>开源成熟，企业级，支持标量+向量混合检索</td></tr><tr><td>大模型</td><td>DeepSeek + Qwen3</td><td>国产模型性价比高，支持长上下文</td></tr></tbody></table><p>选 Golang 的原因很实际。客服系统的核心接口是对话，需要流式输出。Go 的 goroutine + channel 天然适合做 SSE（Server-Sent Events）。如果用 Java 做，还得折腾 WebFlux 或者异步 Servlet，代码复杂度高一截。</p><p>选 DeepSeek 和 Qwen3 主要是成本和合规。国外模型在国内企业场景下延迟高、有数据出境的合规风险。DeepSeek 的 API 价格相对低，Qwen3 语义理解能力强，两个互补着用。</p><h2 id="知识库构建过程"><a href="#知识库构建过程" class="headerlink" title="知识库构建过程"></a>知识库构建过程</h2><p><img src="/images/rag-enterprise-ai-customer-service/rag-knowledge-pipeline.png" alt="RAG 知识库构建流水线：文档解析、OCR、语义切块、Embedding 与索引入库"></p><h3 id="文档上传与解析"><a href="#文档上传与解析" class="headerlink" title="文档上传与解析"></a>文档上传与解析</h3><p>企业的知识来源五花八门。我们遇到过的：</p><ul><li>产品说明书（PDF）</li><li>内部培训文档（Word）</li><li>运维运维文档（Markdown）</li><li>官网帮助中心（需要爬虫抓取）</li></ul><p>上传之后就按类型分发到不同的解析器：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Parser <span class="hljs-keyword">interface</span> &#123;<br>    Parse(reader io.Reader) ([]TextBlock, <span class="hljs-type">error</span>)<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetParser</span><span class="hljs-params">(fileType <span class="hljs-type">string</span>)</span></span> Parser &#123;<br>    <span class="hljs-keyword">switch</span> fileType &#123;<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;pdf&quot;</span>:<br>        <span class="hljs-keyword">return</span> &amp;PDFParser&#123;&#125;<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;docx&quot;</span>:<br>        <span class="hljs-keyword">return</span> &amp;WordParser&#123;&#125;<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;md&quot;</span>:<br>        <span class="hljs-keyword">return</span> &amp;MarkdownParser&#123;&#125;<br>    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;html&quot;</span>:<br>        <span class="hljs-keyword">return</span> &amp;HTMLParser&#123;&#125;<br>    <span class="hljs-keyword">default</span>:<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>PDF 解析是最大的坑。很多 PDF 看着排版漂亮，但底层是图片拼起来的——文字根本提不出来。后来我们对这类 PDF 走了 OCR 通道，用 PaddleOCR 做文字识别，虽然慢一点但至少不漏内容。</p><h3 id="Chunk-切块策略"><a href="#Chunk-切块策略" class="headerlink" title="Chunk 切块策略"></a>Chunk 切块策略</h3><p>解析完文档之后，最关键的步骤是切块。说白了就是：把一篇文章切成一段一段的，每一段作为一个独立的知识单元入库。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> ChunkStrategy <span class="hljs-keyword">struct</span> &#123;<br>    Size    <span class="hljs-type">int</span><br>    Overlap <span class="hljs-type">int</span><br>    SplitBy <span class="hljs-type">string</span> <span class="hljs-comment">// paragraph | sentence | fixed</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NewDefaultStrategy</span><span class="hljs-params">()</span></span> ChunkStrategy &#123;<br>    <span class="hljs-keyword">return</span> ChunkStrategy&#123;<br>        Size:    <span class="hljs-number">500</span>,<br>        Overlap: <span class="hljs-number">100</span>,<br>        SplitBy: <span class="hljs-string">&quot;paragraph&quot;</span>,<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>Chunk Size &#x3D; 500，Overlap &#x3D; 100</strong>，这个配置是我们试了十几种组合试出来的。</p><p>为什么是 500？太小的 chunk，比如 100~200 个字，语义不完整。比如一段话：</p><blockquote><p>退货流程需要用户先提交申请，客服审核通过后，用户将商品寄回，仓库收到后 3 个工作日内退款。</p></blockquote><p>如果 chunk size 太小，可能只切到「客服审核通过后」就断了，后面的退款流程就丢了。召回的时候搜到了这段，但内容是不完整的。</p><p>太大也不行，比如 2000 字一段。里面可能讨论了退货、换货、退款三个话题。用户问「换货怎么操作」，向量检索把这整段都召回了，但里面只有一小段是讲换货的，其余都是噪声。大模型看到这么一大段，注意力被分散了，反而答不准。</p><p>Overlap &#x3D; 100 的目的是让相邻 chunk 之间有一部分重叠内容。这样切在边界上的关键信息不会丢失。</p><h3 id="Embedding-向量化"><a href="#Embedding-向量化" class="headerlink" title="Embedding 向量化"></a>Embedding 向量化</h3><p>文本切好之后，需要转成向量才能做相似度检索。这个过程叫做 Embedding。</p><p>简单理解：一段文本 -&gt; 一个固定长度的数字数组。比如：</p><figure class="highlight jboss-cli"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs jboss-cli"><span class="hljs-string">&quot;如何退货&quot;</span> → [0.12, -0.34, 0.56, <span class="hljs-string">...</span>, 0.89]  <span class="hljs-string">//</span> 768维向量<br></code></pre></td></tr></table></figure><p>两段文本的语义相似度，用余弦距离算一下就行。越相近的文本，向量距离越小。</p><p>Embedding 模型的选择：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment"># BGE-M3 示例</span><br><span class="hljs-keyword">from</span> sentence_transformers <span class="hljs-keyword">import</span> SentenceTransformer<br><br>model = SentenceTransformer(<span class="hljs-string">&quot;BAAI/bge-m3&quot;</span>)<br>embeddings = model.encode([<span class="hljs-string">&quot;如何申请退货&quot;</span>, <span class="hljs-string">&quot;退货流程是怎样的&quot;</span>])<br><span class="hljs-built_in">print</span>(embeddings.shape)  <span class="hljs-comment"># (2, 1024)</span><br></code></pre></td></tr></table></figure><p>我们线上用的是 BGE-M3，1024 维，支持 100+ 种语言，中文效果不错。Qwen3-Embedding 我们也试过，在某些垂直领域（如医疗、法律）表现更好，但通用场景下 BGE-M3 性价比更高。</p><p>选 Embedding 模型不要光看 MTEB 榜单。企业场景下要实际测你的文档。拿 200 个真实问答做召回测试，看 top-5 命中率。榜单第一名的模型不一定在你业务数据上好用。</p><h2 id="RAG-最核心：知识召回"><a href="#RAG-最核心：知识召回" class="headerlink" title="RAG 最核心：知识召回"></a>RAG 最核心：知识召回</h2><p><img src="/images/rag-enterprise-ai-customer-service/rag-hybrid-search-rerank.png" alt="Hybrid Search 与 Rerank：向量检索和关键词检索融合后再重排序"></p><p>很多人以为 RAG 就是「把问题转成向量 -&gt; 去向量库搜一下 -&gt; 把结果拼到 prompt 里」。实际做起来远不止这么简单。</p><h3 id="只做向量检索为什么不够"><a href="#只做向量检索为什么不够" class="headerlink" title="只做向量检索为什么不够"></a>只做向量检索为什么不够</h3><p>向量检索本质上是语义检索。它能理解「退货」和「退换货流程」是相关的。但它有一个致命缺点：对精确匹配不敏感。</p><p>举个例子，用户问：「订单 KF20240315001 的快递到哪了？」</p><p>向量检索会理解「订单」「快递」「在哪」这几个语义，但<strong>它不会在意 KF20240315001 这个具体的订单号</strong>。如果知识库里有 100 条快递相关的 FAQ，向量检索可能会召回「如何查询快递」这种通用 FAQ，而不是跟 KF20240315001 相关的那个具体回答。</p><p>关键词检索正好相反。它对语义不敏感，但精确匹配能力强。它能准确找到包含某个订单号或者 SKU 编码的文档。</p><p>所以 Hybrid Search 是必须的。</p><h3 id="Hybrid-Search-实现"><a href="#Hybrid-Search-实现" class="headerlink" title="Hybrid Search 实现"></a>Hybrid Search 实现</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> SearchResult <span class="hljs-keyword">struct</span> &#123;<br>    ChunkID    <span class="hljs-type">string</span><br>    Content    <span class="hljs-type">string</span><br>    VectorScore <span class="hljs-type">float64</span><br>    KeywordScore <span class="hljs-type">float64</span><br>    FinalScore  <span class="hljs-type">float64</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">HybridSearch</span><span class="hljs-params">(query <span class="hljs-type">string</span>, topK <span class="hljs-type">int</span>)</span></span> []SearchResult &#123;<br>    <span class="hljs-comment">// 1. 向量检索</span><br>    queryVec := embeddingModel.Encode(query)<br>    vectorResults := milvus.Search(queryVec, topK*<span class="hljs-number">2</span>)<br>    <br>    <span class="hljs-comment">// 2. 关键词检索</span><br>    keywordResults := pg.FullTextSearch(query, topK*<span class="hljs-number">2</span>)<br>    <br>    <span class="hljs-comment">// 3. 分数融合</span><br>    results := mergeAndNormalize(vectorResults, keywordResults)<br>    <br>    <span class="hljs-comment">// RRF 融合算法</span><br>    <span class="hljs-keyword">for</span> _, r := <span class="hljs-keyword">range</span> results &#123;<br>        r.FinalScore = <span class="hljs-number">0.5</span>*r.VectorScore + <span class="hljs-number">0.5</span>*r.KeywordScore<br>    &#125;<br>    <br>    sort.Slice(results, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(i, j <span class="hljs-type">int</span>)</span></span> <span class="hljs-type">bool</span> &#123;<br>        <span class="hljs-keyword">return</span> results[i].FinalScore &gt; results[j].FinalScore<br>    &#125;)<br>    <br>    <span class="hljs-keyword">return</span> results[:topK]<br>&#125;<br></code></pre></td></tr></table></figure><p>分数融合我们用的是最简单的加权平均。向量分和关键词分各占 0.5。有些场景需要调整权重，比如用户问题里有大量专有名词（订单号、SKU、产品型号），就适当提高关键词权重。</p><h3 id="Rerank-为什么重要"><a href="#Rerank-为什么重要" class="headerlink" title="Rerank 为什么重要"></a>Rerank 为什么重要</h3><p>Hybrid Search 召回的结果一般是 20 条。但这 20 条里，可能只有 3~5 条是真正有用的。</p><p>Rerank 做的事情是：<strong>用另一个模型把这 20 条重新排序，把最相关的排前面。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> rerankers <span class="hljs-keyword">import</span> Reranker<br><br>reranker = Reranker(<span class="hljs-string">&quot;BAAI/bge-reranker-v2-m3&quot;</span>)<br>results = reranker.rank(<br>    query=<span class="hljs-string">&quot;怎么退货&quot;</span>,<br>    docs=[<br>        <span class="hljs-string">&quot;退货流程需要登录后...&quot;</span>,<br>        <span class="hljs-string">&quot;我们的营业时间是...&quot;</span>,<br>        <span class="hljs-string">&quot;换货流程与退货不同...&quot;</span>,<br>        <span class="hljs-string">&quot;退款将在收到退货后...&quot;</span>,<br>        ...<br>    ]<br>)<br><span class="hljs-comment"># 返回排序后的结果，保留 top-5</span><br></code></pre></td></tr></table></figure><p>没有 Rerank 会出现什么问题？</p><p>知识库里的文档，相似度高的不一定是答案需要的。比如：</p><ul><li>用户问「怎么退货」</li><li>向量检索召回了「退货流程」（相关度 0.89）和「换货流程」（相关度 0.82）</li><li>按照向量距离排，前三条都是跟「退款」「售后」相关的，但最相关的「退货流程」其实排在第二条</li><li>结果大模型看到两条不太相关的内容，混淆了，给出了错误的回答</li></ul><p>加了 Rerank 之后，准确率从 72% 提升到了 91%。这是线上真实数据。</p><h2 id="多租户知识库设计"><a href="#多租户知识库设计" class="headerlink" title="多租户知识库设计"></a>多租户知识库设计</h2><p>SaaS 系统最核心的问题：怎么保证不同客户的知识库数据不串？</p><h3 id="数据隔离方案"><a href="#数据隔离方案" class="headerlink" title="数据隔离方案"></a>数据隔离方案</h3><p>最简单的做法：<strong>按 tenant_id 过滤</strong>。每条知识库记录都带上 tenant_id，查询的时候强制过滤。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">CREATE TABLE</span> knowledge_docs (<br>    id          BIGSERIAL <span class="hljs-keyword">PRIMARY KEY</span>,<br>    tenant_id   <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">64</span>) <span class="hljs-keyword">NOT NULL</span>,<br>    doc_name    <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT NULL</span>,<br>    doc_type    <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">32</span>) <span class="hljs-keyword">NOT NULL</span>, <span class="hljs-comment">-- pdf, docx, md</span><br>    status      <span class="hljs-type">INT</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>,       <span class="hljs-comment">-- 0=待处理, 1=已处理</span><br>    created_at  <span class="hljs-type">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> NOW(),<br>    updated_at  <span class="hljs-type">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> NOW()<br>);<br><br><span class="hljs-keyword">CREATE</span> INDEX idx_docs_tenant <span class="hljs-keyword">ON</span> knowledge_docs(tenant_id);<br><br><span class="hljs-keyword">CREATE TABLE</span> knowledge_chunks (<br>    id          BIGSERIAL <span class="hljs-keyword">PRIMARY KEY</span>,<br>    tenant_id   <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">64</span>) <span class="hljs-keyword">NOT NULL</span>,<br>    doc_id      <span class="hljs-type">BIGINT</span> <span class="hljs-keyword">REFERENCES</span> knowledge_docs(id),<br>    chunk_index <span class="hljs-type">INT</span> <span class="hljs-keyword">NOT NULL</span>,<br>    content     TEXT <span class="hljs-keyword">NOT NULL</span>,<br>    embedding   VECTOR(<span class="hljs-number">1024</span>),      <span class="hljs-comment">-- pgvector</span><br>    metadata    JSONB,<br>    created_at  <span class="hljs-type">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> NOW()<br>);<br><br><span class="hljs-keyword">CREATE</span> INDEX idx_chunks_tenant <span class="hljs-keyword">ON</span> knowledge_chunks(tenant_id);<br></code></pre></td></tr></table></figure><p>Milvus 那边的处理方式类似。建 Collection 时带上 tenant_id 作为分区键，检索时指定 partition key 过滤。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// Milvus 多租户过滤</span><br>searchParams := <span class="hljs-string">`&#123;</span><br><span class="hljs-string">    &quot;tenant_id&quot;: &quot;`</span> + tenantID + <span class="hljs-string">`&quot;</span><br><span class="hljs-string">&#125;`</span><br><br>results, err := milvusClient.Search(ctx, &amp;milvus.SearchRequest&#123;<br>    CollectionName: <span class="hljs-string">&quot;knowledge_chunks&quot;</span>,<br>    PartitionNames: []<span class="hljs-type">string</span>&#123;&#125;,<br>    Data:           []entity.Vector&#123;queryVec&#125;,<br>    SearchParams:   searchParams,<br>    Filter:         fmt.Sprintf(<span class="hljs-string">&quot;tenant_id == &#x27;%s&#x27;&quot;</span>, tenantID),<br>    Limit:          <span class="hljs-number">20</span>,<br>&#125;)<br></code></pre></td></tr></table></figure><p>这里有个注意点：<strong>不要在查询的时候才想起来过滤 tenant_id。</strong>写数据的时候就按 tenant_id 分 collection 或者分 partition，检索时直接从对应的 partition 里搜。否则所有客户的数据混在一起，出隔离问题的风险很大。</p><h3 id="权限控制"><a href="#权限控制" class="headerlink" title="权限控制"></a>权限控制</h3><p>数据隔离之外，还有权限控制。同一个企业内的不同角色能看到的知识库范围是不一样的。</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs css">租户 <span class="hljs-selector-tag">A</span><br>├── 公共知识库（所有人可见）<br>├── 售后知识库（售后团队可见）<br>└── 技术知识库（技术团队可见）<br></code></pre></td></tr></table></figure><p>实现上就是在知识库文档上加 permission_groups 字段，检索时加上用户角色过滤。</p><h2 id="项目中遇到的几个坑"><a href="#项目中遇到的几个坑" class="headerlink" title="项目中遇到的几个坑"></a>项目中遇到的几个坑</h2><p>做 RAG 客服这大半年，踩过的坑能写一本小册子。挑几个印象深刻的：</p><h3 id="坑-1：Chunk-切太大，召回全是噪声"><a href="#坑-1：Chunk-切太大，召回全是噪声" class="headerlink" title="坑 1：Chunk 切太大，召回全是噪声"></a>坑 1：Chunk 切太大，召回全是噪声</h3><p>刚开始做的时候，我们参考了一些开源项目，把 chunk size 设到了 1500。结果用户问一个简单的问题，召回了 2000 多字的文档片段。大模型看到这么多内容，注意力被稀释了，回答经常跑偏。</p><p><strong>解决：</strong> 缩短到 500~600，配合 overlap。同时也做了 chunk 内标题语义提取——把段落标题也塞进 chunk 里帮助定位。</p><h3 id="坑-2：Chunk-切太小，上下文断了"><a href="#坑-2：Chunk-切太小，上下文断了" class="headerlink" title="坑 2：Chunk 切太小，上下文断了"></a>坑 2：Chunk 切太小，上下文断了</h3><p>有一次用户问「售后退款需要什么材料」。我们知识库里有一条 chunk 是「退款材料清单：1. 订单截图 2. 退款原因说明 3. 商品照片」，但前面的 chunk 被切到了「退款流程第一步：确认收货状态」。两条 chunk 都不完整。</p><p><strong>解决：</strong> 语义切块，按段落切而不是按固定字符切。同时通过 overlap 保证关键上下文不丢失。</p><h3 id="坑-3：切换-Embedding-模型，全部重建索引"><a href="#坑-3：切换-Embedding-模型，全部重建索引" class="headerlink" title="坑 3：切换 Embedding 模型，全部重建索引"></a>坑 3：切换 Embedding 模型，全部重建索引</h3><p>初期用的 text2vec-large-chinese，后来发现效果不如 BGE-M3，决定换。结果发现——不同模型产生的向量空间不一样，没法直接增量迁移。只能全部重新跑一遍 Embedding。</p><p>100 万份文档重跑，花了 3 天。中间还因为并发数太高把 GPU 打满了，服务挂了两次。</p><p><strong>解决：</strong> 做好预案。先在小数据集上验证新模型效果，确认提升明显再换。跑任务时控制并发，分批做。现在业内有个趋势是用同一个模型公司出品的 Embedding+Reranker 组合，减少切换概率。</p><h3 id="坑-4：向量库内存占用爆炸"><a href="#坑-4：向量库内存占用爆炸" class="headerlink" title="坑 4：向量库内存占用爆炸"></a>坑 4：向量库内存占用爆炸</h3><p>Milvus 默认配置索引，100 万条 1024 维向量，内存占用直奔 20GB+。客户的服务器配置是 32GB 的，Milvus 一个进程就吃满了，PostgreSQL 和其他服务只能挤在一起。</p><p><strong>解决：</strong></p><ul><li>使用 IVF_FLAT 索引替代默认的 HNSW，内存降低 40%</li><li>降低向量维度？不行，效果会掉。改用了 Mmap 模式，把不常访问的数据映射到磁盘</li><li>分片存储，按 tenant_id 分 partition</li></ul><h3 id="坑-5：大模型上下文装不下"><a href="#坑-5：大模型上下文装不下" class="headerlink" title="坑 5：大模型上下文装不下"></a>坑 5：大模型上下文装不下</h3><p>召回的结果加上 prompt 模板，有时候能到 15K tokens。早期用的 DeepSeek-V2 上下文只有 32K，一超过就截断，截掉了关键信息。</p><p><strong>解决：</strong> 换了支持 128K 上下文的模型（Qwen3-72B 和 DeepSeek-V3）。同时做了上下文压缩——如果召回结果超过限制，按 Rerank 得分裁剪，只保留分数最高的几条。</p><h2 id="成本分析"><a href="#成本分析" class="headerlink" title="成本分析"></a>成本分析</h2><p>很多人担心 AI 客服成本高不可攀。其实算下来还好。假设 100 家企业客户，知识库总计约 10 万份文档（平均每家企业 1000 份）。</p><h3 id="基础设施成本（月）"><a href="#基础设施成本（月）" class="headerlink" title="基础设施成本（月）"></a>基础设施成本（月）</h3><table><thead><tr><th>组件</th><th>配置</th><th>月费用（约）</th></tr></thead><tbody><tr><td>PostgreSQL</td><td>16C 64G 1TB SSD</td><td>¥3000</td></tr><tr><td>Redis</td><td>8C 16G</td><td>¥800</td></tr><tr><td>Milvus</td><td>8C 32G 200G SSD</td><td>¥2500</td></tr><tr><td>应用服务器</td><td>8C 16G × 2 台</td><td>¥2000</td></tr><tr><td><strong>合计</strong></td><td></td><td><strong>¥8300&#x2F;月</strong></td></tr></tbody></table><h3 id="Embedding-成本（一次性）"><a href="#Embedding-成本（一次性）" class="headerlink" title="Embedding 成本（一次性）"></a>Embedding 成本（一次性）</h3><p>10 万份文档，按平均每份切 30 个 chunk，总计约 300 万条。</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">300</span>万条 × <span class="hljs-number">0</span>.<span class="hljs-number">0005</span>元/条（BGE-M3本地部署） ≈ ¥<span class="hljs-number">1500</span>（一次性）<br></code></pre></td></tr></table></figure><p>如果用本地 GPU 做，基本只有电费。我们用的是一张 RTX 4090 跑离线任务。</p><h3 id="模型调用成本（按日估）"><a href="#模型调用成本（按日估）" class="headerlink" title="模型调用成本（按日估）"></a>模型调用成本（按日估）</h3><figure class="highlight maxima"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs maxima">日均对话量：<span class="hljs-number">10</span>万次<br>每次对话平均 <span class="hljs-built_in">tokens</span>：输入 <span class="hljs-number">3000</span> + 输出 <span class="hljs-number">500</span> = <span class="hljs-number">3500</span><br>日均 <span class="hljs-built_in">tokens</span>：<span class="hljs-number">10</span>万 × <span class="hljs-number">3500</span> = <span class="hljs-number">3.5</span>亿<br><br>DeepSeek API 价格：输入 ¥<span class="hljs-number">0.5</span>/百万<span class="hljs-built_in">tokens</span>，输出 ¥<span class="hljs-number">2</span>/百万<span class="hljs-built_in">tokens</span><br>日均成本：<span class="hljs-number">3.5</span>亿 × <span class="hljs-number">0.0007</span>元/1000<span class="hljs-built_in">tokens</span>（混合价）≈ ¥<span class="hljs-number">245</span>/天<br>月成本：¥<span class="hljs-number">7350</span><br></code></pre></td></tr></table></figure><p>实际上因为很多简单问题可以用缓存回答，加上 BGE-M3 的本地 reranker，实际月成本大概在 ¥5000~8000 之间。</p><h3 id="总成本"><a href="#总成本" class="headerlink" title="总成本"></a>总成本</h3><figure class="highlight tap"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs tap">基础设施：¥8300<br>模型调用：¥5000~¥8000<br>总计：约 ¥13,000~¥16,000/月<br><br>分摊到<span class="hljs-number"> 100 </span>家企业：每家企业 ¥130~¥160/月<br></code></pre></td></tr></table></figure><p>对比招一个客服的月薪（¥5000~¥8000），RAG 客服能处理 70% 左右的常见问题，剩下的 30% 转人工。企业其实是省钱了的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>做了两年多 RAG 客服项目，交付了十几个客户，我的体会是：<strong>RAG 本身不是什么新技术，但做好的难度被远远低估了。</strong></p><p>真正影响最终效果的因素，按重要性排序：</p><ol><li><strong>知识质量</strong> — 垃圾进垃圾出。文档不规范、信息过时，模型再强也救不了</li><li><strong>Chunk 策略</strong> — 太大太小都不行，要找到适合你业务的粒度</li><li><strong>Hybrid Search</strong> — 纯向量检索不够，必须加关键词兜底</li><li><strong>Rerank</strong> — 召回之后的二次筛选，准确率提升 10~20 个百分点</li><li><strong>Prompt 设计</strong> — 让模型学会「不知道就说不知道」，而不是瞎编</li></ol><p>RAG 不是万能的。它不能解决「知识库里根本没有答案」的问题，也不能代替人工客服做情感安抚和复杂沟通。但它是目前企业知识库和 AI 客服落地最成熟的方案——不魔改模型、不烧 GPU、数据可控、效果可预期。</p><p>如果你的团队正准备做 AI 客服，建议从一个小场景开始：选一个产品线，配好知识库，走通全流程，再逐步扩展。一口吃不成胖子，RAG 也一样。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/rag-enterprise-ai-customer-service/</id>
    <link href="https://blog.280303.xyz/posts/rag-enterprise-ai-customer-service/"/>
    <published>2026-06-01T03:00:00.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/rag-enterprise-ai-customer-service/rag-customer-service-architecture.png" alt="企业级 RAG AI 智能客服整体架构：多端接入、知识库检索与大模型生成"></]]>
    </summary>
    <title>基于RAG构建企业级AI智能客服：从架构到落地全流程实践</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="Java" scheme="https://blog.280303.xyz/categories/Java/"/>
    <category term="Java" scheme="https://blog.280303.xyz/tags/Java/"/>
    <category term="Spring Boot" scheme="https://blog.280303.xyz/tags/Spring-Boot/"/>
    <category term="Exception" scheme="https://blog.280303.xyz/tags/Exception/"/>
    <category term="后端" scheme="https://blog.280303.xyz/tags/%E5%90%8E%E7%AB%AF/"/>
    <content>
      <![CDATA[<h2 id="为什么会遇到这个问题"><a href="#为什么会遇到这个问题" class="headerlink" title="为什么会遇到这个问题"></a>为什么会遇到这个问题</h2><p>写 Java 最烦的事之一：线上跑得好好的，突然冒出个异常，日志一翻——空的。或者只看到一句 <code>NullPointerException</code>，根本不知道从哪蹦出来的。</p><p>更头疼的是，用户发来截图说「你的页面崩了」，你连哪个接口报的错都找不到。这种体验，做过后端的都懂。</p><p>Java 的异常机制本身很完善——try-catch、throws、finally——但实际项目里，总有一些漏网之鱼：</p><ul><li>开发忘了加 try-catch</li><li>异步线程里的异常，主线程感知不到</li><li>框架层抛出的异常，业务代码接不住</li></ul><p>所以<strong>全局异常捕获</strong>不是一个「锦上添花」的功能，而是每个 Java 项目都应该有的基础设施。</p><h2 id="全局异常捕获的本质"><a href="#全局异常捕获的本质" class="headerlink" title="全局异常捕获的本质"></a>全局异常捕获的本质</h2><p>说白了，全局异常捕获就是给整个应用装一个「逃生网」。不管异常从哪冒出来，最终都能落到一个统一的地方处理，然后决定怎么响应——记录日志、返回友好的错误信息、或者做降级处理。</p><p>Java 层面提供了几个入口来做这件事，不同场景用不同的方案。</p><h2 id="方案一：Thread-UncaughtExceptionHandler-—-主线程最后的防线"><a href="#方案一：Thread-UncaughtExceptionHandler-—-主线程最后的防线" class="headerlink" title="方案一：Thread.UncaughtExceptionHandler — 主线程最后的防线"></a>方案一：Thread.UncaughtExceptionHandler — 主线程最后的防线</h2><p>这是 Java 最底层的全局异常捕获机制。当线程抛出异常但没有被 catch 时，JVM 会调用线程的 <code>UncaughtExceptionHandler</code>。</p><h3 id="最基本的用法"><a href="#最基本的用法" class="headerlink" title="最基本的用法"></a>最基本的用法</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java">Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -&gt; &#123;<br>    System.err.println(<span class="hljs-string">&quot;线程 [&quot;</span> + thread.getName() + <span class="hljs-string">&quot;] 挂了，原因：&quot;</span>);<br>    throwable.printStackTrace();<br>    <span class="hljs-comment">// 这里可以发报警、写日志、做兜底</span><br>&#125;);<br></code></pre></td></tr></table></figure><p>这段代码一写，整个 JVM 进程里所有线程未捕获的异常，都会走到这个回调里。</p><h3 id="实际项目里的增强版"><a href="#实际项目里的增强版" class="headerlink" title="实际项目里的增强版"></a>实际项目里的增强版</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">GlobalExceptionHandler</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">Thread</span>.UncaughtExceptionHandler &#123;<br>    <br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Logger</span> <span class="hljs-variable">log</span> <span class="hljs-operator">=</span> LoggerFactory.getLogger(GlobalExceptionHandler.class);<br>    <br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">uncaughtException</span><span class="hljs-params">(Thread t, Throwable e)</span> &#123;<br>        log.error(<span class="hljs-string">&quot;未捕获异常 - 线程: &#123;&#125; (id=&#123;&#125;), 线程组: &#123;&#125;&quot;</span>, <br>            t.getName(), t.getId(), t.getThreadGroup() != <span class="hljs-literal">null</span> ? t.getThreadGroup().getName() : <span class="hljs-string">&quot;null&quot;</span>, e);<br>        <br>        <span class="hljs-comment">// 发送告警通知（邮件、钉钉、企业微信等）</span><br>        alertService.sendAlert(<span class="hljs-string">&quot;线程 &quot;</span> + t.getName() + <span class="hljs-string">&quot; 崩溃&quot;</span>, e);<br>        <br>        <span class="hljs-comment">// 记录埋点</span><br>        metricsCollector.increment(<span class="hljs-string">&quot;jvm.uncaught.exception&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>启动时注册：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> &#123;<br>    Thread.setDefaultUncaughtExceptionHandler(<span class="hljs-keyword">new</span> <span class="hljs-title class_">GlobalExceptionHandler</span>());<br>    SpringApplication.run(YourApp.class, args);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="容易踩的坑"><a href="#容易踩的坑" class="headerlink" title="容易踩的坑"></a>容易踩的坑</h3><p><code>setDefaultUncaughtExceptionHandler</code> 设置的是全局默认处理器。如果某个线程自己调了 <code>setUncaughtExceptionHandler</code> 设置了专有处理器，那全局的这个不会覆盖它。这点要清楚——它不是万能的，它是兜底的兜底。</p><p>另外，<strong>守护线程的异常</strong>也会被捕获，但要注意守护线程不受 JVM 退出保护，它抛异常时 JVM 可能已经在退出了，handler 里的逻辑可能没跑完。</p><h2 id="方案二：Spring-Boot-的-ControllerAdvice-—-Web-层的统一拦截"><a href="#方案二：Spring-Boot-的-ControllerAdvice-—-Web-层的统一拦截" class="headerlink" title="方案二：Spring Boot 的 @ControllerAdvice — Web 层的统一拦截"></a>方案二：Spring Boot 的 @ControllerAdvice — Web 层的统一拦截</h2><p>如果你的项目是 Spring Boot 或者 Spring MVC，<code>@ControllerAdvice</code> 是处理 Web 层异常最优雅的方式。</p><h3 id="基本实现"><a href="#基本实现" class="headerlink" title="基本实现"></a>基本实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@RestControllerAdvice</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">GlobalWebExceptionHandler</span> &#123;<br><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Logger</span> <span class="hljs-variable">log</span> <span class="hljs-operator">=</span> LoggerFactory.getLogger(GlobalWebExceptionHandler.class);<br><br>    <span class="hljs-meta">@ExceptionHandler(IllegalArgumentException.class)</span><br>    <span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleIllegalArgument</span><span class="hljs-params">(IllegalArgumentException e)</span> &#123;<br>        log.warn(<span class="hljs-string">&quot;参数校验失败: &#123;&#125;&quot;</span>, e.getMessage());<br>        <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">400</span>, e.getMessage());<br>    &#125;<br><br>    <span class="hljs-meta">@ExceptionHandler(NullPointerException.class)</span><br>    <span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleNullPointer</span><span class="hljs-params">(NullPointerException e)</span> &#123;<br>        log.error(<span class="hljs-string">&quot;空指针异常: &quot;</span>, e);<br>        <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">500</span>, <span class="hljs-string">&quot;服务器内部错误&quot;</span>);<br>    &#125;<br><br>    <span class="hljs-meta">@ExceptionHandler(Exception.class)</span><br>    <span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleException</span><span class="hljs-params">(Exception e)</span> &#123;<br>        log.error(<span class="hljs-string">&quot;未知异常: &quot;</span>, e);<br>        <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">500</span>, <span class="hljs-string">&quot;服务器内部错误&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="配合自定义异常使用"><a href="#配合自定义异常使用" class="headerlink" title="配合自定义异常使用"></a>配合自定义异常使用</h3><p>光靠捕获内置异常不够灵活。实际项目里，大家都会搭配自定义业务异常：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">BusinessException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">RuntimeException</span> &#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> code;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String message;<br>    <br>    <span class="hljs-keyword">public</span> <span class="hljs-title function_">BusinessException</span><span class="hljs-params">(<span class="hljs-type">int</span> code, String message)</span> &#123;<br>        <span class="hljs-built_in">super</span>(message);<br>        <span class="hljs-built_in">this</span>.code = code;<br>    &#125;<br>    <br>    <span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">getCode</span><span class="hljs-params">()</span> &#123; <span class="hljs-keyword">return</span> code; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>然后在全局处理器里统一处理：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ExceptionHandler(BusinessException.class)</span><br><span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleBusiness</span><span class="hljs-params">(BusinessException e)</span> &#123;<br>    log.warn(<span class="hljs-string">&quot;业务异常: code=&#123;&#125;, msg=&#123;&#125;&quot;</span>, e.getCode(), e.getMessage());<br>    <span class="hljs-keyword">return</span> Result.error(e.getCode(), e.getMessage());<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="从异常里获取更多上下文"><a href="#从异常里获取更多上下文" class="headerlink" title="从异常里获取更多上下文"></a>从异常里获取更多上下文</h3><p>Spring 的 <code>@ExceptionHandler</code> 还能注入更多参数，拿到请求上下文：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ExceptionHandler(Exception.class)</span><br><span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleException</span><span class="hljs-params">(Exception e, HttpServletRequest request)</span> &#123;<br>    log.error(<span class="hljs-string">&quot;请求 &#123;&#125; &#123;&#125; 发生异常: &quot;</span>, request.getMethod(), request.getRequestURI(), e);<br>    <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">500</span>, <span class="hljs-string">&quot;服务器繁忙，请稍后重试&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="不同异常返回不同-HTTP-状态码"><a href="#不同异常返回不同-HTTP-状态码" class="headerlink" title="不同异常返回不同 HTTP 状态码"></a>不同异常返回不同 HTTP 状态码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ExceptionHandler(MissingServletRequestParameterException.class)</span><br><span class="hljs-meta">@ResponseStatus(HttpStatus.BAD_REQUEST)</span><br><span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleMissingParam</span><span class="hljs-params">(MissingServletRequestParameterException e)</span> &#123;<br>    <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">400</span>, <span class="hljs-string">&quot;缺少参数: &quot;</span> + e.getParameterName());<br>&#125;<br><br><span class="hljs-meta">@ExceptionHandler(HttpRequestMethodNotSupportedException.class)</span><br><span class="hljs-meta">@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)</span><br><span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleMethodNotSupported</span><span class="hljs-params">(HttpRequestMethodNotSupportedException e)</span> &#123;<br>    <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">405</span>, <span class="hljs-string">&quot;不支持的请求方法: &quot;</span> + e.getMethod());<br>&#125;<br><br><span class="hljs-meta">@ExceptionHandler(NoHandlerFoundException.class)</span><br><span class="hljs-meta">@ResponseStatus(HttpStatus.NOT_FOUND)</span><br><span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleNotFound</span><span class="hljs-params">(NoHandlerFoundException e)</span> &#123;<br>    <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">404</span>, <span class="hljs-string">&quot;接口不存在&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="处理顺序的坑"><a href="#处理顺序的坑" class="headerlink" title="处理顺序的坑"></a>处理顺序的坑</h3><p>多个 <code>@ExceptionHandler</code> 的匹配规则是<strong>找最匹配的</strong>，不是按声明顺序。比如抛了 <code>IllegalArgumentException</code>，会优先命中专门的 <code>handleIllegalArgument</code>，而不是走 <code>handleException</code>。</p><p>但要注意：如果你写了两个都能匹配到同一层级的处理器，顺序就不确定了。建议只保留一个宽泛的 <code>Exception.class</code> 兜底，其他的按具体异常类型写。</p><h2 id="方案三：Filter-ErrorPage-—-Servlet-容器的保底"><a href="#方案三：Filter-ErrorPage-—-Servlet-容器的保底" class="headerlink" title="方案三：Filter + ErrorPage — Servlet 容器的保底"></a>方案三：Filter + ErrorPage — Servlet 容器的保底</h2><p>有些异常连 <code>@ControllerAdvice</code> 都抓不到——比如在 Filter 里抛的、在 Spring 的 DispatcherServlet 之前就炸了的。这时候就得靠 Servlet 容器级别的处理。</p><h3 id="自定义-Filter-捕获"><a href="#自定义-Filter-捕获" class="headerlink" title="自定义 Filter 捕获"></a>自定义 Filter 捕获</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-meta">@Order(Ordered.HIGHEST_PRECEDENCE)</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">ExceptionCaptureFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">Filter</span> &#123;<br><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Logger</span> <span class="hljs-variable">log</span> <span class="hljs-operator">=</span> LoggerFactory.getLogger(ExceptionCaptureFilter.class);<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span><br>            <span class="hljs-keyword">throws</span> IOException, ServletException &#123;<br>        <span class="hljs-keyword">try</span> &#123;<br>            chain.doFilter(request, response);<br>        &#125; <span class="hljs-keyword">catch</span> (Exception e) &#123;<br>            log.error(<span class="hljs-string">&quot;Filter 层捕获异常: &quot;</span>, e);<br>            <span class="hljs-type">HttpServletResponse</span> <span class="hljs-variable">resp</span> <span class="hljs-operator">=</span> (HttpServletResponse) response;<br>            resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());<br>            resp.setContentType(<span class="hljs-string">&quot;application/json;charset=UTF-8&quot;</span>);<br>            resp.getWriter().write(<span class="hljs-string">&quot;&#123;\&quot;code\&quot;:500,\&quot;message\&quot;:\&quot;服务器内部错误\&quot;&#125;&quot;</span>);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="错误页面兜底"><a href="#错误页面兜底" class="headerlink" title="错误页面兜底"></a>错误页面兜底</h3><p>Spring Boot 在 <code>application.yml</code> 里可以配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">server:</span><br>  <span class="hljs-attr">error:</span><br>    <span class="hljs-attr">path:</span> <span class="hljs-string">/error</span><br>    <span class="hljs-attr">whitelabel:</span><br>      <span class="hljs-attr">enabled:</span> <span class="hljs-literal">false</span><br></code></pre></td></tr></table></figure><p>然后自己写一个 <code>/error</code> 的 Controller：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@RestController</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">CustomErrorController</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">ErrorController</span> &#123;<br><br>    <span class="hljs-meta">@RequestMapping(&quot;/error&quot;)</span><br>    <span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleError</span><span class="hljs-params">(HttpServletRequest request)</span> &#123;<br>        <span class="hljs-type">Integer</span> <span class="hljs-variable">statusCode</span> <span class="hljs-operator">=</span> (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);<br>        <span class="hljs-type">String</span> <span class="hljs-variable">message</span> <span class="hljs-operator">=</span> (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);<br>        <span class="hljs-keyword">return</span> Result.error(statusCode != <span class="hljs-literal">null</span> ? statusCode : <span class="hljs-number">500</span>, message != <span class="hljs-literal">null</span> ? message : <span class="hljs-string">&quot;未知错误&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这样连 404、405 这些 Spring 拦截不到的请求也能统一响应 JSON 了，而不是返回一堆丑陋的 HTML 错误页。</p><h2 id="方案四：异步线程的异常捕获"><a href="#方案四：异步线程的异常捕获" class="headerlink" title="方案四：异步线程的异常捕获"></a>方案四：异步线程的异常捕获</h2><p>开发中最容易被忽略的，就是线程池里跑飞了的异常。</p><h3 id="线程池场景"><a href="#线程池场景" class="headerlink" title="线程池场景"></a>线程池场景</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-type">ExecutorService</span> <span class="hljs-variable">executor</span> <span class="hljs-operator">=</span> Executors.newFixedThreadPool(<span class="hljs-number">4</span>);<br>executor.submit(() -&gt; &#123;<br>    <span class="hljs-comment">// 如果这里抛异常，submit 能吞掉！</span><br>    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">RuntimeException</span>(<span class="hljs-string">&quot;任务执行失败&quot;</span>);<br>&#125;);<br></code></pre></td></tr></table></figure><p><code>submit()</code> 的返回值是 <code>Future</code>，异常被封装在 <code>Future.get()</code> 里。如果不调 <code>get()</code>，异常就被静默吞掉了。这是 Java 很多线上事故的根源。</p><h3 id="正确做法"><a href="#正确做法" class="headerlink" title="正确做法"></a>正确做法</h3><p><strong>方案 A：确保调 get()</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java">Future&lt;?&gt; future = executor.submit(task);<br><span class="hljs-keyword">try</span> &#123;<br>    future.get();<br>&#125; <span class="hljs-keyword">catch</span> (ExecutionException e) &#123;<br>    log.error(<span class="hljs-string">&quot;异步任务执行异常&quot;</span>, e.getCause());<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>方案 B：使用 execute 代替 submit</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java">executor.execute(() -&gt; &#123;<br>    <span class="hljs-keyword">try</span> &#123;<br>        <span class="hljs-comment">// 业务逻辑</span><br>    &#125; <span class="hljs-keyword">catch</span> (Exception e) &#123;<br>        log.error(<span class="hljs-string">&quot;任务执行失败&quot;</span>, e);<br>        <span class="hljs-keyword">throw</span> e;<br>    &#125;<br>&#125;);<br></code></pre></td></tr></table></figure><p><code>execute()</code> 的异常会直接抛到线程的 UncaughtExceptionHandler 里（如果设置了的话）。</p><p><strong>方案 C：装饰线程池</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">ExceptionAwareThreadPoolExecutor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">ThreadPoolExecutor</span> &#123;<br>    <br>    <span class="hljs-keyword">public</span> <span class="hljs-title function_">ExceptionAwareThreadPoolExecutor</span><span class="hljs-params">(<span class="hljs-type">int</span> core, <span class="hljs-type">int</span> max, <span class="hljs-type">long</span> keepAlive, TimeUnit unit,</span><br><span class="hljs-params">                                             BlockingQueue&lt;Runnable&gt; queue)</span> &#123;<br>        <span class="hljs-built_in">super</span>(core, max, keepAlive, unit, queue);<br>    &#125;<br>    <br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">afterExecute</span><span class="hljs-params">(Runnable r, Throwable t)</span> &#123;<br>        <span class="hljs-built_in">super</span>.afterExecute(r, t);<br>        <span class="hljs-keyword">if</span> (t == <span class="hljs-literal">null</span> &amp;&amp; r <span class="hljs-keyword">instanceof</span> Future&lt;?&gt;) &#123;<br>            <span class="hljs-keyword">try</span> &#123;<br>                ((Future&lt;?&gt;) r).get();<br>            &#125; <span class="hljs-keyword">catch</span> (CancellationException e) &#123;<br>                <span class="hljs-comment">// 任务被取消，忽略</span><br>            &#125; <span class="hljs-keyword">catch</span> (ExecutionException e) &#123;<br>                t = e.getCause();<br>            &#125; <span class="hljs-keyword">catch</span> (InterruptedException e) &#123;<br>                Thread.currentThread().interrupt();<br>            &#125;<br>        &#125;<br>        <span class="hljs-keyword">if</span> (t != <span class="hljs-literal">null</span>) &#123;<br>            log.error(<span class="hljs-string">&quot;线程池任务异常&quot;</span>, t);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><code>afterExecute</code> 是 <code>ThreadPoolExecutor</code> 提供的钩子，能捕获任务执行后的异常——不管是用 <code>submit</code> 还是 <code>execute</code>。</p><h3 id="Spring-Async-的异常处理"><a href="#Spring-Async-的异常处理" class="headerlink" title="Spring @Async 的异常处理"></a>Spring @Async 的异常处理</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">AsyncConfig</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">AsyncConfigurer</span> &#123;<br>    <br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-keyword">public</span> Executor <span class="hljs-title function_">getAsyncExecutor</span><span class="hljs-params">()</span> &#123;<br>        <span class="hljs-type">ThreadPoolTaskExecutor</span> <span class="hljs-variable">executor</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ThreadPoolTaskExecutor</span>();<br>        executor.setCorePoolSize(<span class="hljs-number">5</span>);<br>        executor.setMaxPoolSize(<span class="hljs-number">10</span>);<br>        executor.setQueueCapacity(<span class="hljs-number">100</span>);<br>        executor.setThreadNamePrefix(<span class="hljs-string">&quot;async-&quot;</span>);<br>        executor.initialize();<br>        <span class="hljs-keyword">return</span> executor;<br>    &#125;<br>    <br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-keyword">public</span> AsyncUncaughtExceptionHandler <span class="hljs-title function_">getAsyncUncaughtExceptionHandler</span><span class="hljs-params">()</span> &#123;<br>        <span class="hljs-keyword">return</span> (throwable, method, params) -&gt; <br>            log.error(<span class="hljs-string">&quot;异步方法 &#123;&#125; 执行异常，参数: &#123;&#125;&quot;</span>, method.getName(), Arrays.toString(params), throwable);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="方案五：日志记录的完善建议"><a href="#方案五：日志记录的完善建议" class="headerlink" title="方案五：日志记录的完善建议"></a>方案五：日志记录的完善建议</h2><p>全局捕获只是第一步，更关键的是<strong>把异常信息完整记录下来</strong>。</p><h3 id="一个合格的异常日志应该包含"><a href="#一个合格的异常日志应该包含" class="headerlink" title="一个合格的异常日志应该包含"></a>一个合格的异常日志应该包含</h3><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-selector-attr">[2026-06-01 10:23:45.678]</span> <span class="hljs-selector-attr">[http-nio-8080-exec-3]</span> ERROR c<span class="hljs-selector-class">.y</span><span class="hljs-selector-class">.p</span><span class="hljs-selector-class">.GlobalWebExceptionHandler</span> - <br>请求: GET /api/user/<span class="hljs-number">123</span> <br>参数: &#123;&#125; <br>用户: userId=<span class="hljs-number">456</span> <br>异常: java<span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.NullPointerException</span>: Cannot invoke <span class="hljs-string">&quot;String.length()&quot;</span> because <span class="hljs-string">&quot;name&quot;</span> is null<br>    at com<span class="hljs-selector-class">.yourproject</span><span class="hljs-selector-class">.service</span><span class="hljs-selector-class">.UserService</span><span class="hljs-selector-class">.getUser</span>(UserService<span class="hljs-selector-class">.java</span>:<span class="hljs-number">45</span>)<br>    at com<span class="hljs-selector-class">.yourproject</span><span class="hljs-selector-class">.controller</span><span class="hljs-selector-class">.UserController</span><span class="hljs-selector-class">.getUser</span>(UserController<span class="hljs-selector-class">.java</span>:<span class="hljs-number">23</span>)<br>    ...<br></code></pre></td></tr></table></figure><p>能做到这个级别，线上排查效率会提升很多。</p><h3 id="推荐实践"><a href="#推荐实践" class="headerlink" title="推荐实践"></a>推荐实践</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ExceptionHandler(Exception.class)</span><br><span class="hljs-keyword">public</span> Result&lt;Void&gt; <span class="hljs-title function_">handleException</span><span class="hljs-params">(Exception e, HttpServletRequest request)</span> &#123;<br>    MDC.put(<span class="hljs-string">&quot;requestId&quot;</span>, request.getAttribute(<span class="hljs-string">&quot;requestId&quot;</span>).toString());<br>    log.error(<span class="hljs-string">&quot;请求 [&#123;&#125; &#123;&#125;] 异常: &quot;</span>, request.getMethod(), <br>        request.getRequestURI(), e);<br>    <span class="hljs-keyword">return</span> Result.error(<span class="hljs-number">500</span>, <span class="hljs-string">&quot;系统繁忙&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><p>配合 MDC（Mapped Diagnostic Context）可以在日志里自动带上 traceId，把所有相关日志串起来。</p><h2 id="完整的最佳实践方案"><a href="#完整的最佳实践方案" class="headerlink" title="完整的最佳实践方案"></a>完整的最佳实践方案</h2><p>把上面这些串起来，一个相对完善的项目应该这样做：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs markdown"><span class="hljs-bullet">1.</span> 启动时注册 global UncaughtExceptionHandler<br>   → 兜住所有漏掉的线程级异常<br><br><span class="hljs-bullet">2.</span> @RestControllerAdvice 处理 Controller 层异常<br>   → 统一响应格式，返回 JSON 而不是错误页<br><br><span class="hljs-bullet">3.</span> Filter 层包装异常捕获<br>   → 兜住进入 Spring 之前的异常<br><br><span class="hljs-bullet">4.</span> 自定义 ErrorController 处理 404/405 等路径异常<br>   → 消灭白标页<br><br><span class="hljs-bullet">5.</span> 线程池重写 afterExecute<br>   → 异步任务异常不再静默吞掉<br><br><span class="hljs-bullet">6.</span> 统一日志格式 + MDC traceId<br>   → 异常可追溯，可复现<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>全局异常捕获这个事，说起来不难，但真正做好需要覆盖各个层面：</p><ul><li><strong>JVM 层面</strong>：UncaughtExceptionHandler，兜住漏网之鱼</li><li><strong>Web 层面</strong>：@ControllerAdvice + Filter + ErrorController，三层拦截</li><li><strong>异步层面</strong>：线程池 afterExecute + AsyncConfigurer 自定义处理器</li><li><strong>日志层面</strong>：统一格式 + traceId，让异常可追溯</li></ul><p>把这些配好了，线上再出问题，你至少知道从哪查起。别让异常在暗处爆炸——给它一个明确的出口。</p><p>建议现在就去检查一下你的项目，看看有几个口子还漏着。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/java-global-exception-handling/</id>
    <link href="https://blog.280303.xyz/posts/java-global-exception-handling/"/>
    <published>2026-06-01T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="为什么会遇到这个问题"><a href="#为什么会遇到这个问题" class="headerlink" title="为什么会遇到这个问题"></a>为什么会遇到这个问题</h2><p>写 Java 最烦的事之一：线上跑得好好的，突然冒出个异常，日志一翻——空的]]>
    </summary>
    <title>Java 全局异常捕获实战：别再让崩溃裸奔了</title>
    <updated>2026-06-02T07:49:27.291Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="技术实践" scheme="https://blog.280303.xyz/categories/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/"/>
    <category term="WinSW" scheme="https://blog.280303.xyz/tags/WinSW/"/>
    <category term="Windows" scheme="https://blog.280303.xyz/tags/Windows/"/>
    <category term="Java" scheme="https://blog.280303.xyz/tags/Java/"/>
    <category term="Spring Boot" scheme="https://blog.280303.xyz/tags/Spring-Boot/"/>
    <category term="服务部署" scheme="https://blog.280303.xyz/tags/%E6%9C%8D%E5%8A%A1%E9%83%A8%E7%BD%B2/"/>
    <category term="运维" scheme="https://blog.280303.xyz/tags/%E8%BF%90%E7%BB%B4/"/>
    <content>
      <![CDATA[<p>在 Windows 服务器上部署 Java 应用，很多人的做法是写一个 <code>.bat</code> 脚本，然后让运维手动双击运行。这种方式有几个明显的问题：</p><ul><li>服务器重启后需要手动再次启动</li><li>没有守护进程，程序崩溃后不会自动恢复</li><li>日志管理混乱，输出全靠 <code>System.out.println</code></li><li>需要保持一个命令行窗口常驻</li></ul><p><strong>WinSW</strong>（Windows Service Wrapper）解决的就是这些问题。它让任何可执行程序都能注册成 Windows 服务，自带开机自启、崩溃重启、日志滚动等能力，而且完全开源免费。</p><p><img src="/images/winsw-jar-windows-service/winsw-jar-windows-service-cover.png" alt="WinSW 将 JAR 包包装成 Windows 服务，支持开机自启、异常重启和日志滚动"></p><blockquote><p>WinSW 的核心思路是：由 Windows 服务管理器托管 <code>winsw.exe</code>，再由 WinSW 启动并守护你的 <code>javaw -jar app.jar</code> 应用。</p></blockquote><hr><h2 id="技术背景"><a href="#技术背景" class="headerlink" title="技术背景"></a>技术背景</h2><h3 id="Windows-服务是什么"><a href="#Windows-服务是什么" class="headerlink" title="Windows 服务是什么"></a>Windows 服务是什么</h3><p>Windows 服务（Windows Service）是一种在后台长期运行的进程，由系统服务管理器（SCM）统一管理。它的特点：</p><ul><li><strong>开机自动启动</strong>，无需用户登录</li><li><strong>独立于用户会话</strong>，不依赖任何登录的用户</li><li><strong>可配置故障恢复</strong>，崩溃后自动重启</li><li><strong>统一管理入口</strong>：任务管理器、<code>services.msc</code>、<code>sc</code> 命令都可以控制</li></ul><h3 id="WinSW-做了什么"><a href="#WinSW-做了什么" class="headerlink" title="WinSW 做了什么"></a>WinSW 做了什么</h3><p>WinSW 本质上是一个”包装器”。它把你的程序（比如 <code>java -jar app.jar</code>）包装成一个 Windows 服务进程，让 SCM 可以像管理系统服务一样管理你的应用。</p><p>整个结构很简单：</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mipsasm">WinSW.exe（服务宿主进程）<br>  └─ 启动并管理 ─→ <span class="hljs-keyword">java </span>-<span class="hljs-keyword">jar </span>your-app.<span class="hljs-keyword">jar</span><br></code></pre></td></tr></table></figure><p>WinSW 负责与 Windows SCM 通信，你的 JAR 负责跑业务。</p><h3 id="java-和-javaw-的区别"><a href="#java-和-javaw-的区别" class="headerlink" title="java 和 javaw 的区别"></a><code>java</code> 和 <code>javaw</code> 的区别</h3><p>启动 JAR 时可以选择 <code>java</code> 或 <code>javaw</code>，两者的核心区别是：</p><table><thead><tr><th></th><th><code>java</code></th><th><code>javaw</code></th></tr></thead><tbody><tr><td>控制台窗口</td><td><strong>会弹出</strong>黑色命令行窗口</td><td><strong>不弹出</strong>窗口，静默运行</td></tr><tr><td>标准输出</td><td>输出到控制台</td><td>无控制台，输出由 WinSW 捕获写入日志</td></tr><tr><td>适用场景</td><td>调试、命令行工具</td><td><strong>Windows 服务、GUI 程序（推荐）</strong></td></tr></tbody></table><p>在 WinSW 管理的 Windows 服务里，程序运行在系统后台，根本不需要控制台窗口。用 <code>javaw</code> 既干净又符合服务程序的运行方式，所有输出都由 WinSW 接管并写入 <code>.out.log</code> &#x2F; <code>.err.log</code> 文件。</p><blockquote><p><code>javaw</code> 在 JDK 的 <code>bin</code> 目录下，与 <code>java.exe</code> 并列，路径相同，只是换个文件名。</p></blockquote><hr><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><h3 id="环境要求"><a href="#环境要求" class="headerlink" title="环境要求"></a>环境要求</h3><ul><li>Windows Server 2012 R2 及以上，或 Windows 10&#x2F;11</li><li>.NET Framework 4.6.1 及以上（大多数 Windows 已内置）</li><li>JDK 已安装并配置好 <code>JAVA_HOME</code>（或直接用完整路径）</li></ul><h3 id="下载-WinSW"><a href="#下载-WinSW" class="headerlink" title="下载 WinSW"></a>下载 WinSW</h3><p>WinSW 发布在 GitHub：<a href="https://github.com/winsw/winsw/releases">https://github.com/winsw/winsw/releases</a></p><p>下载页面有多个版本，按需选择：</p><table><thead><tr><th>文件名</th><th>适用场景</th></tr></thead><tbody><tr><td><code>WinSW-x64.exe</code></td><td>64 位系统（主流选择）</td></tr><tr><td><code>WinSW-x86.exe</code></td><td>32 位系统</td></tr><tr><td><code>WinSW-arm64.exe</code></td><td>ARM 架构系统</td></tr></tbody></table><p>下载后得到一个单独的 <code>.exe</code> 文件，无需安装，拷贝到目标目录即可使用。</p><hr><h2 id="目录结构规划"><a href="#目录结构规划" class="headerlink" title="目录结构规划"></a>目录结构规划</h2><p>建议把每个服务独立放在一个目录下，结构清晰，便于维护：</p><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs crmsh">D:\services\<br>└─ my-app\<br>   ├─ my-app.exe         ← WinSW 可执行文件（改名与 <span class="hljs-keyword">XML</span> <span class="hljs-title">同名）</span><br><span class="hljs-title">   ├─ my-app</span>.<span class="hljs-keyword">xml</span>         <span class="hljs-title">← WinSW</span> 配置文件（与 exe 同名）<br>   ├─ my-app.jar         ← 你的 Spring Boot JAR<br>   └─ logs\              ← 日志目录（WinSW 自动创建）<br></code></pre></td></tr></table></figure><blockquote><p><strong>命名规则很重要</strong>：WinSW 的 <code>.exe</code> 和 <code>.xml</code> 文件必须同名（不含扩展名），WinSW 会自动查找同目录下同名的 XML 配置文件。</p></blockquote><p>实际操作：把下载的 <code>WinSW-x64.exe</code> 重命名为 <code>my-app.exe</code>，然后在同目录下创建 <code>my-app.xml</code>。</p><hr><h2 id="编写配置文件"><a href="#编写配置文件" class="headerlink" title="编写配置文件"></a>编写配置文件</h2><p><code>my-app.xml</code> 是整个部署的核心，它告诉 WinSW 如何启动你的程序。</p><h3 id="基础配置（最小可运行版本）"><a href="#基础配置（最小可运行版本）" class="headerlink" title="基础配置（最小可运行版本）"></a>基础配置（最小可运行版本）</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-meta">&lt;?xml version=<span class="hljs-string">&quot;1.0&quot;</span> encoding=<span class="hljs-string">&quot;UTF-8&quot;</span>?&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">service</span>&gt;</span><br>  <span class="hljs-comment">&lt;!-- 服务 ID，必须唯一，不能包含空格 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>my-app<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 服务在 services.msc 中显示的名称 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>My App Service<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 服务描述，可选 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>My Spring Boot Application<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 可执行文件：javaw 不弹出控制台窗口，适合 Windows 服务场景 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">executable</span>&gt;</span>javaw<span class="hljs-tag">&lt;/<span class="hljs-name">executable</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 启动参数：完整的 JVM 启动命令（不含 javaw 本身） --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">arguments</span>&gt;</span>-jar &quot;D:\services\my-app\my-app.jar&quot;<span class="hljs-tag">&lt;/<span class="hljs-name">arguments</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 服务启动类型：Automatic（自动）/ Manual（手动）/ Disabled（禁用） --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">startmode</span>&gt;</span>Automatic<span class="hljs-tag">&lt;/<span class="hljs-name">startmode</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">service</span>&gt;</span><br></code></pre></td></tr></table></figure><p>这个配置已经可以运行。下面是生产环境推荐的完整配置：</p><h3 id="完整配置（生产推荐）"><a href="#完整配置（生产推荐）" class="headerlink" title="完整配置（生产推荐）"></a>完整配置（生产推荐）</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-meta">&lt;?xml version=<span class="hljs-string">&quot;1.0&quot;</span> encoding=<span class="hljs-string">&quot;UTF-8&quot;</span>?&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">service</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 基础信息 ==================== --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>my-app<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>My App Service<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>My Spring Boot Application - Production<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 启动命令 ==================== --&gt;</span><br>  <span class="hljs-comment">&lt;!-- 使用 javaw 而非 java，避免弹出控制台窗口 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">executable</span>&gt;</span>javaw<span class="hljs-tag">&lt;/<span class="hljs-name">executable</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">arguments</span>&gt;</span><br>    -server<br>    -Xms512m<br>    -Xmx1024m<br>    -XX:+UseG1GC<br>    -Dfile.encoding=UTF-8<br>    -Dspring.profiles.active=prod<br>    -jar &quot;D:\services\my-app\my-app.jar&quot;<br>    --server.port=8080<br>  <span class="hljs-tag">&lt;/<span class="hljs-name">arguments</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 工作目录：JAR 运行时的当前目录，影响相对路径解析 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">workingdirectory</span>&gt;</span>D:\services\my-app<span class="hljs-tag">&lt;/<span class="hljs-name">workingdirectory</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 环境变量 ==================== --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">env</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;JAVA_HOME&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;C:\Program Files\Java\jdk-17&quot;</span>/&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">env</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;APP_ENV&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;production&quot;</span>/&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 启动类型 ==================== --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">startmode</span>&gt;</span>Automatic<span class="hljs-tag">&lt;/<span class="hljs-name">startmode</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- 延迟启动：系统启动后等一会再启动服务，避免依赖服务未就绪 --&gt;</span><br>  <span class="hljs-comment">&lt;!-- &lt;delayedAutoStart&gt;true&lt;/delayedAutoStart&gt; --&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 故障恢复 ==================== --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">onfailure</span> <span class="hljs-attr">action</span>=<span class="hljs-string">&quot;restart&quot;</span> <span class="hljs-attr">delay</span>=<span class="hljs-string">&quot;10 sec&quot;</span>/&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">onfailure</span> <span class="hljs-attr">action</span>=<span class="hljs-string">&quot;restart&quot;</span> <span class="hljs-attr">delay</span>=<span class="hljs-string">&quot;20 sec&quot;</span>/&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">onfailure</span> <span class="hljs-attr">action</span>=<span class="hljs-string">&quot;restart&quot;</span> <span class="hljs-attr">delay</span>=<span class="hljs-string">&quot;30 sec&quot;</span>/&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">resetfailure</span>&gt;</span>1 hour<span class="hljs-tag">&lt;/<span class="hljs-name">resetfailure</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 日志管理 ==================== --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">logpath</span>&gt;</span>D:\services\my-app\logs<span class="hljs-tag">&lt;/<span class="hljs-name">logpath</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">log</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">&quot;roll-by-size-time&quot;</span>&gt;</span><br>    <span class="hljs-comment">&lt;!-- 日志文件大小上限，超过后滚动 --&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">sizeThreshold</span>&gt;</span>10240<span class="hljs-tag">&lt;/<span class="hljs-name">sizeThreshold</span>&gt;</span><br>    <span class="hljs-comment">&lt;!-- 时间滚动模式：每天一个新文件 --&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">pattern</span>&gt;</span>yyyyMMdd<span class="hljs-tag">&lt;/<span class="hljs-name">pattern</span>&gt;</span><br>    <span class="hljs-comment">&lt;!-- 保留最近 30 天的日志 --&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">autoRollAtTime</span>&gt;</span>00:00:00<span class="hljs-tag">&lt;/<span class="hljs-name">autoRollAtTime</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">zipOlderThanNumDays</span>&gt;</span>3<span class="hljs-tag">&lt;/<span class="hljs-name">zipOlderThanNumDays</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">enablePIDFile</span>&gt;</span>false<span class="hljs-tag">&lt;/<span class="hljs-name">enablePIDFile</span>&gt;</span><br>  <span class="hljs-tag">&lt;/<span class="hljs-name">log</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 停止超时 ==================== --&gt;</span><br>  <span class="hljs-comment">&lt;!-- 发送停止信号后等待多久强制终止 --&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">stoptimeout</span>&gt;</span>30 sec<span class="hljs-tag">&lt;/<span class="hljs-name">stoptimeout</span>&gt;</span><br><br>  <span class="hljs-comment">&lt;!-- ==================== 依赖服务 ==================== --&gt;</span><br>  <span class="hljs-comment">&lt;!-- 如果你的应用依赖数据库服务，可以在这里声明 --&gt;</span><br>  <span class="hljs-comment">&lt;!-- &lt;depend&gt;MySQL&lt;/depend&gt; --&gt;</span><br><br><span class="hljs-tag">&lt;/<span class="hljs-name">service</span>&gt;</span><br></code></pre></td></tr></table></figure><h3 id="配置项详解"><a href="#配置项详解" class="headerlink" title="配置项详解"></a>配置项详解</h3><p><strong><code>&lt;executable&gt;</code> 和 <code>&lt;arguments&gt;</code></strong></p><p><code>executable</code> 填可执行程序的路径（或环境变量中存在的命令名，如 <code>java</code>）。如果系统 <code>PATH</code> 里没有 <code>java</code>，需要写完整路径：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">executable</span>&gt;</span>C:\Program Files\Java\jdk-17\bin\java.exe<span class="hljs-tag">&lt;/<span class="hljs-name">executable</span>&gt;</span><br></code></pre></td></tr></table></figure><p><code>arguments</code> 里的参数换行写更清晰，WinSW 会自动拼接。</p><p><strong><code>&lt;onfailure&gt;</code> 故障恢复</strong></p><p>三条 <code>onfailure</code> 对应三次故障后的动作，按顺序触发：第一次崩溃 10 秒后重启，第二次 20 秒后重启，第三次 30 秒后重启。<code>resetfailure</code> 表示 1 小时内没有崩溃则重置计数器。</p><p><strong><code>&lt;log mode&gt;</code></strong> 支持四种模式：</p><table><thead><tr><th>模式</th><th>说明</th></tr></thead><tbody><tr><td><code>append</code></td><td>追加到同一文件（默认）</td></tr><tr><td><code>reset</code></td><td>每次启动时清空日志</td></tr><tr><td><code>roll</code></td><td>按大小滚动</td></tr><tr><td><code>roll-by-size-time</code></td><td>按大小和时间双重滚动（推荐生产使用）</td></tr></tbody></table><hr><h2 id="注册和管理服务"><a href="#注册和管理服务" class="headerlink" title="注册和管理服务"></a>注册和管理服务</h2><p>所有操作都需要以<strong>管理员权限</strong>打开命令提示符（CMD）或 PowerShell。</p><h3 id="注册服务"><a href="#注册服务" class="headerlink" title="注册服务"></a>注册服务</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cmd"><span class="hljs-built_in">cd</span> D:\services\my-app<br>my-app.exe install<br></code></pre></td></tr></table></figure><p>成功后会看到：</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">2026</span>-<span class="hljs-number">06</span>-<span class="hljs-number">01</span> <span class="hljs-number">09</span>:<span class="hljs-number">00</span>:<span class="hljs-number">00</span>,<span class="hljs-number">000</span> INFO  - Installing service &#x27;My App Service (my-app)&#x27;...<br><span class="hljs-attribute">2026</span>-<span class="hljs-number">06</span>-<span class="hljs-number">01</span> <span class="hljs-number">09</span>:<span class="hljs-number">00</span>:<span class="hljs-number">00</span>,<span class="hljs-number">100</span> INFO  - Service &#x27;My App Service (my-app)&#x27; was installed successfully.<br></code></pre></td></tr></table></figure><h3 id="启动服务"><a href="#启动服务" class="headerlink" title="启动服务"></a>启动服务</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">my-app.exe <span class="hljs-built_in">start</span><br></code></pre></td></tr></table></figure><p>或者用系统命令：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cmd"><span class="hljs-built_in">net</span> <span class="hljs-built_in">start</span> my-app<br>sc <span class="hljs-built_in">start</span> my-app<br></code></pre></td></tr></table></figure><h3 id="停止服务"><a href="#停止服务" class="headerlink" title="停止服务"></a>停止服务</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">my-app.exe stop<br></code></pre></td></tr></table></figure><p>或者：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cmd"><span class="hljs-built_in">net</span> stop my-app<br>sc stop my-app<br></code></pre></td></tr></table></figure><h3 id="重启服务"><a href="#重启服务" class="headerlink" title="重启服务"></a>重启服务</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">my-app.exe restart<br></code></pre></td></tr></table></figure><h3 id="查看服务状态"><a href="#查看服务状态" class="headerlink" title="查看服务状态"></a>查看服务状态</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">my-app.exe status<br></code></pre></td></tr></table></figure><p>输出示例：</p><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs crmsh"><span class="hljs-literal">Started</span><br></code></pre></td></tr></table></figure><p>或者通过 SC 查看详情：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">sc query my-app<br></code></pre></td></tr></table></figure><h3 id="卸载服务"><a href="#卸载服务" class="headerlink" title="卸载服务"></a>卸载服务</h3><p>先停止服务，再卸载：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cmd">my-app.exe stop<br>my-app.exe uninstall<br></code></pre></td></tr></table></figure><h3 id="图形界面管理"><a href="#图形界面管理" class="headerlink" title="图形界面管理"></a>图形界面管理</h3><p>按 <code>Win + R</code> 输入 <code>services.msc</code>，打开服务管理器，找到 “My App Service”，可以直接右键启动&#x2F;停止&#x2F;重启，也可以修改启动类型。</p><hr><h2 id="日志说明"><a href="#日志说明" class="headerlink" title="日志说明"></a>日志说明</h2><p>WinSW 运行后会在 <code>&lt;logpath&gt;</code> 目录下生成三类日志文件：</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs pgsql">logs\<br>├─ my-app.<span class="hljs-keyword">out</span>.<span class="hljs-keyword">log</span>    ← 程序标准输出（<span class="hljs-keyword">System</span>.<span class="hljs-keyword">out</span>.println）<br>├─ my-app.err.<span class="hljs-keyword">log</span>    ← 程序错误输出（<span class="hljs-keyword">System</span>.err / 异常堆栈）<br>└─ my-app.<span class="hljs-keyword">wrapper</span>.<span class="hljs-keyword">log</span> ← WinSW 自身的运行日志（启动/停止记录）<br></code></pre></td></tr></table></figure><p>排查问题时，先看 <code>wrapper.log</code> 确认服务是否正常启动，再看 <code>err.log</code> 查应用异常。</p><p>Spring Boot 应用通常自带日志框架（Logback&#x2F;Log4j2），建议在 <code>application.yml</code> 里也配置日志文件路径，两套日志各司其职：</p><ul><li><strong>WinSW 日志</strong>：记录服务生命周期事件（启动&#x2F;停止&#x2F;崩溃&#x2F;重启）</li><li><strong>应用日志</strong>：记录业务逻辑和异常</li></ul><hr><h2 id="实战示例：部署-Spring-Boot-应用"><a href="#实战示例：部署-Spring-Boot-应用" class="headerlink" title="实战示例：部署 Spring Boot 应用"></a>实战示例：部署 Spring Boot 应用</h2><p>假设有一个打包好的 <code>user-service-1.0.0.jar</code>，需要部署到 <code>D:\services\user-service\</code>。</p><h3 id="第一步：创建目录并放置文件"><a href="#第一步：创建目录并放置文件" class="headerlink" title="第一步：创建目录并放置文件"></a>第一步：创建目录并放置文件</h3><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs pgsql">D:\services\<span class="hljs-keyword">user</span>-service\<br>├─ <span class="hljs-keyword">user</span>-service.exe    ← 重命名后的 WinSW<br>├─ <span class="hljs-keyword">user</span>-service.xml    ← 下面要写的配置文件<br>└─ <span class="hljs-keyword">user</span>-service<span class="hljs-number">-1.0</span><span class="hljs-number">.0</span>.jar<br></code></pre></td></tr></table></figure><h3 id="第二步：编写配置文件"><a href="#第二步：编写配置文件" class="headerlink" title="第二步：编写配置文件"></a>第二步：编写配置文件</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-meta">&lt;?xml version=<span class="hljs-string">&quot;1.0&quot;</span> encoding=<span class="hljs-string">&quot;UTF-8&quot;</span>?&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">service</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>user-service<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>用户服务<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>用户管理微服务，端口 8081<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span><br><br>  <span class="hljs-tag">&lt;<span class="hljs-name">executable</span>&gt;</span>javaw<span class="hljs-tag">&lt;/<span class="hljs-name">executable</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">arguments</span>&gt;</span><br>    -Xms256m<br>    -Xmx512m<br>    -Dfile.encoding=UTF-8<br>    -Dspring.profiles.active=prod<br>    -jar &quot;D:\services\user-service\user-service-1.0.0.jar&quot;<br>    --server.port=8081<br>  <span class="hljs-tag">&lt;/<span class="hljs-name">arguments</span>&gt;</span><br><br>  <span class="hljs-tag">&lt;<span class="hljs-name">workingdirectory</span>&gt;</span>D:\services\user-service<span class="hljs-tag">&lt;/<span class="hljs-name">workingdirectory</span>&gt;</span><br><br>  <span class="hljs-tag">&lt;<span class="hljs-name">startmode</span>&gt;</span>Automatic<span class="hljs-tag">&lt;/<span class="hljs-name">startmode</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">delayedAutoStart</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">delayedAutoStart</span>&gt;</span><br><br>  <span class="hljs-tag">&lt;<span class="hljs-name">onfailure</span> <span class="hljs-attr">action</span>=<span class="hljs-string">&quot;restart&quot;</span> <span class="hljs-attr">delay</span>=<span class="hljs-string">&quot;10 sec&quot;</span>/&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">onfailure</span> <span class="hljs-attr">action</span>=<span class="hljs-string">&quot;restart&quot;</span> <span class="hljs-attr">delay</span>=<span class="hljs-string">&quot;30 sec&quot;</span>/&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">resetfailure</span>&gt;</span>1 hour<span class="hljs-tag">&lt;/<span class="hljs-name">resetfailure</span>&gt;</span><br><br>  <span class="hljs-tag">&lt;<span class="hljs-name">logpath</span>&gt;</span>D:\services\user-service\logs<span class="hljs-tag">&lt;/<span class="hljs-name">logpath</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">log</span> <span class="hljs-attr">mode</span>=<span class="hljs-string">&quot;roll-by-size-time&quot;</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">sizeThreshold</span>&gt;</span>10240<span class="hljs-tag">&lt;/<span class="hljs-name">sizeThreshold</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">pattern</span>&gt;</span>yyyyMMdd<span class="hljs-tag">&lt;/<span class="hljs-name">pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">autoRollAtTime</span>&gt;</span>00:00:00<span class="hljs-tag">&lt;/<span class="hljs-name">autoRollAtTime</span>&gt;</span><br>  <span class="hljs-tag">&lt;/<span class="hljs-name">log</span>&gt;</span><br><br>  <span class="hljs-tag">&lt;<span class="hljs-name">stoptimeout</span>&gt;</span>30 sec<span class="hljs-tag">&lt;/<span class="hljs-name">stoptimeout</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">service</span>&gt;</span><br></code></pre></td></tr></table></figure><h3 id="第三步：以管理员身份注册并启动"><a href="#第三步：以管理员身份注册并启动" class="headerlink" title="第三步：以管理员身份注册并启动"></a>第三步：以管理员身份注册并启动</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cmd"><span class="hljs-built_in">cd</span> D:\services\user-service<br>user-service.exe install<br>user-service.exe <span class="hljs-built_in">start</span><br></code></pre></td></tr></table></figure><h3 id="第四步：验证服务运行"><a href="#第四步：验证服务运行" class="headerlink" title="第四步：验证服务运行"></a>第四步：验证服务运行</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cmd"># 查看服务状态<br>user-service.exe status<br><br># 验证端口是否监听<br>netstat -ano | <span class="hljs-built_in">findstr</span> <span class="hljs-number">8081</span><br><br># 测试接口<br>curl http://localhost:<span class="hljs-number">8081</span>/actuator/health<br></code></pre></td></tr></table></figure><h3 id="升级版本（替换-JAR）"><a href="#升级版本（替换-JAR）" class="headerlink" title="升级版本（替换 JAR）"></a>升级版本（替换 JAR）</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cmd"># <span class="hljs-number">1</span>. 停止服务<br>user-service.exe stop<br><br># <span class="hljs-number">2</span>. 替换 JAR 文件<br><span class="hljs-built_in">copy</span> /Y user-service-<span class="hljs-number">1</span>.<span class="hljs-number">1</span>.<span class="hljs-number">0</span>.jar D:\services\user-service\user-service-<span class="hljs-number">1</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>.jar<br><br># <span class="hljs-number">3</span>. 启动服务<br>user-service.exe <span class="hljs-built_in">start</span><br></code></pre></td></tr></table></figure><blockquote><p>如果新版本 JAR 改了文件名，也需要同步更新 XML 里的 <code>&lt;arguments&gt;</code> 路径。</p></blockquote><hr><h2 id="常见问题排查"><a href="#常见问题排查" class="headerlink" title="常见问题排查"></a>常见问题排查</h2><h3 id="服务注册失败：拒绝访问"><a href="#服务注册失败：拒绝访问" class="headerlink" title="服务注册失败：拒绝访问"></a>服务注册失败：拒绝访问</h3><p><strong>现象：</strong> <code>install</code> 时提示 “Access is denied”</p><p><strong>原因：</strong> 没有以管理员身份运行</p><p><strong>解决：</strong> 右键命令提示符 → “以管理员身份运行”，再执行命令</p><hr><h3 id="服务启动后立即停止"><a href="#服务启动后立即停止" class="headerlink" title="服务启动后立即停止"></a>服务启动后立即停止</h3><p><strong>现象：</strong> <code>start</code> 后状态马上变回 Stopped</p><p><strong>排查步骤：</strong></p><ol><li>查看 <code>logs\my-app.wrapper.log</code>，找 <code>ERROR</code> 关键字</li><li>查看 <code>logs\my-app.err.log</code>，看 Java 异常信息</li><li>常见原因：<ul><li>JAR 路径写错（注意反斜杠和空格）</li><li>端口被占用：<code>netstat -ano | findstr 8080</code></li><li>JVM 参数不正确（内存设置超出系统可用内存）</li><li>依赖的配置文件路径不对</li></ul></li></ol><hr><h3 id="javaw-命令找不到"><a href="#javaw-命令找不到" class="headerlink" title="javaw 命令找不到"></a><code>javaw</code> 命令找不到</h3><p><strong>现象：</strong> <code>wrapper.log</code> 里提示找不到 <code>javaw</code></p><p><strong>解决：</strong> 在 XML 里用 Java 的完整路径：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">executable</span>&gt;</span>C:\Program Files\Java\jdk-17\bin\javaw.exe<span class="hljs-tag">&lt;/<span class="hljs-name">executable</span>&gt;</span><br></code></pre></td></tr></table></figure><p>或者在 XML 里补充 PATH 环境变量：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">env</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;PATH&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;%PATH%;C:\Program Files\Java\jdk-17\bin&quot;</span>/&gt;</span><br></code></pre></td></tr></table></figure><hr><h3 id="中文路径或中文日志乱码"><a href="#中文路径或中文日志乱码" class="headerlink" title="中文路径或中文日志乱码"></a>中文路径或中文日志乱码</h3><p><strong>现象：</strong> 日志文件里中文显示为乱码</p><p><strong>解决：</strong> 在 <code>&lt;arguments&gt;</code> 里加上编码参数：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs xml">-Dfile.encoding=UTF-8<br>-Dstdout.encoding=UTF-8<br>-Dstderr.encoding=UTF-8<br></code></pre></td></tr></table></figure><p>同时确保 XML 文件本身以 UTF-8 编码保存，第一行声明 <code>encoding=&quot;UTF-8&quot;</code>。</p><hr><h3 id="卸载时提示服务正在运行"><a href="#卸载时提示服务正在运行" class="headerlink" title="卸载时提示服务正在运行"></a>卸载时提示服务正在运行</h3><p>先停止再卸载：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cmd">my-app.exe stop<br># 等待几秒<br>my-app.exe uninstall<br></code></pre></td></tr></table></figure><p>如果还是卸载失败，用 SC 命令强制删除：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">sc delete my-app<br></code></pre></td></tr></table></figure><hr><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>WinSW 的核心就三个文件：一个 EXE、一个 XML、一个 JAR。配置逻辑也很清晰——XML 里告诉 WinSW 怎么启动你的程序，剩下的事情交给 Windows 服务管理器。</p><p>用一张表总结常用命令：</p><table><thead><tr><th>操作</th><th>WinSW 命令</th><th>系统命令</th></tr></thead><tbody><tr><td>注册服务</td><td><code>my-app.exe install</code></td><td>—</td></tr><tr><td>启动服务</td><td><code>my-app.exe start</code></td><td><code>net start my-app</code></td></tr><tr><td>停止服务</td><td><code>my-app.exe stop</code></td><td><code>net stop my-app</code></td></tr><tr><td>重启服务</td><td><code>my-app.exe restart</code></td><td>—</td></tr><tr><td>查看状态</td><td><code>my-app.exe status</code></td><td><code>sc query my-app</code></td></tr><tr><td>卸载服务</td><td><code>my-app.exe uninstall</code></td><td><code>sc delete my-app</code></td></tr></tbody></table><p>生产部署时，几个细节值得注意：</p><ul><li><strong>用完整路径</strong>：<code>executable</code> 和 <code>arguments</code> 里涉及路径的地方，尽量用绝对路径，避免工作目录带来的歧义</li><li><strong>配置故障恢复</strong>：<code>onfailure</code> 是生产环境必备，程序崩溃后自动重启是刚需</li><li><strong>管理员权限</strong>：注册和卸载服务必须在管理员权限下执行</li><li><strong>日志滚动</strong>：生产环境一定要配 <code>roll-by-size-time</code> 模式，防止日志把磁盘撑爆</li></ul>]]>
    </content>
    <id>https://blog.280303.xyz/posts/winsw-jar-windows-service/</id>
    <link href="https://blog.280303.xyz/posts/winsw-jar-windows-service/"/>
    <published>2026-06-01T01:30:00.000Z</published>
    <summary>在 Windows 服务器上跑 Spring Boot 应用，最省事的方案就是把它注册成系统服务。开机自启、崩溃重启、日志管理，全都有。这篇文章把 WinSW 的完整用法讲清楚。</summary>
    <title>用 WinSW 把 JAR 包注册成 Windows 服务</title>
    <updated>2026-06-01T01:22:22.290Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="技术实践" scheme="https://blog.280303.xyz/categories/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/"/>
    <category term="Git" scheme="https://blog.280303.xyz/tags/Git/"/>
    <category term="GitHub" scheme="https://blog.280303.xyz/tags/GitHub/"/>
    <category term="Gitee" scheme="https://blog.280303.xyz/tags/Gitee/"/>
    <category term="SSH" scheme="https://blog.280303.xyz/tags/SSH/"/>
    <category term="版本控制" scheme="https://blog.280303.xyz/tags/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6/"/>
    <category term="开发工具" scheme="https://blog.280303.xyz/tags/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<p>Git 是每个开发者绕不开的工具，但很多人只会 <code>add</code>、<code>commit</code>、<code>push</code> 这三板斧，遇到稍微复杂一点的需求就手足无措。</p><p>这篇文章从实际使用出发，覆盖四个高频场景：</p><ol><li><strong>基础配置</strong>——第一次用 Git 该怎么设置</li><li><strong>SSH 密钥</strong>——生成密钥并连接到 GitHub</li><li><strong>多远程仓库</strong>——同时推送到 GitHub 和 Gitee</li><li><strong>撤销提交</strong>——删除已提交到仓库的记录，但保留本地文件</li></ol><p><img src="/images/git-essential-guide/git-workflow-overview.png" alt="Git 工作流总览：配置、SSH、多仓库与历史清理"></p><blockquote><p>从基础配置到 SSH 认证、多仓库同步和历史清理，Git 的核心能力都围绕“安全、可追踪、可协作”展开。</p></blockquote><hr><h2 id="一、Git-基础配置"><a href="#一、Git-基础配置" class="headerlink" title="一、Git 基础配置"></a>一、Git 基础配置</h2><h3 id="为什么要先配置"><a href="#为什么要先配置" class="headerlink" title="为什么要先配置"></a>为什么要先配置</h3><p>Git 的每一次提交都会记录作者信息（姓名 + 邮箱）。如果不配置，提交历史里要么是空白，要么是系统默认的奇怪字符串。这些信息会永久写入提交记录，同步到远程仓库，也会显示在 GitHub 的贡献图上。</p><h3 id="全局配置（适用所有项目）"><a href="#全局配置（适用所有项目）" class="headerlink" title="全局配置（适用所有项目）"></a>全局配置（适用所有项目）</h3><p>打开终端，执行以下两条命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">git config --global user.name <span class="hljs-string">&quot;你的名字&quot;</span><br>git config --global user.email <span class="hljs-string">&quot;你的邮箱@example.com&quot;</span><br></code></pre></td></tr></table></figure><p><code>--global</code> 表示全局生效，配置写入 <code>~/.gitconfig</code> 文件。大多数情况下，用全局配置就够了。</p><h3 id="项目级配置（仅当前仓库生效）"><a href="#项目级配置（仅当前仓库生效）" class="headerlink" title="项目级配置（仅当前仓库生效）"></a>项目级配置（仅当前仓库生效）</h3><p>如果某个项目需要用不同的身份提交（比如公司项目用公司邮箱，个人项目用个人邮箱），进入项目目录后，去掉 <code>--global</code> 即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">git config user.name <span class="hljs-string">&quot;工作账号&quot;</span><br>git config user.email <span class="hljs-string">&quot;work@company.com&quot;</span><br></code></pre></td></tr></table></figure><p>项目级配置会写入当前仓库的 <code>.git/config</code> 文件，优先级高于全局配置。</p><h3 id="查看当前配置"><a href="#查看当前配置" class="headerlink" title="查看当前配置"></a>查看当前配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 查看全局配置</span><br>git config --global --list<br><br><span class="hljs-comment"># 查看当前仓库配置（含全局继承）</span><br>git config --list<br></code></pre></td></tr></table></figure><h3 id="常用附加配置"><a href="#常用附加配置" class="headerlink" title="常用附加配置"></a>常用附加配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 设置默认分支名为 main（与 GitHub 保持一致）</span><br>git config --global init.defaultBranch main<br><br><span class="hljs-comment"># 设置默认编辑器为 VS Code</span><br>git config --global core.editor <span class="hljs-string">&quot;code --wait&quot;</span><br><br><span class="hljs-comment"># Windows 用户：解决中文路径乱码问题</span><br>git config --global core.quotepath <span class="hljs-literal">false</span><br><br><span class="hljs-comment"># 配置换行符处理（Windows 推荐）</span><br>git config --global core.autocrlf <span class="hljs-literal">true</span><br><br><span class="hljs-comment"># 配置换行符处理（macOS / Linux 推荐）</span><br>git config --global core.autocrlf input<br></code></pre></td></tr></table></figure><hr><h2 id="二、生成-SSH-密钥并连接-GitHub"><a href="#二、生成-SSH-密钥并连接-GitHub" class="headerlink" title="二、生成 SSH 密钥并连接 GitHub"></a>二、生成 SSH 密钥并连接 GitHub</h2><h3 id="SSH-和-HTTPS-的区别"><a href="#SSH-和-HTTPS-的区别" class="headerlink" title="SSH 和 HTTPS 的区别"></a>SSH 和 HTTPS 的区别</h3><p>连接 GitHub 有两种方式：</p><table><thead><tr><th>方式</th><th>地址格式</th><th>认证方式</th><th>特点</th></tr></thead><tbody><tr><td>HTTPS</td><td><code>https://github.com/...</code></td><td>用户名 + Token</td><td>简单，但每次可能需要输入凭证</td></tr><tr><td>SSH</td><td><code>git@github.com:...</code></td><td>密钥对</td><td>配置一次，永久免密推拉</td></tr></tbody></table><p>SSH 配置稍微复杂一点，但用起来更顺手，推荐长期使用 Git 的开发者配置 SSH。</p><h3 id="第一步：检查是否已有密钥"><a href="#第一步：检查是否已有密钥" class="headerlink" title="第一步：检查是否已有密钥"></a>第一步：检查是否已有密钥</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> ~/.ssh<br></code></pre></td></tr></table></figure><p>如果看到 <code>id_ed25519</code> 和 <code>id_ed25519.pub</code>（或者 <code>id_rsa</code> 和 <code>id_rsa.pub</code>），说明已经有密钥了，可以跳过生成步骤，直接去第三步添加公钥。</p><h3 id="第二步：生成新密钥"><a href="#第二步：生成新密钥" class="headerlink" title="第二步：生成新密钥"></a>第二步：生成新密钥</h3><p>推荐使用 <code>ed25519</code> 算法，比老式的 RSA 更安全，密钥也更短：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh-keygen -t ed25519 -C <span class="hljs-string">&quot;你的邮箱@example.com&quot;</span><br></code></pre></td></tr></table></figure><p>执行后会提示：</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs gradle">Generating <span class="hljs-keyword">public</span>/<span class="hljs-keyword">private</span> ed25519 key pair.<br>Enter <span class="hljs-keyword">file</span> in which to save the key (<span class="hljs-regexp">/Users/y</span>ourname<span class="hljs-regexp">/.ssh/i</span>d_ed25519):<br></code></pre></td></tr></table></figure><p>直接回车使用默认路径。然后会提示设置密码短语（passphrase）：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-attribute">Enter</span> passphrase (empty for <span class="hljs-literal">no</span> passphrase):<br></code></pre></td></tr></table></figure><p>可以直接回车跳过（不设密码），或者设置一个密码增加安全性。设置密码后每次使用密钥都需要输入，可以配合 <code>ssh-agent</code> 缓存密码避免重复输入。</p><p>生成完成后，<code>~/.ssh/</code> 目录下会有两个文件：</p><ul><li><code>id_ed25519</code>：<strong>私钥</strong>，绝对不能泄露，不能上传到任何地方</li><li><code>id_ed25519.pub</code>：<strong>公钥</strong>，内容需要添加到 GitHub</li></ul><h3 id="第三步：将公钥添加到-GitHub"><a href="#第三步：将公钥添加到-GitHub" class="headerlink" title="第三步：将公钥添加到 GitHub"></a>第三步：将公钥添加到 GitHub</h3><p>查看公钥内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cat</span> ~/.ssh/id_ed25519.pub<br></code></pre></td></tr></table></figure><p>复制输出的整段内容（以 <code>ssh-ed25519</code> 开头，以邮箱结尾）。</p><p>打开 GitHub：</p><ol><li>右上角头像 → <strong>Settings</strong></li><li>左侧菜单 → <strong>SSH and GPG keys</strong></li><li>点击 <strong>New SSH key</strong></li><li>Title 填写一个描述，比如 “MacBook Pro” 或 “工作电脑”</li><li>Key type 保持 <strong>Authentication Key</strong></li><li>Key 字段粘贴刚才复制的公钥内容</li><li>点击 <strong>Add SSH key</strong></li></ol><h3 id="第四步：验证连接"><a href="#第四步：验证连接" class="headerlink" title="第四步：验证连接"></a>第四步：验证连接</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh -T git@github.com<br></code></pre></td></tr></table></figure><p>首次连接会提示是否信任 GitHub 的主机指纹：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs rust">The authenticity of host <span class="hljs-symbol">&#x27;github</span>.<span class="hljs-title function_ invoke__">com</span> (<span class="hljs-number">20.27</span>.<span class="hljs-number">177.113</span>)<span class="hljs-string">&#x27; can&#x27;</span>t be established.<br>...<br>Are you sure you want to <span class="hljs-keyword">continue</span> <span class="hljs-title function_ invoke__">connecting</span> (yes/no/[fingerprint])?<br></code></pre></td></tr></table></figure><p>输入 <code>yes</code> 回车，如果看到以下输出，说明连接成功：</p><figure class="highlight ada"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ada">Hi yourname! You<span class="hljs-symbol">&#x27;ve</span> successfully authenticated, but GitHub does <span class="hljs-keyword">not</span> provide shell <span class="hljs-keyword">access</span>.<br></code></pre></td></tr></table></figure><h3 id="启动-ssh-agent（可选，用于缓存密码）"><a href="#启动-ssh-agent（可选，用于缓存密码）" class="headerlink" title="启动 ssh-agent（可选，用于缓存密码）"></a>启动 ssh-agent（可选，用于缓存密码）</h3><p>如果生成密钥时设置了密码短语，可以用 <code>ssh-agent</code> 缓存，避免每次推拉都输入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 启动 ssh-agent</span><br><span class="hljs-built_in">eval</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(ssh-agent -s)</span>&quot;</span><br><br><span class="hljs-comment"># 添加私钥到 agent</span><br>ssh-add ~/.ssh/id_ed25519<br></code></pre></td></tr></table></figure><p>macOS 用户可以在 <code>~/.ssh/config</code> 里配置自动加载：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">Host github.com<br>  AddKeysToAgent <span class="hljs-built_in">yes</span><br>  UseKeychain <span class="hljs-built_in">yes</span><br>  IdentityFile ~/.ssh/id_ed25519<br></code></pre></td></tr></table></figure><hr><h2 id="三、同时推送到-GitHub-和-Gitee"><a href="#三、同时推送到-GitHub-和-Gitee" class="headerlink" title="三、同时推送到 GitHub 和 Gitee"></a>三、同时推送到 GitHub 和 Gitee</h2><p>国内有很多开发者同时维护 GitHub 和 Gitee 两个平台——GitHub 面向国际社区，Gitee 访问更稳定。每次手动推两次太麻烦，下面介绍两种一次推送双仓库的方案。</p><p><img src="/images/git-essential-guide/git-ssh-multi-remote.png" alt="通过 SSH 将一个本地 Git 仓库同步推送到两个远程仓库"></p><blockquote><p>本地仓库只需要维护一套提交历史，通过 SSH 密钥认证后，可以把同一次 <code>push</code> 同步到多个远程地址。</p></blockquote><h3 id="准备工作：在-Gitee-也添加-SSH-公钥"><a href="#准备工作：在-Gitee-也添加-SSH-公钥" class="headerlink" title="准备工作：在 Gitee 也添加 SSH 公钥"></a>准备工作：在 Gitee 也添加 SSH 公钥</h3><p>Gitee 的 SSH 配置方式与 GitHub 类似：</p><ol><li>登录 Gitee，点击右上角头像 → <strong>设置</strong></li><li>左侧 → <strong>SSH 公钥</strong></li><li>将同一个 <code>id_ed25519.pub</code> 的内容粘贴进去（GitHub 和 Gitee 可以共用同一个公钥）</li><li>标题随意填写，确认添加</li></ol><p>验证 Gitee 连接：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh -T git@gitee.com<br></code></pre></td></tr></table></figure><p>看到 <code>Hi yourname! You&#39;ve successfully authenticated</code> 说明连接成功。</p><h3 id="方案一：为一个远程名配置多个推送地址（推荐）"><a href="#方案一：为一个远程名配置多个推送地址（推荐）" class="headerlink" title="方案一：为一个远程名配置多个推送地址（推荐）"></a>方案一：为一个远程名配置多个推送地址（推荐）</h3><p>这是最简洁的方案。<code>git push</code> 时，一次命令同时推到两个仓库。</p><p><strong>第一步：设置 origin 指向 GitHub</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git remote add origin git@github.com:yourname/your-repo.git<br></code></pre></td></tr></table></figure><p>如果 <code>origin</code> 已存在，用 <code>set-url</code> 修改：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git remote set-url origin git@github.com:yourname/your-repo.git<br></code></pre></td></tr></table></figure><p><strong>第二步：为 origin 添加 Gitee 作为额外推送地址</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git remote set-url --add origin git@gitee.com:yourname/your-repo.git<br></code></pre></td></tr></table></figure><p><strong>第三步：验证配置</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git remote -v<br></code></pre></td></tr></table></figure><p>输出如下，表示配置成功：</p><figure class="highlight scss"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scss">origin  git<span class="hljs-keyword">@github</span>.<span class="hljs-attribute">com</span>:yourname/your-repo.git (fetch)<br>origin  git<span class="hljs-keyword">@github</span>.<span class="hljs-attribute">com</span>:yourname/your-repo.git (push)<br>origin  git<span class="hljs-keyword">@gitee</span>.<span class="hljs-attribute">com</span>:yourname/your-repo.git (push)<br></code></pre></td></tr></table></figure><p>注意：<code>fetch</code>（拉取）只从 GitHub 拉，<code>push</code>（推送）会推到两个地址。这个设计是合理的——同步两个仓库时，以 GitHub 为主源即可。</p><p><strong>第四步：正常推送</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git push origin main<br></code></pre></td></tr></table></figure><p>Git 会依次推送到 GitHub 和 Gitee，两个仓库同时更新。</p><hr><h3 id="方案二：配置独立的远程名"><a href="#方案二：配置独立的远程名" class="headerlink" title="方案二：配置独立的远程名"></a>方案二：配置独立的远程名</h3><p>如果需要分别控制推送到哪个仓库，可以给 GitHub 和 Gitee 设置不同的远程名：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 添加 GitHub（命名为 github）</span><br>git remote add github git@github.com:yourname/your-repo.git<br><br><span class="hljs-comment"># 添加 Gitee（命名为 gitee）</span><br>git remote add gitee git@gitee.com:yourname/your-repo.git<br></code></pre></td></tr></table></figure><p>查看配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git remote -v<br></code></pre></td></tr></table></figure><p>推送时指定目标：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 只推 GitHub</span><br>git push github main<br><br><span class="hljs-comment"># 只推 Gitee</span><br>git push gitee main<br><br><span class="hljs-comment"># 同时推两个</span><br>git push github main &amp;&amp; git push gitee main<br></code></pre></td></tr></table></figure><p>这种方案更灵活，适合两个仓库有时需要独立管理的场景。</p><hr><h2 id="四、删除已提交到仓库的记录，但保留文件"><a href="#四、删除已提交到仓库的记录，但保留文件" class="headerlink" title="四、删除已提交到仓库的记录，但保留文件"></a>四、删除已提交到仓库的记录，但保留文件</h2><p>这是个很常见的需求：不小心把不该提交的文件（比如配置文件、密钥、大文件）推上去了，需要从提交历史里删除，但本地文件要保留。</p><h3 id="场景一：只从-Git-追踪中移除，保留本地文件"><a href="#场景一：只从-Git-追踪中移除，保留本地文件" class="headerlink" title="场景一：只从 Git 追踪中移除，保留本地文件"></a>场景一：只从 Git 追踪中移除，保留本地文件</h3><p>如果文件已经被追踪（tracked），想让 Git 忘掉它，但本地文件保留不动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 移除单个文件</span><br>git <span class="hljs-built_in">rm</span> --cached 文件名<br><br><span class="hljs-comment"># 移除整个目录</span><br>git <span class="hljs-built_in">rm</span> --cached -r 目录名/<br><br><span class="hljs-comment"># 移除所有 .env 文件（通配符）</span><br>git <span class="hljs-built_in">rm</span> --cached .<span class="hljs-built_in">env</span><br></code></pre></td></tr></table></figure><p><code>--cached</code> 的含义是：只从 Git 的暂存区和追踪列表中删除，不删除本地文件。</p><p>操作完成后，把文件加入 <code>.gitignore</code>，然后提交：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;.env&quot;</span> &gt;&gt; .gitignore<br>git add .gitignore<br>git commit -m <span class="hljs-string">&quot;remove .env from tracking&quot;</span><br>git push<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ 注意：这只是从当前节点开始不再追踪。历史提交记录中依然存在这个文件。如果文件包含敏感信息（如密钥、密码），需要用下面的方法彻底清除历史。</p></blockquote><hr><h3 id="场景二：彻底从所有历史记录中删除文件"><a href="#场景二：彻底从所有历史记录中删除文件" class="headerlink" title="场景二：彻底从所有历史记录中删除文件"></a>场景二：彻底从所有历史记录中删除文件</h3><p>如果需要从所有提交历史中完全抹掉某个文件（比如误提交了密钥），需要重写 Git 历史。</p><p><strong>方法：使用 <code>git filter-repo</code></strong></p><p><code>git filter-repo</code> 是目前官方推荐的历史重写工具，比已废弃的 <code>filter-branch</code> 更快更安全。</p><p><strong>安装：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># macOS（Homebrew）</span><br>brew install git-filter-repo<br><br><span class="hljs-comment"># pip 安装（跨平台）</span><br>pip install git-filter-repo<br><br><span class="hljs-comment"># Windows（通过 pip）</span><br>pip install git-filter-repo<br></code></pre></td></tr></table></figure><p><strong>从历史中删除指定文件：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git filter-repo --path 要删除的文件名 --invert-paths<br></code></pre></td></tr></table></figure><p><code>--invert-paths</code> 表示反向匹配，即删除指定路径，保留其他所有文件。</p><p><strong>删除某个目录：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git filter-repo --path 目录名/ --invert-paths<br></code></pre></td></tr></table></figure><p><strong>删除多个文件：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git filter-repo --path file1.txt --path secrets/config.json --invert-paths<br></code></pre></td></tr></table></figure><p><strong>操作完成后，强制推送到远程：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 重新关联远程仓库（filter-repo 会移除 remote，需手动添加回来）</span><br>git remote add origin git@github.com:yourname/your-repo.git<br><br><span class="hljs-comment"># 强制推送所有分支</span><br>git push origin --force --all<br><br><span class="hljs-comment"># 强制推送所有 tag</span><br>git push origin --force --tags<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>强制推送会重写远程历史</strong>，团队协作时必须通知所有成员重新克隆仓库，否则他们再次推送时会把旧历史带回来。</p></blockquote><hr><h3 id="场景三：撤销最近一次提交，保留文件修改"><a href="#场景三：撤销最近一次提交，保留文件修改" class="headerlink" title="场景三：撤销最近一次提交，保留文件修改"></a>场景三：撤销最近一次提交，保留文件修改</h3><p>如果只是最近一次提交提交错了，想撤回但保留文件改动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git reset --soft HEAD~1<br></code></pre></td></tr></table></figure><p><code>--soft</code> 表示撤销提交，但保留改动在暂存区（staged）。文件内容不变，可以重新修改后再提交。</p><p>如果想连暂存区也清空（改动保留在工作区但未 staged）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git reset HEAD~1<br></code></pre></td></tr></table></figure><p>等同于 <code>git reset --mixed HEAD~1</code>（默认模式）。</p><blockquote><p>如果已经推送到远程，撤销后需要强制推送：<code>git push --force</code>。同样需要注意团队协作的影响。</p></blockquote><hr><h3 id="撤销操作对比"><a href="#撤销操作对比" class="headerlink" title="撤销操作对比"></a>撤销操作对比</h3><table><thead><tr><th>命令</th><th>提交记录</th><th>暂存区</th><th>工作区文件</th><th>适用场景</th></tr></thead><tbody><tr><td><code>git rm --cached</code></td><td>保留（新增一次提交）</td><td>移除追踪</td><td><strong>保留</strong></td><td>取消追踪但不删文件</td></tr><tr><td><code>git reset --soft HEAD~1</code></td><td>撤销最近提交</td><td>保留改动</td><td><strong>保留</strong></td><td>撤销提交但保留暂存</td></tr><tr><td><code>git reset HEAD~1</code></td><td>撤销最近提交</td><td>清空</td><td><strong>保留</strong></td><td>撤销提交，文件回到未暂存</td></tr><tr><td><code>git reset --hard HEAD~1</code></td><td>撤销最近提交</td><td>清空</td><td><strong>丢失</strong></td><td>彻底放弃改动（危险）</td></tr><tr><td><code>git filter-repo</code></td><td>重写所有历史</td><td>—</td><td><strong>保留</strong></td><td>彻底清除敏感文件</td></tr></tbody></table><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这篇文章覆盖了 Git 日常使用中最常见的几个场景：</p><ul><li><strong>基础配置</strong>：<code>user.name</code> 和 <code>user.email</code> 是最基本的，项目级配置可以覆盖全局配置，适合多身份开发场景</li><li><strong>SSH 连接</strong>：<code>ed25519</code> 算法生成密钥，公钥添加到 GitHub，一次配置永久免密</li><li><strong>多仓库推送</strong>：用 <code>git remote set-url --add</code> 为 <code>origin</code> 添加第二个推送地址，一次 <code>push</code> 同步 GitHub 和 Gitee</li><li><strong>删除提交记录</strong>：用 <code>git rm --cached</code> 取消追踪文件；用 <code>git filter-repo</code> 彻底重写历史；用 <code>git reset --soft</code> 撤销最近提交但保留改动</li></ul><p>Git 的强大之处在于它给了你完整的历史控制权——但权力越大，操作前越要谨慎，尤其是涉及强制推送和历史重写的操作，在团队中使用前一定要先沟通。</p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/git-essential-guide/</id>
    <link href="https://blog.280303.xyz/posts/git-essential-guide/"/>
    <published>2026-06-01T01:00:00.000Z</published>
    <summary>从零开始配置 Git，生成 SSH 密钥连接 GitHub，同时推送到 GitHub 和 Gitee 双仓库，以及如何安全删除已提交记录但保留文件——这篇文章把常用的 Git 操作讲清楚。</summary>
    <title>Git 实用指南：从基础配置到多仓库协作</title>
    <updated>2026-06-01T01:03:09.318Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="技术实践" scheme="https://blog.280303.xyz/categories/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/"/>
    <category term="ChatGPT" scheme="https://blog.280303.xyz/tags/ChatGPT/"/>
    <category term="Apple ID" scheme="https://blog.280303.xyz/tags/Apple-ID/"/>
    <category term="土耳其区" scheme="https://blog.280303.xyz/tags/%E5%9C%9F%E8%80%B3%E5%85%B6%E5%8C%BA/"/>
    <category term="礼品卡" scheme="https://blog.280303.xyz/tags/%E7%A4%BC%E5%93%81%E5%8D%A1/"/>
    <category term="省钱技巧" scheme="https://blog.280303.xyz/tags/%E7%9C%81%E9%92%B1%E6%8A%80%E5%B7%A7/"/>
    <content>
      <![CDATA[<p>ChatGPT Plus 一个月 20 美元，折合人民币 140 块左右，说贵不贵，但每个月交钱还是有点肉疼。</p><p>其实有一个办法可以把月费压到 <strong>80 块左右</strong>：用土耳其区 Apple ID + 礼品卡。</p><p>这篇文章先讲为什么土耳其区能做到这个价格，再说完整操作流程。</p><hr><h2 id="技术背景"><a href="#技术背景" class="headerlink" title="技术背景"></a>技术背景</h2><h3 id="为什么不同国家定价不一样"><a href="#为什么不同国家定价不一样" class="headerlink" title="为什么不同国家定价不一样"></a>为什么不同国家定价不一样</h3><p>OpenAI 对 GPT Plus 的定价是<strong>按地区分层的</strong>，不是一刀切收美元。背后的逻辑是”购买力平价”——同一个产品，在消费水平不同的国家定不同的价。</p><h3 id="为什么走-Apple-内购，而不直接绑卡"><a href="#为什么走-Apple-内购，而不直接绑卡" class="headerlink" title="为什么走 Apple 内购，而不直接绑卡"></a>为什么走 Apple 内购，而不直接绑卡</h3><p>用土耳其区 Apple ID 通过 App Store 订阅 GPT Plus，比直接绑土耳其信用卡有三个关键优势：</p><ol><li><strong>不需要土耳其银行卡</strong>：直接绑卡会验证发卡国家，国内发行的卡被拒概率极大。走 Apple ID + 礼品卡就绕过了这一步</li><li><strong>充值灵活</strong>：花多少钱充多少钱，不怕自动续费时余额不够卡住</li><li><strong>Apple 统一结算</strong>：OpenAI 看不到你的支付方式，只收到 Apple 转来的订阅款</li></ol><h3 id="礼品卡从哪来——Oyunfor"><a href="#礼品卡从哪来——Oyunfor" class="headerlink" title="礼品卡从哪来——Oyunfor"></a>礼品卡从哪来——Oyunfor</h3><p>土耳其的 App Store 礼品卡不像日本、美国那样容易买到。Oyunfor 是目前最稳定的第三方渠道：</p><ul><li>专门卖土耳其区游戏充值卡和 App Store 礼品卡</li><li>支持多种在线支付（Visa、MasterCard、加密货币等）</li><li><strong>到账快</strong>，一般几分钟内邮箱收到兑换码</li><li>汇率接近官方，溢价不高</li></ul><p>当然它也有风险：第三方平台本身不是苹果官方渠道，有极小概率买到无效卡。但实际使用下来，这个渠道的稳定性在圈内认可度较高。</p><hr><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h3 id="前提准备"><a href="#前提准备" class="headerlink" title="前提准备"></a>前提准备</h3><table><thead><tr><th>你需要准备</th><th>说明</th></tr></thead><tbody><tr><td>一个国内 Apple ID</td><td>日常用的就好，用来绑定 App Store「退出-换号-登录」</td></tr><tr><td>一个能收短信的手机号</td><td>国内手机号即可，注册土耳其 ID 时收验证码</td></tr><tr><td>一台 iPhone 或 iPad</td><td>全程不需要电脑，手机端完成所有操作</td></tr><tr><td>科学上网环境</td><td>ChatGPT 和 Oyunfor 都需要</td></tr></tbody></table><h3 id="第一步：注册土耳其区-Apple-ID"><a href="#第一步：注册土耳其区-Apple-ID" class="headerlink" title="第一步：注册土耳其区 Apple ID"></a>第一步：注册土耳其区 Apple ID</h3><p>这部分是核心，注意细节。</p><ol><li>打开 Safari 浏览器（<strong>用无痕&#x2F;隐私模式</strong>），访问 <a href="https://appleid.apple.com/">appleid.apple.com</a></li><li>点击「创建 Apple ID」，填写注册信息：<ul><li>姓名：用拼音即可</li><li><strong>国家&#x2F;地区：一定要选「Turkey（土耳其）」</strong></li><li>生日：填成年日期</li><li>邮箱：用一个<strong>没注册过 Apple ID</strong> 的邮箱，<strong>不要用 QQ 邮箱</strong>（容易触发风控，验证邮件也经常被拦截）</li><li>手机号：选中国大陆 <code>+86</code>，填你自己的手机号</li></ul></li><li>完成手机号和邮箱验证码</li><li>注册成功后会提示验证账单信息——<strong>直接跳过，不用填</strong></li></ol><blockquote><p><strong>关键点</strong>：付款方式留空，后面用礼品卡充值。如果这里选了付款方式反而麻烦。</p></blockquote><h3 id="第二步：在-App-Store-登录土耳其-ID（关键：锁定地区）"><a href="#第二步：在-App-Store-登录土耳其-ID（关键：锁定地区）" class="headerlink" title="第二步：在 App Store 登录土耳其 ID（关键：锁定地区）"></a>第二步：在 App Store 登录土耳其 ID（关键：锁定地区）</h3><p>这一步很容易踩坑——如果直接登录，App Store 可能会自动切回中国大陆地区。</p><p>正确做法：</p><ol><li><p>先别急着登录，<strong>在 Safari 中打开这个链接</strong>：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs vim">itms-apps://itunes.apple.<span class="hljs-keyword">com</span>/WebObjects/MZStore.woa/<span class="hljs-keyword">wa</span>/resetAndRedirect?dsf=<span class="hljs-number">143480</span>&amp;<span class="hljs-keyword">cc</span>=<span class="hljs-keyword">tr</span><br></code></pre></td></tr></table></figure><blockquote><p>这个链接会强制把 App Store 地区重置为土耳其（<code>cc=tr</code>），并跳转到土区商店首页</p></blockquote></li><li><p>跳转成功后，这时再登录你的土耳其 ID</p></li><li><p>首次登录可能会提示「此 Apple ID 还未在 App Store 使用过」——点「检查」然后一路跳过，付款方式选「无」</p></li></ol><p>登录成功后，App Store 界面会变成土耳其语或英语。<strong>价格单位显示为 ₺（土耳其里拉）就对了。</strong></p><blockquote><p><strong>为什么要这样做</strong>：不通过这个链接锁定地区，App Store 可能会根据设备语言、IP 地址等因素悄悄切回中国区。一旦切回国区，你在土耳其 ID 下的余额就看不到了——钱还在，但你用不了。</p></blockquote><h3 id="第三步：在-Oyunfor-购买礼品卡"><a href="#第三步：在-Oyunfor-购买礼品卡" class="headerlink" title="第三步：在 Oyunfor 购买礼品卡"></a>第三步：在 Oyunfor 购买礼品卡</h3><ol><li><p>打开 <a href="https://www.oyunfor.com/">Oyunfor</a> 网站</p><blockquote><p><strong>务必认准 oyunfor.com</strong>，有些山寨网站名称类似。最好手动输入 URL，不要点搜索结果里的广告链接。</p></blockquote></li><li><p>注册账号（可以用邮箱注册，不需要土耳其手机号）</p></li><li><p>搜索 <strong>App Store &amp; iTunes Turkey</strong> 面额卡，或找 <strong>iTunes Turkey Gift Card</strong></p></li><li><p>选择面额。<strong>首次建议先买小额卡（₺50-100），不要一次充够订阅费</strong>。GPT Plus 正式订阅时月费 ₺500：</p><ul><li><strong>养号期</strong>：先买一张小额卡（₺50 左右），充进去放着</li><li><strong>正式订阅</strong>：养号几天后，再买 ₺500 的卡补足余额</li><li><strong>长期使用</strong>：之后可以买 ₺1000 一次性够两个月</li></ul></li><li><p>结算支付：</p><ul><li>支持 Visa &#x2F; MasterCard 美元卡、加密货币等</li><li>国内发行的双币信用卡一般可以用（Oyunfor 不会验证发卡国家）</li><li>支付成功后有 2%-5% 的平台手续费，比直接汇率略高一点，但仍在可接受范围</li></ul></li><li><p>等邮件——买完后几分钟内收到兑换码到注册邮箱</p></li></ol><h3 id="第四步：兑换礼品卡，充入土耳其-Apple-ID"><a href="#第四步：兑换礼品卡，充入土耳其-Apple-ID" class="headerlink" title="第四步：兑换礼品卡，充入土耳其 Apple ID"></a>第四步：兑换礼品卡，充入土耳其 Apple ID</h3><ol><li>打开 App Store，确认已登录土耳其 ID</li><li>点右上角头像 → <strong>Redeem Gift Card or Code</strong>（兑换礼品卡或代码）</li><li>输入 Oyunfor 邮件里的兑换码</li><li>点击 Redeem，余额立刻到账</li></ol><p>此时回到 App Store 头像页，能看到 <strong>Apple ID Balance（余额）</strong> 增加了对应的里拉金额。</p><blockquote><p><strong>关键：充完别马上订阅，先放几天。</strong> 新注册的土耳其 Apple ID 直接大额消费容易被风控标记。建议先充一张小额卡（₺50 左右），正常用这个 ID 在 App Store 下载一两个免费 App，等 3-5 天再补足余额去订阅。这个「养号」过程能大大降低封号风险。</p></blockquote><h3 id="第五步：订阅-ChatGPT-Plus（养号完成后）"><a href="#第五步：订阅-ChatGPT-Plus（养号完成后）" class="headerlink" title="第五步：订阅 ChatGPT Plus（养号完成后）"></a>第五步：订阅 ChatGPT Plus（养号完成后）</h3><ol><li>在 App Store 搜索 <strong>ChatGPT</strong></li><li>下载安装（如果之前用其他区 Apple ID 下载过，需要先删掉再重装，否则订阅入口可能不对）</li><li>打开 ChatGPT App → 登录你的 OpenAI 账号 → <strong>Settings → Go to Plus</strong></li><li>订阅页面显示价格是 <strong>₺500 &#x2F; month</strong>，确认无误后点订阅</li><li>使用 Apple ID 余额支付，完成后 GPT Plus 立即生效</li></ol><p><strong>确认订阅成功的标志</strong>：ChatGPT 设置页显示「ChatGPT Plus - Active」，模型选择有 GPT-5.5 等 Plus 专属模型。</p><h3 id="第六步：保持续费（重要）"><a href="#第六步：保持续费（重要）" class="headerlink" title="第六步：保持续费（重要）"></a>第六步：保持续费（重要）</h3><p>Apple 会在每次续费日自动从余额扣款。需要留意两件事：</p><ol><li><strong>余额要够</strong>：快到期时确保 Apple ID 余额 ≥ ₺500，否则扣费失败，订阅会被取消</li><li><strong>礼品卡有有效期</strong>：Oyunfor 买的卡一般半年到一年内有效，不要囤太多，够用就行</li></ol><p>建议在手机日历上设置一个每月提醒，提前 3 天检查余额。</p><hr><h2 id="日常使用：切换-Apple-ID-不影响-GPT-Plus"><a href="#日常使用：切换-Apple-ID-不影响-GPT-Plus" class="headerlink" title="日常使用：切换 Apple ID 不影响 GPT Plus"></a>日常使用：切换 Apple ID 不影响 GPT Plus</h2><p>很多人担心在 App Store 切换回国内 ID 后，GPT Plus 会掉。</p><p>不会。<strong>订阅跟 Apple ID 走</strong>，不跟设备上登录的账号走。流程是这样的：</p><ul><li>在土耳其 ID 下订阅 GPT Plus</li><li>之后在 App Store 切回国内 ID 正常用其他 App、更新应用</li><li>ChatGPT App 依然是 Plus 状态</li><li>Apple 每月自动从土耳其 ID 扣款（只要余额够）</li></ul><p>你只是在需要下载土耳其区专属 App 或兑换新礼品卡时需要切回土耳其 ID——平时正常用国内 ID 就行。</p><hr><h2 id="风险与避坑"><a href="#风险与避坑" class="headerlink" title="风险与避坑"></a>风险与避坑</h2><table><thead><tr><th>风险</th><th>说明</th><th>怎么避免</th></tr></thead><tbody><tr><td>Apple 封号</td><td>跨区使用违反 Apple 服务条款</td><td>概率极低，但确实存在。建议不要频繁切换不同地区</td></tr><tr><td>礼品卡无效</td><td>Oyunfor 卡有极小概率出问题</td><td>保留购买凭证邮件，联系 Oyunfor 客服处理</td></tr><tr><td>订阅中断</td><td>余额不足导致扣费失败</td><td>设置日历提醒，提前充值</td></tr><tr><td>汇率波动</td><td>里拉对人民币汇率每天在变</td><td>实际影响很小（几分钱），不必纠结</td></tr><tr><td>政策变化</td><td>OpenAI 可能调整土耳其定价或限制 Apple 内购</td><td>无法控制。一旦发生，卡内余额可以买其他土耳其区 App</td></tr></tbody></table><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>土耳其区 Apple ID 订阅 GPT Plus 的核心原理就一句话：<strong>利用地区定价差异 + Apple 内购礼品卡机制，绕过支付验证，用土耳其价格付款。</strong></p><p>操作上分六步走：注册土区 Apple ID → 登录 App Store → Oyunfor 买卡 → 兑换充值 → ChatGPT 内订阅 → 保持余额够续费。</p><p>唯一的持续工作就是每月检查一次余额——相比省下来的 60 多块钱，这个代价可以忽略不计。</p><hr><blockquote><p><strong>免责声明</strong>：跨区使用 Apple ID 订阅服务不符合 Apple 服务条款，本文仅分享技术操作流程，请自行评估风险后决定是否使用。如果 OpenAI 或 Apple 更改政策，上述方法可能失效。</p></blockquote>]]>
    </content>
    <id>https://blog.280303.xyz/posts/turkish-appleid-gpt-plus/</id>
    <link href="https://blog.280303.xyz/posts/turkish-appleid-gpt-plus/"/>
    <published>2026-05-29T09:22:00.000Z</published>
    <summary>GPT Plus 订阅在不同国家定价差异很大。用土耳其区 Apple ID 配合 Oyunfor 礼品卡，月费从 20 美元降到约 80 元人民币——本文讲清楚背后的原因和完整操作流程。</summary>
    <title>用土耳其区 Apple ID + 礼品卡订阅 GPT Plus，省下一半费用</title>
    <updated>2026-05-29T09:35:08.604Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="技术实践" scheme="https://blog.280303.xyz/categories/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/"/>
    <category term="Hexo" scheme="https://blog.280303.xyz/tags/Hexo/"/>
    <category term="Cloudflare Pages" scheme="https://blog.280303.xyz/tags/Cloudflare-Pages/"/>
    <category term="Google Search Console" scheme="https://blog.280303.xyz/tags/Google-Search-Console/"/>
    <category term="博客搭建" scheme="https://blog.280303.xyz/tags/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/"/>
    <category term="SEO" scheme="https://blog.280303.xyz/tags/SEO/"/>
    <content>
      <![CDATA[<p>很多人写博客，最后卡在”写完了放哪”这件事上。自己搭服务器太贵、GitHub Pages 国内访问慢、Vercel 部分地区被墙……有没有一个又免费、又快、又稳的方案？</p><p>有。<strong>Hexo + Cloudflare Pages + Google Search Console</strong>，这三个工具组合在一起，就是一套完整的个人博客解决方案。</p><p>这篇文章讲完整的链路：本地写文章 → 推到 GitHub → 自动构建部署到 Cloudflare Pages → 提交给 Google 收录。</p><p><img src="/images/hexo-cloudflare-pages-google/hexo-cloudflare-google-hero.png" alt="Hexo 博客从本地写作、Git 推送、Cloudflare Pages 部署到 Google 收录的完整链路"></p><blockquote><p>这套方案的关键，是把写作、构建、部署和收录拆成清晰的自动化链路。</p></blockquote><hr><h2 id="技术背景"><a href="#技术背景" class="headerlink" title="技术背景"></a>技术背景</h2><h3 id="为什么选-Hexo"><a href="#为什么选-Hexo" class="headerlink" title="为什么选 Hexo"></a>为什么选 Hexo</h3><p>Hexo 是一个基于 Node.js 的静态博客框架。核心逻辑很简单：你写 Markdown 文件，Hexo 把它们渲染成 HTML。</p><p>它的优势是：</p><ul><li><strong>纯静态</strong>：生成的是 HTML&#x2F;CSS&#x2F;JS 文件，不需要数据库，也不需要服务器运行时</li><li><strong>主题生态丰富</strong>：Fluid、NexT、Butterfly 等主题开箱即用，颜值不错</li><li><strong>部署友好</strong>：生成的静态文件可以直接扔到任何静态托管服务</li></ul><p>缺点也很明显：不适合频繁更新的动态内容，评论功能需要依赖第三方服务（Disqus、Waline 等）。</p><h3 id="为什么选-Cloudflare-Pages"><a href="#为什么选-Cloudflare-Pages" class="headerlink" title="为什么选 Cloudflare Pages"></a>为什么选 Cloudflare Pages</h3><p>你可能熟悉 GitHub Pages，但 Cloudflare Pages 在它基础上有几个关键优势：</p><table><thead><tr><th>对比项</th><th>GitHub Pages</th><th>Cloudflare Pages</th></tr></thead><tbody><tr><td>全球 CDN</td><td>有限</td><td>✅ 覆盖全球 200+ 节点</td></tr><tr><td>国内访问</td><td>不稳定</td><td>✅ 相对稳定</td></tr><tr><td>自定义域名</td><td>支持</td><td>✅ 支持，且自动 HTTPS</td></tr><tr><td>构建触发</td><td>Push 触发</td><td>✅ Push 触发</td></tr><tr><td>免费额度</td><td>无限</td><td>✅ 每月 500 次构建免费</td></tr><tr><td>预览部署</td><td>不支持</td><td>✅ PR 自动生成预览 URL</td></tr></tbody></table><p>Cloudflare 本身就是全球最大的 CDN 服务商之一，用它托管静态博客，访问速度和稳定性都很有保障。</p><h3 id="为什么要提交-Google-Search-Console"><a href="#为什么要提交-Google-Search-Console" class="headerlink" title="为什么要提交 Google Search Console"></a>为什么要提交 Google Search Console</h3><p>写完博客不代表 Google 能搜到。Google 的爬虫会自己发现网页，但这个过程可能要几周甚至更久。</p><p>Google Search Console（GSC）是 Google 提供的免费工具，核心功能有两个：</p><ol><li><strong>验证你的网站所有权</strong>：告诉 Google”这个域名是我的”</li><li><strong>主动提交 Sitemap</strong>：让 Google 知道你网站的所有页面，加速收录</li></ol><p>配合 Hexo 的 sitemap 插件，每次发布新文章后提交一次，通常一两天内就能在 Google 搜到。</p><hr><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h3 id="第一步：安装-Hexo，初始化博客"><a href="#第一步：安装-Hexo，初始化博客" class="headerlink" title="第一步：安装 Hexo，初始化博客"></a>第一步：安装 Hexo，初始化博客</h3><p>确保本地已安装 Node.js（建议 v18 以上），然后全局安装 Hexo：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install -g hexo-cli<br></code></pre></td></tr></table></figure><p>初始化博客项目：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">hexo init my-blog<br><span class="hljs-built_in">cd</span> my-blog<br>npm install<br></code></pre></td></tr></table></figure><p>本地预览，确认能正常运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">hexo server<br><span class="hljs-comment"># 浏览器打开 http://localhost:4000</span><br></code></pre></td></tr></table></figure><h3 id="第二步：安装主题（以-Fluid-为例）"><a href="#第二步：安装主题（以-Fluid-为例）" class="headerlink" title="第二步：安装主题（以 Fluid 为例）"></a>第二步：安装主题（以 Fluid 为例）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install --save hexo-theme-fluid<br></code></pre></td></tr></table></figure><p>在 <code>_config.yml</code> 中切换主题：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">theme:</span> <span class="hljs-string">fluid</span><br></code></pre></td></tr></table></figure><p>创建 <code>_config.fluid.yml</code>（主题专属配置，优先级更高），参考 <a href="https://hexo.fluid-dev.com/docs/start/">Fluid 官方文档</a> 按需配置。</p><h3 id="第三步：安装-Sitemap-插件（必须）"><a href="#第三步：安装-Sitemap-插件（必须）" class="headerlink" title="第三步：安装 Sitemap 插件（必须）"></a>第三步：安装 Sitemap 插件（必须）</h3><blockquote><p><strong>这一步不能跳过。</strong> 没有 Sitemap，Google 不知道你网站有哪些页面，SEO 等于白做。</p></blockquote><p>Hexo 默认不带 Sitemap，需要手动安装插件。安装后每次 <code>hexo generate</code> 都会自动生成 <code>/sitemap.xml</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install hexo-generator-sitemap --save<br></code></pre></td></tr></table></figure><p>同时把博客的 URL 改成你的真实域名（这影响 sitemap 里的链接是否正确）：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">url:</span> <span class="hljs-string">https://your-domain.com</span><br></code></pre></td></tr></table></figure><h3 id="第四步：推送到-GitHub"><a href="#第四步：推送到-GitHub" class="headerlink" title="第四步：推送到 GitHub"></a>第四步：推送到 GitHub</h3><p>在 GitHub 创建一个新的仓库（比如 <code>my-blog</code>），然后把本地项目推上去：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">git init<br>git add .<br>git commit -m <span class="hljs-string">&quot;init: hexo blog&quot;</span><br>git remote add origin https://github.com/your-username/my-blog.git<br>git push -u origin main<br></code></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：把 <code>public/</code> 目录加入 <code>.gitignore</code>，不需要把生成物提交到仓库——Cloudflare Pages 会帮你在云端构建。</p></blockquote><p><code>.gitignore</code> 里添加：</p><figure class="highlight x86asm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs x86asm"><span class="hljs-meta">public</span>/<br><span class="hljs-meta">.deploy_git</span>/<br></code></pre></td></tr></table></figure><h3 id="第五步：连接-Cloudflare-Pages"><a href="#第五步：连接-Cloudflare-Pages" class="headerlink" title="第五步：连接 Cloudflare Pages"></a>第五步：连接 Cloudflare Pages</h3><ol><li><p>登录 <a href="https://dash.cloudflare.com/">Cloudflare Dashboard</a>，进入 <strong>Workers &amp; Pages</strong></p></li><li><p>点击 <strong>Create application</strong> → <strong>Pages</strong> → <strong>Connect to Git</strong></p></li><li><p>授权连接你的 GitHub 账号，选择刚才的仓库</p></li><li><p>配置构建参数：</p><table><thead><tr><th>配置项</th><th>填写内容</th></tr></thead><tbody><tr><td>Framework preset</td><td>Hexo</td></tr><tr><td>Build command</td><td><code>hexo generate</code></td></tr><tr><td>Build output directory</td><td><code>public</code></td></tr></tbody></table></li><li><p>点击 <strong>Save and Deploy</strong>，等待第一次构建完成</p></li></ol><p>构建成功后，Cloudflare 会给你一个默认域名，格式类似 <code>my-blog-xxx.pages.dev</code>，可以直接访问。</p><p><img src="/images/hexo-cloudflare-pages-google/cloudflare-pages-deploy.png" alt="Cloudflare Pages 接收 Git 推送后自动构建 Hexo，并把静态文件发布到全球节点"></p><blockquote><p>GitHub 负责存源码，Cloudflare Pages 负责构建和分发，<code>public/</code> 目录不需要手动提交。</p></blockquote><h3 id="第六步：绑定自定义域名（可选但推荐）"><a href="#第六步：绑定自定义域名（可选但推荐）" class="headerlink" title="第六步：绑定自定义域名（可选但推荐）"></a>第六步：绑定自定义域名（可选但推荐）</h3><p>在 Pages 项目设置里，进入 <strong>Custom domains</strong> → 添加你的域名。</p><p>如果域名也托管在 Cloudflare，DNS 会自动配置；如果在其他平台（如阿里云、腾讯云），按提示添加 CNAME 记录即可：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs makefile"><span class="hljs-section">类型: CNAME</span><br><span class="hljs-section">名称: @（或 www）</span><br><span class="hljs-section">目标: your-project.pages.dev</span><br></code></pre></td></tr></table></figure><h3 id="第七步：提交-Google-Search-Console"><a href="#第七步：提交-Google-Search-Console" class="headerlink" title="第七步：提交 Google Search Console"></a>第七步：提交 Google Search Console</h3><p><strong>7.1 验证网站所有权</strong></p><p>打开 <a href="https://search.google.com/search-console">Google Search Console</a>，点击 <strong>添加资源</strong>。</p><p>推荐选择 <strong>网址前缀</strong> 方式，输入你的完整域名（如 <code>https://your-domain.com</code>）。</p><p>验证方式推荐 <strong>HTML 标签</strong>：</p><ol><li><p>Google 会给你一段 meta 标签，类似：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;google-site-verification&quot;</span> <span class="hljs-attr">content</span>=<span class="hljs-string">&quot;xxxxxxxxxxxxxxxx&quot;</span> /&gt;</span><br></code></pre></td></tr></table></figure></li><li><p>在 Hexo 配置中加入这段 meta。如果用的是 Fluid 主题，在 <code>_config.fluid.yml</code> 里：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">head:</span><br>  <span class="hljs-attr">meta:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">google-site-verification</span><br>      <span class="hljs-attr">content:</span> <span class="hljs-string">&quot;xxxxxxxxxxxxxxxx&quot;</span><br></code></pre></td></tr></table></figure></li><li><p>重新生成部署（push 到 GitHub，等 Cloudflare 自动构建），然后回到 GSC 点击验证。</p></li></ol><p><strong>7.2 提交 Sitemap</strong></p><p>验证通过后，在 GSC 左侧菜单找到 <strong>站点地图</strong>，输入：</p><figure class="highlight lasso"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs lasso">sitemap.<span class="hljs-built_in">xml</span><br></code></pre></td></tr></table></figure><p>点击提交。Google 会开始抓取你的所有页面，通常 1-3 天内完成首次收录。</p><p><img src="/images/hexo-cloudflare-pages-google/google-search-console-indexing.png" alt="通过 Google Search Console 提交 sitemap，让搜索引擎更快发现博客页面"></p><blockquote><p>Sitemap 的作用是把站点结构主动交给 Google，减少新文章“等爬虫偶遇”的时间。</p></blockquote><hr><h2 id="后续工作流"><a href="#后续工作流" class="headerlink" title="后续工作流"></a>后续工作流</h2><p>搭好之后，日常写博客的流程就变得很简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 新建一篇文章</span><br>hexo new post <span class="hljs-string">&quot;我的新文章&quot;</span><br><br><span class="hljs-comment"># 本地预览</span><br>hexo server<br><br><span class="hljs-comment"># 写完后推送到 GitHub</span><br>git add .<br>git commit -m <span class="hljs-string">&quot;post: 我的新文章&quot;</span><br>git push<br><br><span class="hljs-comment"># Cloudflare Pages 自动触发构建，几分钟后上线</span><br></code></pre></td></tr></table></figure><p>每次推送新文章后，可以去 Google Search Console 的 <strong>URL 检查</strong> 工具，输入文章 URL 手动请求编入索引，加快收录速度。</p><hr><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p><strong>Q：没装 Sitemap 插件，Google 还能收录我的文章吗？</strong><br>A：能，但很慢。Google 没有 Sitemap 就只能靠自然抓取——新文章可能要等几周甚至不被发现。<strong>强烈建议安装</strong>，这是 SEO 最基础的配置。</p><p><strong>Q：Sitemap 里的链接是 <code>http://example.com/...</code>？</strong><br>A：没有修改 <code>_config.yml</code> 里的 <code>url</code> 字段。把它改成你的真实域名并重新部署。</p><p><strong>Q：Google 很久没收录怎么办？</strong><br>A：确认 sitemap 可以正常访问（<code>https://your-domain.com/sitemap.xml</code>），并在 GSC 的 URL 检查里手动提交几篇文章的链接。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整条链路的核心是：<strong>Hexo 生成静态文件 → Git 管理版本 → Cloudflare Pages 自动构建部署 → Google Search Console 加速收录</strong>。</p><p>每个环节都选了成本最低（免费）、维护最少的方案。一次搭好，之后只需要专注写文章。</p><hr>]]>
    </content>
    <id>https://blog.280303.xyz/posts/hexo-cloudflare-pages-google/</id>
    <link href="https://blog.280303.xyz/posts/hexo-cloudflare-pages-google/"/>
    <published>2026-05-29T08:54:00.000Z</published>
    <summary>从本地写 Markdown，到全球可访问的博客，再到 Google 能搜到你——这篇文章把整条链路讲清楚。</summary>
    <title>用 Hexo 写博客，部署到 Cloudflare Pages，再推送给 Google</title>
    <updated>2026-05-29T09:21:00.058Z</updated>
  </entry>
  <entry>
    <author>
      <name>lingyi</name>
    </author>
    <category term="技术思考" scheme="https://blog.280303.xyz/categories/%E6%8A%80%E6%9C%AF%E6%80%9D%E8%80%83/"/>
    <category term="AI" scheme="https://blog.280303.xyz/tags/AI/"/>
    <category term="开发效率" scheme="https://blog.280303.xyz/tags/%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%87/"/>
    <category term="Git" scheme="https://blog.280303.xyz/tags/Git/"/>
    <category term="工程实践" scheme="https://blog.280303.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<p>最近有个趋势很明显：越来越多人开始用 AI 写代码，然后用”AI 写了整个项目”来炫耀生产力。但我的感受恰恰相反——<strong>AI 做主导，是最低效的用法。</strong></p><p>这篇文章聊聊我实际用下来的一套工作方式：让 AI 做助手，Git 做守卫，人来做判断。</p><p><img src="/images/ai-dev/ai-assisted-development-hero.png" alt="AI 辅助开发中，开发者负责判断，AI 负责执行，Git 负责记录"></p><blockquote><p>真正高效的 AI 辅助开发，不是让 AI 接管项目，而是把它放在清晰边界内做执行。</p></blockquote><hr><h2 id="AI-不适合做主导，原因很简单"><a href="#AI-不适合做主导，原因很简单" class="headerlink" title="AI 不适合做主导，原因很简单"></a>AI 不适合做主导，原因很简单</h2><p>AI 很聪明，但它没有上下文判断力。</p><p>你让它写一个完整功能，它可以给你一堆代码，但它不知道：</p><ul><li>这个功能背后的业务逻辑是否真的成立</li><li>某段看起来合理的代码是否会和你已有模块冲突</li><li>一个”优化”是否会悄悄改变原来的行为边界</li></ul><p>更危险的是，<strong>AI 生成的代码，语法上完全正确，逻辑上可能千疮百孔</strong>。如果你把它当主角、全盘接收，最后要填的坑，比自己写还多。</p><p>AI 适合做助手，不适合做架构师，更不适合做决策者。</p><hr><h2 id="AI-真正擅长的：重复性工作"><a href="#AI-真正擅长的：重复性工作" class="headerlink" title="AI 真正擅长的：重复性工作"></a>AI 真正擅长的：重复性工作</h2><p>换个思路，把 AI 定位成一个”极其高效的代码民工”，它的价值就出来了。</p><h3 id="工具类函数"><a href="#工具类函数" class="headerlink" title="工具类函数"></a>工具类函数</h3><p>这是 AI 最拿手的场景之一。</p><p>日期格式化、字符串处理、数组去重、文件路径解析……这类代码有标准答案、没有业务歧义、可以直接测试验证。以前这类工具函数可能要翻文档、找案例，花十几分钟。现在描述清楚需求，AI 30 秒出代码，你 Review 一遍、跑一下测试，搞定。</p><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs awk"><span class="hljs-regexp">//</span> 告诉 AI：写一个把秒级时间戳转成 <span class="hljs-string">&quot;YYYY-MM-DD HH:mm&quot;</span> 格式的函数，需要支持时区参数<br></code></pre></td></tr></table></figure><p>这类需求交给 AI，效率提升是肉眼可见的。</p><h3 id="接口调用的样板代码"><a href="#接口调用的样板代码" class="headerlink" title="接口调用的样板代码"></a>接口调用的样板代码</h3><p>每次对接一个新的第三方接口，都要写一堆重复的模板：请求封装、参数构造、错误处理、响应解析。</p><p>这些代码结构高度相似，逻辑几乎没有创造性可言。AI 非常适合生成这类脚手架代码，你只需要核对字段对不对、错误分支有没有漏就行。</p><h3 id="单元测试用例"><a href="#单元测试用例" class="headerlink" title="单元测试用例"></a>单元测试用例</h3><p>测试代码是另一个 AI 的”主场”。</p><p>给 AI 一个函数，让它生成边界测试、异常用例、正常流程用例——它不会觉得枯燥，也不会”偷懒只写 happy path”（只要你在提示里强调边界）。</p><p>这部分对很多开发者来说是最容易被拖延的工作，AI 来做，省时省心。</p><h3 id="功能优化与重构"><a href="#功能优化与重构" class="headerlink" title="功能优化与重构"></a>功能优化与重构</h3><p>“帮我把这段代码的时间复杂度从 O(n²) 降到 O(n)”、”把这个 if-else 嵌套改成策略模式”——这类优化任务，方向是你决定的，细节让 AI 做。</p><p>你要做的是判断优化前后的行为是否一致，而不是自己手写每一行。</p><p><img src="/images/ai-dev/ai-assisted-development-workflow.png" alt="把需求拆成小任务，再交给 AI 生成、人工 Review、测试验证"></p><blockquote><p>AI 最适合处理边界清楚、可验证的小任务；任务越小，Review 成本越低。</p></blockquote><hr><h2 id="Git-是这套工作方式的底座"><a href="#Git-是这套工作方式的底座" class="headerlink" title="Git 是这套工作方式的底座"></a>Git 是这套工作方式的底座</h2><p>AI 辅助开发有一个隐患：你很容易在短时间内产生大量代码改动，然后发现某处出问题了，却不知道是哪个 AI 给的代码惹的祸。</p><p>解决这个问题的方式，是<strong>把 Git 的粒度做细</strong>。</p><p>我的原则是：<strong>一个函数、一个功能、一个 commit。</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">git commit -m <span class="hljs-string">&quot;feat: add formatTimestamp util function&quot;</span><br>git commit -m <span class="hljs-string">&quot;feat: add POST /api/order request wrapper&quot;</span><br>git commit -m <span class="hljs-string">&quot;test: add edge cases for formatTimestamp&quot;</span><br>git commit -m <span class="hljs-string">&quot;refactor: simplify order status check logic&quot;</span><br></code></pre></td></tr></table></figure><p>这样做有几个好处：</p><p><strong>随时可以回退到任何一步。</strong> AI 给了你一个”优化”，跑起来发现有问题，一条 <code>git revert</code> 或 <code>git reset</code> 就能精准回到上一个干净的状态，不用大范围 undo。</p><p><strong>Review 成本低。</strong> 每个 commit 只做一件事，改动量小，你能快速判断这段代码对不对。如果是大块 AI 生成代码一次性提交，Review 就变成了大海捞针。</p><p><strong>问题定位精准。</strong> 出 bug 了，<code>git log</code> 一眼看出来是哪个 commit 引入的，<code>git diff</code> 对比一下就知道改了什么。</p><p><strong>和 AI 的协作节奏对齐。</strong> 让 AI 生成一个工具函数 → Review → 测试通过 → commit，再让它生成下一个。这是一个健康的节奏，每一步你都是清醒的，而不是一堆代码塞进来然后发现乱了。</p><p><img src="/images/ai-dev/ai-assisted-development-git-guard.png" alt="Git 用细粒度 commit 把 AI 生成的每一步代码固定下来，方便回退和追踪"></p><blockquote><p>Git 的价值不是“最后备份一下”，而是让每一次 AI 生成都变成可审查、可回退的工程步骤。</p></blockquote><hr><h2 id="一个实际的工作流"><a href="#一个实际的工作流" class="headerlink" title="一个实际的工作流"></a>一个实际的工作流</h2><p>整理成一个流程大概是这样：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-number">1.</span> 确定你要做什么（你来决策）<br>        ↓<br><span class="hljs-number">2.</span> 拆解成小任务（你来拆分）<br>        ↓<br><span class="hljs-number">3.</span> 把其中重复性的部分交给 AI（AI 生成）<br>        ↓<br><span class="hljs-number">4.</span> Review 代码，确认逻辑和业务对齐（你来判断）<br>        ↓<br><span class="hljs-number">5.</span> 跑测试，验证行为正确（AI 可以辅助生成用例）<br>        ↓<br><span class="hljs-number">6.</span> <span class="hljs-keyword">commit</span>（一个功能一个 <span class="hljs-keyword">commit</span>，写清楚 message）<br>        ↓<br><span class="hljs-number">7.</span> 重复以上<br></code></pre></td></tr></table></figure><p>这个流程里，AI 是步骤 3 的执行者，你是步骤 1、2、4 的主导者。<strong>权责分明，出了问题知道找谁。</strong></p><hr><h2 id="几个容易踩的坑"><a href="#几个容易踩的坑" class="headerlink" title="几个容易踩的坑"></a>几个容易踩的坑</h2><p><strong>不要让 AI 一次性生成太多代码。</strong> 生成量越大，你 Review 的成本越高，遗漏问题的概率越大。宁可多来几次，每次只要一个小块。</p><p><strong>AI 给的命名和风格不一定符合你的项目规范。</strong> 记得在提示里说清楚，或者 Review 时统一调整。</p><p><strong>不要把业务判断交给 AI。</strong> “这个状态值应该是 1 还是 2”、”这个接口是否需要鉴权”——这些决策属于你，不属于 AI。</p><p><strong>commit message 要有意义。</strong> 不要 <code>fix bug</code>，不要 <code>update</code>，要能从 message 里看出这个 commit 做了什么事，方便以后 review 和回溯。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>AI 辅助开发的正确姿势，不是把项目交给 AI，而是把<strong>可以标准化、可以复用、可以验证</strong>的那部分工作外包给它，然后用 Git 把每一步的结果固定下来。</p><ul><li>AI 负责执行重复性工作：工具函数、接口样板、测试用例、代码优化</li><li>你负责做判断：需求拆解、逻辑 Review、边界确认、架构决策</li><li>Git 负责记录：细粒度 commit，随时可回退，清晰可追溯</li></ul><p>这样用下来，AI 是真的提效，而不是制造麻烦。</p><hr><p><em>如果你也在用 AI 做开发，欢迎在评论区聊聊你的用法和踩坑经验。</em></p>]]>
    </content>
    <id>https://blog.280303.xyz/posts/ai-assisted-development/</id>
    <link href="https://blog.280303.xyz/posts/ai-assisted-development/"/>
    <published>2026-05-29T07:46:00.000Z</published>
    <summary>AI 不是来取代开发者的，它只是一个极其高效的&quot;代码民工&quot;。让 AI 做它擅长的事——重复性工作，然后用 Git 把每一步牢牢钉住，才是真正的提效之道。</summary>
    <title>AI 辅助开发提效：正确姿势是让它当助手，而不是主角</title>
    <updated>2026-05-29T08:00:28.308Z</updated>
  </entry>
</feed>
