<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://yucol.top/feed.xml" rel="self" type="application/atom+xml" /><link href="https://yucol.top/" rel="alternate" type="text/html" /><updated>2026-07-17T02:10:10+00:00</updated><id>https://yucol.top/feed.xml</id><title type="html">Yucol</title><subtitle>个人的一个技术博客站点，主要用于记录个人在学习过程中遇到的技术问题及解决方法、以及一些比较有趣的事情。</subtitle><author><name>yucol</name></author><entry><title type="html">OAuth2（含密码哈希）与带 JWT 令牌的 Bearer 认证</title><link href="https://yucol.top/tech/OAuth2-JWT.html" rel="alternate" type="text/html" title="OAuth2（含密码哈希）与带 JWT 令牌的 Bearer 认证" /><published>2026-07-06T00:00:00+00:00</published><updated>2026-07-06T00:00:00+00:00</updated><id>https://yucol.top/tech/OAuth2-JWT</id><content type="html" xml:base="https://yucol.top/tech/OAuth2-JWT.html"><![CDATA[<h2 id="fastapi-教程示例">FastAPI 教程示例</h2>

<p><em>来源：<a href="https://fastapi.org.cn/tutorial/security/oauth2-jwt/">FastAPI 教程示例</a></em></p>

<blockquote>
  <p>这段代码是一个基于 <strong>FastAPI</strong> 框架实现的、完整的用户认证与授权系统。它使用了行业标准的 <strong>OAuth2</strong> 协议和 <strong>JWT (JSON Web Token)</strong> 技术来安全地验证用户身份，并保护后续的 API 接口。</p>
</blockquote>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Annotated</span>

<span class="kn">import</span> <span class="n">jwt</span>
<span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">Depends</span><span class="p">,</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">HTTPException</span><span class="p">,</span> <span class="n">status</span>
<span class="kn">from</span> <span class="n">fastapi.security</span> <span class="kn">import</span> <span class="n">OAuth2PasswordBearer</span><span class="p">,</span> <span class="n">OAuth2PasswordRequestForm</span>
<span class="kn">from</span> <span class="n">jwt.exceptions</span> <span class="kn">import</span> <span class="n">InvalidTokenError</span>
<span class="kn">from</span> <span class="n">pwdlib</span> <span class="kn">import</span> <span class="n">PasswordHash</span>
<span class="kn">from</span> <span class="n">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>

<span class="c1"># to get a string like this run:
# openssl rand -hex 32
</span><span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="sh">"</span><span class="s">09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7</span><span class="sh">"</span>
<span class="n">ALGORITHM</span> <span class="o">=</span> <span class="sh">"</span><span class="s">HS256</span><span class="sh">"</span>
<span class="n">ACCESS_TOKEN_EXPIRE_MINUTES</span> <span class="o">=</span> <span class="mi">30</span>


<span class="n">fake_users_db</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">johndoe</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">username</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">johndoe</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">full_name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">John Doe</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">email</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">johndoe@example.com</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">hashed_password</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">disabled</span><span class="sh">"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">}</span>


<span class="k">class</span> <span class="nc">Token</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">access_token</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">token_type</span><span class="p">:</span> <span class="nb">str</span>


<span class="k">class</span> <span class="nc">TokenData</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>


<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">disabled</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>


<span class="k">class</span> <span class="nc">UserInDB</span><span class="p">(</span><span class="n">User</span><span class="p">):</span>
    <span class="n">hashed_password</span><span class="p">:</span> <span class="nb">str</span>


<span class="n">password_hash</span> <span class="o">=</span> <span class="n">PasswordHash</span><span class="p">.</span><span class="nf">recommended</span><span class="p">()</span>

<span class="n">DUMMY_HASH</span> <span class="o">=</span> <span class="n">password_hash</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="sh">"</span><span class="s">dummypassword</span><span class="sh">"</span><span class="p">)</span>

<span class="n">oauth2_scheme</span> <span class="o">=</span> <span class="nc">OAuth2PasswordBearer</span><span class="p">(</span><span class="n">tokenUrl</span><span class="o">=</span><span class="sh">"</span><span class="s">token</span><span class="sh">"</span><span class="p">)</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">FastAPI</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">verify_password</span><span class="p">(</span><span class="n">plain_password</span><span class="p">,</span> <span class="n">hashed_password</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">password_hash</span><span class="p">.</span><span class="nf">verify</span><span class="p">(</span><span class="n">plain_password</span><span class="p">,</span> <span class="n">hashed_password</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">get_password_hash</span><span class="p">(</span><span class="n">password</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">password_hash</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">username</span> <span class="ow">in</span> <span class="n">db</span><span class="p">:</span>
        <span class="n">user_dict</span> <span class="o">=</span> <span class="n">db</span><span class="p">[</span><span class="n">username</span><span class="p">]</span>
        <span class="k">return</span> <span class="nc">UserInDB</span><span class="p">(</span><span class="o">**</span><span class="n">user_dict</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">authenticate_user</span><span class="p">(</span><span class="n">fake_db</span><span class="p">,</span> <span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">password</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">user</span> <span class="o">=</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">fake_db</span><span class="p">,</span> <span class="n">username</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
        <span class="nf">verify_password</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">DUMMY_HASH</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">False</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">verify_password</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="n">hashed_password</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="n">user</span>


<span class="k">def</span> <span class="nf">create_access_token</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">expires_delta</span><span class="p">:</span> <span class="n">timedelta</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
    <span class="n">to_encode</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="nf">copy</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">expires_delta</span><span class="p">:</span>
        <span class="n">expire</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="n">expires_delta</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">expire</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span>
    <span class="n">to_encode</span><span class="p">.</span><span class="nf">update</span><span class="p">({</span><span class="sh">"</span><span class="s">exp</span><span class="sh">"</span><span class="p">:</span> <span class="n">expire</span><span class="p">})</span>
    <span class="n">encoded_jwt</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">to_encode</span><span class="p">,</span> <span class="n">SECRET_KEY</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="n">ALGORITHM</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">encoded_jwt</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">get_current_user</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">oauth2_scheme</span><span class="p">)]):</span>
    <span class="n">credentials_exception</span> <span class="o">=</span> <span class="nc">HTTPException</span><span class="p">(</span>
        <span class="n">status_code</span><span class="o">=</span><span class="n">status</span><span class="p">.</span><span class="n">HTTP_401_UNAUTHORIZED</span><span class="p">,</span>
        <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Could not validate credentials</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">WWW-Authenticate</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Bearer</span><span class="sh">"</span><span class="p">},</span>
    <span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">SECRET_KEY</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">[</span><span class="n">ALGORITHM</span><span class="p">])</span>
        <span class="n">username</span> <span class="o">=</span> <span class="n">payload</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">sub</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">username</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">raise</span> <span class="n">credentials_exception</span>
        <span class="n">token_data</span> <span class="o">=</span> <span class="nc">TokenData</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">InvalidTokenError</span><span class="p">:</span>
        <span class="k">raise</span> <span class="n">credentials_exception</span>
    <span class="n">user</span> <span class="o">=</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">fake_users_db</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="n">token_data</span><span class="p">.</span><span class="n">username</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">user</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">raise</span> <span class="n">credentials_exception</span>
    <span class="k">return</span> <span class="n">user</span>


<span class="k">async</span> <span class="k">def</span> <span class="nf">get_current_active_user</span><span class="p">(</span>
    <span class="n">current_user</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">User</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_user</span><span class="p">)],</span>
<span class="p">):</span>
    <span class="k">if</span> <span class="n">current_user</span><span class="p">.</span><span class="n">disabled</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Inactive user</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">current_user</span>


<span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/token</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">login_for_access_token</span><span class="p">(</span>
    <span class="n">form_data</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">OAuth2PasswordRequestForm</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">()],</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Token</span><span class="p">:</span>
    <span class="n">user</span> <span class="o">=</span> <span class="nf">authenticate_user</span><span class="p">(</span><span class="n">fake_users_db</span><span class="p">,</span> <span class="n">form_data</span><span class="p">.</span><span class="n">username</span><span class="p">,</span> <span class="n">form_data</span><span class="p">.</span><span class="n">password</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span>
            <span class="n">status_code</span><span class="o">=</span><span class="n">status</span><span class="p">.</span><span class="n">HTTP_401_UNAUTHORIZED</span><span class="p">,</span>
            <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Incorrect username or password</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">WWW-Authenticate</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Bearer</span><span class="sh">"</span><span class="p">},</span>
        <span class="p">)</span>
    <span class="n">access_token_expires</span> <span class="o">=</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="n">ACCESS_TOKEN_EXPIRE_MINUTES</span><span class="p">)</span>
    <span class="n">access_token</span> <span class="o">=</span> <span class="nf">create_access_token</span><span class="p">(</span>
        <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">sub</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">.</span><span class="n">username</span><span class="p">},</span> <span class="n">expires_delta</span><span class="o">=</span><span class="n">access_token_expires</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="nc">Token</span><span class="p">(</span><span class="n">access_token</span><span class="o">=</span><span class="n">access_token</span><span class="p">,</span> <span class="n">token_type</span><span class="o">=</span><span class="sh">"</span><span class="s">bearer</span><span class="sh">"</span><span class="p">)</span>


<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/users/me/</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">read_users_me</span><span class="p">(</span>
    <span class="n">current_user</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">User</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_active_user</span><span class="p">)],</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">current_user</span>


<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/users/me/items/</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">read_own_items</span><span class="p">(</span>
    <span class="n">current_user</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">User</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_active_user</span><span class="p">)],</span>
<span class="p">):</span>
    <span class="k">return</span> <span class="p">[{</span><span class="sh">"</span><span class="s">item_id</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Foo</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">owner</span><span class="sh">"</span><span class="p">:</span> <span class="n">current_user</span><span class="p">.</span><span class="n">username</span><span class="p">}]</span>
</code></pre></div></div>

<h2 id="核心配置与模拟数据库">核心配置与模拟数据库</h2>

<p>这部分定义了生成和验证 JWT 令牌所需的关键参数，以及一个模拟的用户数据库。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 用于对 JWT 令牌进行签名和验证的密钥。在生产环境中，必须保密！
</span><span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="sh">"</span><span class="s">09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7</span><span class="sh">"</span>
<span class="c1"># 使用的加密算法，这里是 HMAC-SHA256
</span><span class="n">ALGORITHM</span> <span class="o">=</span> <span class="sh">"</span><span class="s">HS256</span><span class="sh">"</span>
<span class="c1"># 令牌有效期，30分钟
</span><span class="n">ACCESS_TOKEN_EXPIRE_MINUTES</span> <span class="o">=</span> <span class="mi">30</span>

<span class="c1"># 模拟的用户数据库，实际应用中应替换为真实数据库
</span><span class="n">fake_users_db</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">johndoe</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">username</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">johndoe</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">full_name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">John Doe</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">email</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">johndoe@example.com</span><span class="sh">"</span><span class="p">,</span>
        <span class="c1"># 这是用户密码的哈希值，而非明文密码
</span>        <span class="sh">"</span><span class="s">hashed_password</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">disabled</span><span class="sh">"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong>专业知识点：密码学安全</strong>
    <ul>
      <li><strong>SECRET_KEY</strong>: 用于对 JWT 令牌进行签名的密钥。服务端用它来“盖章”（签名）令牌，确保其未被篡改。这个密钥必须严格保密，一旦泄露，攻击者就可以伪造任意令牌。</li>
      <li><strong>ALGORITHM</strong>: 指定的 <code class="language-plaintext highlighter-rouge">HS256</code> 是一种对称加密算法，意味着签名和验证使用的是同一个密钥。对于分布式系统，有时会使用非对称算法（如 RSA）。</li>
    </ul>
  </li>
</ul>

<h2 id="数据模型定义-pydantic">数据模型定义 (Pydantic)</h2>

<p>这部分定义了 API 交互中使用的数据结构。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 令牌模型，用于定义登录成功后返回给客户端的数据格式
</span><span class="k">class</span> <span class="nc">Token</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">access_token</span><span class="p">:</span> <span class="nb">str</span>  <span class="c1"># 令牌字符串本身
</span>    <span class="n">token_type</span><span class="p">:</span> <span class="nb">str</span>    <span class="c1"># 令牌类型，通常是 "bearer"
</span>
<span class="c1"># 令牌数据模型，用于定义令牌中携带的用户信息的结构
</span><span class="k">class</span> <span class="nc">TokenData</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>  <span class="c1"># 这里只存放了用户名
</span>
<span class="c1"># 用户模型，定义了用户的基本信息
</span><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">disabled</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>

<span class="c1"># 用户数据库模型，继承自User，并增加了hashed_password字段
</span><span class="k">class</span> <span class="nc">UserInDB</span><span class="p">(</span><span class="n">User</span><span class="p">):</span>
    <span class="n">hashed_password</span><span class="p">:</span> <span class="nb">str</span>
</code></pre></div></div>

<ul>
  <li><strong>专业知识点：数据验证与序列化</strong>
    <ul>
      <li><strong>Pydantic Models</strong>: 这些类继承自 <code class="language-plaintext highlighter-rouge">BaseModel</code>，是 Pydantic 库的核心功能。它们用于：
1) <strong>数据验证</strong>：确保传入或传出的数据符合预定义的类型和结构。
2) <strong>数据序列化/反序列化</strong>：方便地将 Python 对象转换为 JSON 格式（发送给客户端），或从 JSON 格式转换为 Python 对象（接收自客户端）。</li>
    </ul>
  </li>
</ul>

<h2 id="核心安全工具函数">核心安全工具函数</h2>

<p>这部分包含了密码哈希处理和 JWT 令牌创建的核心逻辑。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 初始化密码哈希工具，使用推荐的算法（通常是 Argon2）
</span><span class="n">password_hash</span> <span class="o">=</span> <span class="n">PasswordHash</span><span class="p">.</span><span class="nf">recommended</span><span class="p">()</span>

<span class="c1"># 一个用于防止时序攻击的dummy哈希值
</span><span class="n">DUMMY_HASH</span> <span class="o">=</span> <span class="n">password_hash</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="sh">"</span><span class="s">dummypassword</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># 验证用户输入的密码是否与存储的哈希值匹配
</span><span class="k">def</span> <span class="nf">verify_password</span><span class="p">(</span><span class="n">plain_password</span><span class="p">,</span> <span class="n">hashed_password</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">password_hash</span><span class="p">.</span><span class="nf">verify</span><span class="p">(</span><span class="n">plain_password</span><span class="p">,</span> <span class="n">hashed_password</span><span class="p">)</span>

<span class="c1"># 对用户密码进行哈希处理，用于存储
</span><span class="k">def</span> <span class="nf">get_password_hash</span><span class="p">(</span><span class="n">password</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">password_hash</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>

<span class="c1"># 根据用户名从数据库中获取用户信息
</span><span class="k">def</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">username</span> <span class="ow">in</span> <span class="n">db</span><span class="p">:</span>
        <span class="n">user_dict</span> <span class="o">=</span> <span class="n">db</span><span class="p">[</span><span class="n">username</span><span class="p">]</span>
        <span class="k">return</span> <span class="nc">UserInDB</span><span class="p">(</span><span class="o">**</span><span class="n">user_dict</span><span class="p">)</span>

<span class="c1"># 认证用户：验证用户名和密码
</span><span class="k">def</span> <span class="nf">authenticate_user</span><span class="p">(</span><span class="n">fake_db</span><span class="p">,</span> <span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">password</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">user</span> <span class="o">=</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">fake_db</span><span class="p">,</span> <span class="n">username</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
        <span class="c1"># 关键安全实践：即使用户不存在，也进行一次哈希验证，防止时序攻击
</span>        <span class="nf">verify_password</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">DUMMY_HASH</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">False</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">verify_password</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="n">hashed_password</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="n">user</span>

<span class="c1"># 创建访问令牌（JWT）
</span><span class="k">def</span> <span class="nf">create_access_token</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">expires_delta</span><span class="p">:</span> <span class="n">timedelta</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
    <span class="n">to_encode</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="nf">copy</span><span class="p">()</span>
    <span class="c1"># 设置令牌过期时间
</span>    <span class="k">if</span> <span class="n">expires_delta</span><span class="p">:</span>
        <span class="n">expire</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="n">expires_delta</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">expire</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span>
    <span class="n">to_encode</span><span class="p">.</span><span class="nf">update</span><span class="p">({</span><span class="sh">"</span><span class="s">exp</span><span class="sh">"</span><span class="p">:</span> <span class="n">expire</span><span class="p">})</span>
    <span class="c1"># 使用密钥和算法对数据进行签名，生成令牌
</span>    <span class="n">encoded_jwt</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">to_encode</span><span class="p">,</span> <span class="n">SECRET_KEY</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="n">ALGORITHM</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">encoded_jwt</span>
</code></pre></div></div>

<ul>
  <li><strong>专业知识点：密码存储安全与时序攻击</strong>
    <ul>
      <li><strong>密码哈希 (Password Hashing)</strong>: 密码绝不能以明文形式存储。<code class="language-plaintext highlighter-rouge">pwdlib</code> 库使用 <strong>Argon2</strong> 算法（2015年密码哈希竞赛 winner），它通过加盐（salt）和多次迭代，极大增加了暴力破解的难度。</li>
      <li><strong>防止时序攻击 (Timing Attack)</strong>: 在 <code class="language-plaintext highlighter-rouge">authenticate_user</code> 函数中，如果用户不存在，代码仍会调用 <code class="language-plaintext highlighter-rouge">verify_password</code>。这是为了确保用户存在和不存在两种情况下的函数执行时间大致相同，防止攻击者通过精确测量响应时间来判断用户名是否存在。</li>
    </ul>
  </li>
  <li><strong>专业知识点：JWT (JSON Web Token)</strong>
    <ul>
      <li><strong>结构</strong>: 由三部分组成：Header（算法）、Payload（数据，如用户名、过期时间）和 Signature（签名）。</li>
      <li><strong>无状态认证</strong>: 服务端生成令牌后，自身无需保存会话（Session）。客户端在后续请求中携带此令牌，服务端通过验证签名即可确认用户身份，非常适合分布式和微服务架构。</li>
    </ul>
  </li>
</ul>

<h2 id="认证依赖项-fastapi-dependencies">认证依赖项 (FastAPI Dependencies)</h2>

<p>这部分是 FastAPI 安全机制的核心，通过依赖注入（Dependency Injection）实现认证逻辑的复用。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 初始化 OAuth2PasswordBearer，它会从请求头中提取令牌
</span><span class="n">oauth2_scheme</span> <span class="o">=</span> <span class="nc">OAuth2PasswordBearer</span><span class="p">(</span><span class="n">tokenUrl</span><span class="o">=</span><span class="sh">"</span><span class="s">token</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># 从令牌中获取当前用户信息
</span><span class="k">async</span> <span class="k">def</span> <span class="nf">get_current_user</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">oauth2_scheme</span><span class="p">)]):</span>
    <span class="n">credentials_exception</span> <span class="o">=</span> <span class="nc">HTTPException</span><span class="p">(</span>
        <span class="n">status_code</span><span class="o">=</span><span class="n">status</span><span class="p">.</span><span class="n">HTTP_401_UNAUTHORIZED</span><span class="p">,</span>
        <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Could not validate credentials</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">WWW-Authenticate</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Bearer</span><span class="sh">"</span><span class="p">},</span>
    <span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># 解码并验证令牌
</span>        <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">SECRET_KEY</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">[</span><span class="n">ALGORITHM</span><span class="p">])</span>
        <span class="n">username</span> <span class="o">=</span> <span class="n">payload</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">sub</span><span class="sh">"</span><span class="p">)</span>  <span class="c1"># "sub" 是 JWT 标准中代表主题（用户）的字段
</span>        <span class="k">if</span> <span class="n">username</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">raise</span> <span class="n">credentials_exception</span>
        <span class="n">token_data</span> <span class="o">=</span> <span class="nc">TokenData</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">InvalidTokenError</span><span class="p">:</span>
        <span class="k">raise</span> <span class="n">credentials_exception</span>
    <span class="n">user</span> <span class="o">=</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">fake_users_db</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="n">token_data</span><span class="p">.</span><span class="n">username</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">user</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">raise</span> <span class="n">credentials_exception</span>
    <span class="k">return</span> <span class="n">user</span>

<span class="c1"># 获取当前的活跃用户（非禁用状态）
</span><span class="k">async</span> <span class="k">def</span> <span class="nf">get_current_active_user</span><span class="p">(</span>
    <span class="n">current_user</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">User</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_user</span><span class="p">)],</span>
<span class="p">):</span>
    <span class="k">if</span> <span class="n">current_user</span><span class="p">.</span><span class="n">disabled</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Inactive user</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">current_user</span>
</code></pre></div></div>

<ul>
  <li><strong>专业知识点：依赖注入 (Dependency Injection)</strong>
    <ul>
      <li><strong><code class="language-plaintext highlighter-rouge">Depends</code></strong>: FastAPI 的核心特性。它允许您将一个函数（依赖项）注入到路由处理函数中。FastAPI 会自动在请求处理前执行这个依赖项，并将其返回值传递给处理函数。这实现了逻辑的解耦和复用。</li>
      <li><strong><code class="language-plaintext highlighter-rouge">oauth2_scheme</code></strong>: 一个内置的依赖项，它会自动从请求的 <code class="language-plaintext highlighter-rouge">Authorization</code> 头中提取 <code class="language-plaintext highlighter-rouge">Bearer</code> 类型的令牌字符串。</li>
    </ul>
  </li>
</ul>

<h2 id="api-路由-endpoints">API 路由 (Endpoints)</h2>

<p>这部分定义了两个核心 API：一个用于获取令牌，另一个是受保护的接口。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 登录端点，接受用户名和密码，返回访问令牌
</span><span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/token</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">login_for_access_token</span><span class="p">(</span>
    <span class="n">form_data</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">OAuth2PasswordRequestForm</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">()],</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Token</span><span class="p">:</span>
    <span class="n">user</span> <span class="o">=</span> <span class="nf">authenticate_user</span><span class="p">(</span><span class="n">fake_users_db</span><span class="p">,</span> <span class="n">form_data</span><span class="p">.</span><span class="n">username</span><span class="p">,</span> <span class="n">form_data</span><span class="p">.</span><span class="n">password</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span>
            <span class="n">status_code</span><span class="o">=</span><span class="n">status</span><span class="p">.</span><span class="n">HTTP_401_UNAUTHORIZED</span><span class="p">,</span>
            <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Incorrect username or password</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">WWW-Authenticate</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Bearer</span><span class="sh">"</span><span class="p">},</span>
        <span class="p">)</span>
    <span class="c1"># 创建令牌
</span>    <span class="n">access_token_expires</span> <span class="o">=</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="n">ACCESS_TOKEN_EXPIRE_MINUTES</span><span class="p">)</span>
    <span class="n">access_token</span> <span class="o">=</span> <span class="nf">create_access_token</span><span class="p">(</span>
        <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">sub</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">.</span><span class="n">username</span><span class="p">},</span> <span class="n">expires_delta</span><span class="o">=</span><span class="n">access_token_expires</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="nc">Token</span><span class="p">(</span><span class="n">access_token</span><span class="o">=</span><span class="n">access_token</span><span class="p">,</span> <span class="n">token_type</span><span class="o">=</span><span class="sh">"</span><span class="s">bearer</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># 受保护的端点，需要有效的令牌才能访问
</span><span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/users/me/</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">read_users_me</span><span class="p">(</span>
    <span class="n">current_user</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">User</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_active_user</span><span class="p">)],</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">current_user</span>

<span class="c1"># 另一个受保护的端点
</span><span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/users/me/items/</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">read_own_items</span><span class="p">(</span>
    <span class="n">current_user</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="n">User</span><span class="p">,</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_active_user</span><span class="p">)],</span>
<span class="p">):</span>
    <span class="k">return</span> <span class="p">[{</span><span class="sh">"</span><span class="s">item_id</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Foo</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">owner</span><span class="sh">"</span><span class="p">:</span> <span class="n">current_user</span><span class="p">.</span><span class="n">username</span><span class="p">}]</span>
</code></pre></div></div>

<ul>
  <li><strong>专业知识点：OAuth2 协议与 API 安全</strong>
    <ul>
      <li><strong><code class="language-plaintext highlighter-rouge">/token</code> 端点</strong>: 实现了 OAuth2 的 <strong>Resource Owner Password Credentials Grant</strong> 流程。客户端通过此接口提交用户名和密码，换取一个访问令牌（Access Token）。</li>
      <li><strong>受保护的端点</strong>: 通过在函数参数中声明 <code class="language-plaintext highlighter-rouge">Depends(get_current_active_user)</code>，FastAPI 会在执行函数前自动运行整个认证链：
1) 从请求头获取令牌 (<code class="language-plaintext highlighter-rouge">oauth2_scheme</code>)
2) 验证令牌并解析用户信息 (<code class="language-plaintext highlighter-rouge">get_current_user</code>)
3) 检查用户是否处于活跃状态 (<code class="language-plaintext highlighter-rouge">get_current_active_user</code>)</li>
      <li>如果任何一步失败，都会直接返回 401 或 400 错误，无法访问受保护的资源。</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="总结核心知识点图谱">总结：核心知识点图谱</h2>

<ol>
  <li><strong>FastAPI 框架</strong>: 利用其高性能、依赖注入（<code class="language-plaintext highlighter-rouge">Depends</code>）和自动文档生成等现代特性构建 API。</li>
  <li><strong>OAuth2 协议</strong>: 一个开放标准的授权框架，用于在不暴露用户密码的情况下，让客户端应用获取对用户资源的有限访问权限。</li>
  <li><strong>JWT (JSON Web Token)</strong>: 一种开放标准，用于在网络应用环境间安全地传输信息。它通过数字签名保证信息的完整性与认证性。</li>
  <li><strong>密码学安全</strong>: 使用强哈希算法（如 <strong>Argon2</strong>）存储密码，并通过设置强密钥（<code class="language-plaintext highlighter-rouge">SECRET_KEY</code>）和令牌有效期来保障系统安全。</li>
  <li><strong>安全最佳实践</strong>: 包括防止时序攻击、使用 HTTPS 传输令牌、将令牌存储在安全的地方（如 HTTP Only Cookie）等。</li>
</ol>]]></content><author><name>FastAPI</name></author><category term="tech" /><category term="FastAPI" /><category term="JWT" /><category term="OAuth2" /><summary type="html"><![CDATA[FastAPI 教程示例]]></summary></entry><entry><title type="html">FastAPI 响应状态码指南</title><link href="https://yucol.top/tech/response-status-code.html" rel="alternate" type="text/html" title="FastAPI 响应状态码指南" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://yucol.top/tech/response-status-code</id><content type="html" xml:base="https://yucol.top/tech/response-status-code.html"><![CDATA[<h1 id="fastapi-响应状态码指南">FastAPI 响应状态码指南</h1>

<p>在构建 RESTful API 时，正确使用 HTTP 状态码是至关重要的。它不仅是服务器与客户端通信的“语言”，更是衡量 API 设计是否规范、健壮的关键指标。FastAPI 作为一款现代化的 Web 框架，提供了极其便捷的方式来处理响应状态码。本文将深入解析 FastAPI 中关于响应状态码的一切，助你打造专业级的 API 接口。</p>

<hr />

<h2 id="为什么要关注-http-状态码">为什么要关注 HTTP 状态码？</h2>

<p>HTTP 状态码是服务器在处理客户端请求后返回的三位数字代码，它简洁明了地表达了请求的处理结果。一个设计良好的 API 应该利用状态码来传达语义，而不是仅仅依赖响应体中的自定义字段。</p>

<p>根据 FastAPI 官方文档的逻辑，状态码通常分为以下几类：</p>

<ul>
  <li><strong>100-199 (信息)</strong>：表示接收的请求正在处理中，通常很少直接使用。</li>
  <li><strong>200-299 (成功)</strong>：表示请求已成功接收、理解并接受。这是最常用的类别。
    <ul>
      <li><code class="language-plaintext highlighter-rouge">200 OK</code>：默认状态码，表示一切正常。</li>
      <li><code class="language-plaintext highlighter-rouge">201 Created</code>：表示资源创建成功，通常在 POST 请求后使用。</li>
      <li><code class="language-plaintext highlighter-rouge">204 No Content</code>：表示请求成功，但没有返回内容（常用于 DELETE 请求）。</li>
    </ul>
  </li>
  <li><strong>300-399 (重定向)</strong>：表示需要客户端采取进一步的操作才能完成请求。</li>
  <li><strong>400-499 (客户端错误)</strong>：表示客户端请求有误。
    <ul>
      <li><code class="language-plaintext highlighter-rouge">400 Bad Request</code>：通用的客户端错误。</li>
      <li><code class="language-plaintext highlighter-rouge">404 Not Found</code>：请求的资源不存在。</li>
    </ul>
  </li>
  <li><strong>500-599 (服务器错误)</strong>：表示服务器在处理请求时发生了错误，通常由框架或服务器自动返回。</li>
</ul>

<hr />

<h2 id="在-fastapi-中声明状态码">在 FastAPI 中声明状态码</h2>

<p>在 FastAPI 中，你可以通过路径操作装饰器（如 <code class="language-plaintext highlighter-rouge">@app.get()</code>）的 <code class="language-plaintext highlighter-rouge">status_code</code> 参数来声明响应状态码。这不仅会改变实际的 HTTP 响应状态，还会自动更新 OpenAPI 文档（Swagger UI），让 API 的使用者一目了然。</p>

<h3 id="基础用法使用数字">基础用法：使用数字</h3>

<p>你可以直接传递一个三位数的整数作为 <code class="language-plaintext highlighter-rouge">status_code</code> 的值。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">FastAPI</span><span class="p">()</span>

<span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/items/</span><span class="sh">"</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="mi">201</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">create_item</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="n">name</span><span class="p">}</span>
</code></pre></div></div>

<p><strong>说明</strong>：在上述代码中，我们创建了一个 POST 接口，当用户添加新项目时，服务器将返回 <code class="language-plaintext highlighter-rouge">201 Created</code> 状态码，而不是默认的 <code class="language-plaintext highlighter-rouge">200 OK</code>。</p>

<h3 id="进阶用法使用-status-模块推荐">进阶用法：使用 status 模块（推荐）</h3>

<p>硬编码数字虽然可行，但容易出错且不易记忆。FastAPI 提供了 <code class="language-plaintext highlighter-rouge">status</code> 模块，其中包含了所有标准的 HTTP 状态码常量，利用 IDE 的自动补全功能可以极大提高开发效率。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">status</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">FastAPI</span><span class="p">()</span>

<span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/items/</span><span class="sh">"</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">create_item</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="n">name</span><span class="p">}</span>
</code></pre></div></div>

<p>这种方式不仅代码可读性更强，还能有效避免拼写错误。</p>

<hr />

<h2 id="状态码对文档和响应体的影响">状态码对文档和响应体的影响</h2>

<p>FastAPI 是一个智能的框架，它会根据你选择的状态码自动推断文档信息：</p>

<ul>
  <li><strong>文档自动生成</strong>：当你指定 <code class="language-plaintext highlighter-rouge">status_code=201</code> 时，FastAPI 会在生成的 OpenAPI 文档中自动记录该接口可能返回 <code class="language-plaintext highlighter-rouge">201</code> 状态码。</li>
  <li><strong>响应体逻辑</strong>：某些状态码（如 <code class="language-plaintext highlighter-rouge">204 No Content</code>）明确规定不能包含响应体。FastAPI 会识别这些状态码，并在文档中声明该响应没有响应体，从而避免产生歧义。</li>
</ul>

<hr />

<h2 id="最佳实践与常见误区">最佳实践与常见误区</h2>

<ol>
  <li><strong>区分默认值与实际返回值</strong>
在路径操作函数中声明的 <code class="language-plaintext highlighter-rouge">status_code</code> 是“默认”值。在后续的高级用法中（如依赖注入或异常处理），你可能会根据业务逻辑动态改变最终返回的状态码。</li>
  <li>善用 <code class="language-plaintext highlighter-rouge">201 Created</code> 与 <code class="language-plaintext highlighter-rouge">204 No Content</code>
    <ul>
      <li>在创建资源（POST）时，优先使用 <code class="language-plaintext highlighter-rouge">201 Created</code>，这符合 RESTful 规范。</li>
      <li>在删除资源（DELETE）或更新资源（PUT/PATCH）且无需返回数据时，使用 <code class="language-plaintext highlighter-rouge">204 No Content</code> 可以减少网络传输的数据量。</li>
    </ul>
  </li>
  <li><strong>错误处理</strong>
虽然 FastAPI 会自动处理大部分服务器错误（5xx），但对于客户端错误（4xx），建议结合 <code class="language-plaintext highlighter-rouge">HTTPException</code> 使用：</li>
</ol>]]></content><author><name>yucol</name></author><category term="tech" /><category term="FastAPI" /><category term="响应状态码" /><summary type="html"><![CDATA[FastAPI 响应状态码指南]]></summary></entry><entry><title type="html">用纯前端优雅地解析并渲染 Shapefile：RxView</title><link href="https://yucol.top/tech/RxView.html" rel="alternate" type="text/html" title="用纯前端优雅地解析并渲染 Shapefile：RxView" /><published>2026-05-15T00:00:00+00:00</published><updated>2026-05-15T00:00:00+00:00</updated><id>https://yucol.top/tech/RxView</id><content type="html" xml:base="https://yucol.top/tech/RxView.html"><![CDATA[<h1 id="纯前端实现基于-vue3--leaflet-的变量施肥处方图渲染系统">纯前端实现基于 Vue3 + Leaflet 的变量施肥处方图渲染系统</h1>

<p>在农业空间数据处理中，变量施肥处方图通常以 Shapefile 格式进行流转。传统的 Web GIS 方案往往依赖后端的 GeoServer 或 PostGIS 进行数据解析和切片服务发布。但在最近推进白矖（Baixi）生态系统的开发中，我们需要为前端提供一个轻量级的预览工具——用户上传包含处方图的 ZIP 压缩包，前端直接在浏览器内完成解析、属性提取、色彩分级计算并叠加到高精度卫星底图上。</p>

<p>本文将复盘我们如何使用 Vue3、Leaflet 以及几个核心开源库，构建这套轻巧、流畅的纯前端 RxView 渲染系统，并分享在这个过程中遇到的一些经典交互“天坑”及解决思路。</p>

<h2 id="核心技术栈与选型">核心技术栈与选型</h2>

<p>在不依赖后端 GIS 引擎的前提下，前端独立完成空间数据的解析与渲染需要合理的工具链组合：</p>

<ul>
  <li><strong>框架与基础地图</strong>：Vue 3 (Composition API) + Leaflet。Leaflet 以其轻量级和极高的定制性，非常适合用来做单纯的数据叠加上图。</li>
  <li><strong>空间数据解析</strong>：<code class="language-plaintext highlighter-rouge">shpjs</code>。它可以直接在浏览器端将 Shapefile 的 ZIP 压缩包转换为标准的 GeoJSON 格式，这是连接业务数据与地图渲染的关键桥梁。</li>
  <li><strong>色彩分级与计算</strong>：<code class="language-plaintext highlighter-rouge">chroma-js</code>。处方图的核心在于将数值（如施肥量）映射为视觉色彩。利用该库可以非常方便地计算数据的极值、进行数值分段（离散化）并生成对应的色带。</li>
</ul>

<h2 id="核心业务流程与实现逻辑">核心业务流程与实现逻辑</h2>

<h3 id="1-文件的纯前端解析与智能选段">1. 文件的纯前端解析与智能选段</h3>

<p>系统的第一步是拦截原生文件上传，通过 <code class="language-plaintext highlighter-rouge">FileReader</code> 读取为 ArrayBuffer 后直接丢给 <code class="language-plaintext highlighter-rouge">shpjs</code> 处理。解析出 GeoJSON 后，面临的一个实际业务问题是：一个 Shapefile 中可能包含多个属性字段（例如面积、作物类型、各单一元素的推荐量等）。</p>

<p>为了减少用户的操作路径，我们在前端加入了一层简单的智能推断逻辑：在提取出所有数值型字段后，优先匹配包含业务特征的字段。例如在我们的实际应用场景中，很多农机只支持混配肥的单一变量作业，因此我们会在前端正则优先匹配 <code class="language-plaintext highlighter-rouge">mixed</code>、<code class="language-plaintext highlighter-rouge">混配</code> 等关键字作为默认渲染字段。如果找不到，再降级渲染纯地理边界。</p>

<h3 id="2-基于-chroma-js-的动态分级渲染">2. 基于 Chroma-js 的动态分级渲染</h3>

<p>拿到目标字段的数据后，需要将具体的施肥量数值转化为地图上的颜色。农业图层通常采用经典的“浅黄-浅绿-深绿”色谱。</p>

<p>渲染的难点在于不同地块的数值分布差异极大。如果直接按极值做线性映射，极少量的异常高值会导致大部分网格颜色区分度极低。因此，我们通过 <code class="language-plaintext highlighter-rouge">chroma.limits</code> 将有效数值分为最多 5 个区间（Class），并利用 <code class="language-plaintext highlighter-rouge">chroma.scale</code> 将基础色板扩展至对应的阶数，从而实现离散型分级图例。这样不仅视觉层次更鲜明，也符合农学上的分级指导习惯。</p>

<h3 id="3-高精度卫星底图的融合">3. 高精度卫星底图的融合</h3>

<p>传统的街道底图在农业场景下缺乏参考价值，我们需要让用户清晰地看到地块与实际地貌的贴合度。在此系统中，我们去除了 Leaflet 的默认控件，直接引入了 ArcGIS 的高清卫星影像服务（<code class="language-plaintext highlighter-rouge">World_Imagery</code>）作为底图，并将处方图的网格透明度设定为 <code class="language-plaintext highlighter-rouge">0.85</code>，以确保底图纹理若隐若现。</p>

<p><img src="https://img.yucol.uk/2026/05/ee739245a7342ed5c9b61d3eb3595b7d.png" alt="" />
<img src="https://img.yucol.uk/2026/05/824f0dcd6d5be1fd89c1d8a4a21df0fe.png" alt="" /></p>

<h2 id="实战踩坑如何消灭网格交互的高亮残影">实战踩坑：如何消灭网格交互的“高亮残影”</h2>

<p>在系统基本成型后，我们遇到了一个严重影响用户体验的交互问题。</p>

<p>为了方便用户查看数据，我们为每个网格通过 <code class="language-plaintext highlighter-rouge">onEachFeature</code> 绑定了 <code class="language-plaintext highlighter-rouge">mouseover</code> 和 <code class="language-plaintext highlighter-rouge">mouseout</code> 事件，并在其上叠加了 <code class="language-plaintext highlighter-rouge">sticky: true</code> 的原生跟随注记（Tooltip）。但在实际操作中：<strong>当用户按住某个网格拖动地图进行平移时，拖动路径上的所有网格都会被依次点亮，并留下一长串无法复原的高亮残影。</strong></p>

<p>起初我们以为是 Leaflet 的性能瓶颈，但经过排查，这实际上是浏览器原生拖拽机制与地图事件流的冲突：</p>

<ol>
  <li><strong>事件丢失</strong>：在地图拖动状态下，浏览器的焦点被地图容器的平移事件捕获，导致之前触发了 <code class="language-plaintext highlighter-rouge">mouseover</code> 的元素在鼠标移出时，无法稳定接收到 <code class="language-plaintext highlighter-rouge">mouseout</code> 恢复原始样式的指令。</li>
  <li><strong>DOM 滞后</strong>：跟随鼠标的 HTML 注记在快速平移时，DOM 重绘跟不上计算速度，甚至会被浏览器误识别为可拖拽对象。</li>
</ol>

<p><strong>最终的解决思路（事件穿透与状态拦截）：</strong></p>

<p>这绝非简单的 CSS 调整可以解决，我们需要从状态和样式两端进行双重拦截：</p>

<p><strong>第一步：JS 层面的拖拽状态拦截</strong>
我们在 Vue 的 <code class="language-plaintext highlighter-rouge">onMounted</code> 中监听了地图实例的 <code class="language-plaintext highlighter-rouge">dragstart</code> 和 <code class="language-plaintext highlighter-rouge">dragend</code> 事件，维护了一个全局的 <code class="language-plaintext highlighter-rouge">isMapDragging</code> 状态。在网格的 <code class="language-plaintext highlighter-rouge">mouseover</code> 事件触发时，首先判断该状态：如果地图正在被拖拽，则直接 <code class="language-plaintext highlighter-rouge">return</code> 拦截高亮逻辑。这就从源头上切断了残影的产生。</p>

<p><strong>第二步：CSS 层面的事件穿透与幽灵化</strong>
针对卡顿的注记，如果用 JS 强制在拖拽瞬间销毁它（<code class="language-plaintext highlighter-rouge">closeTooltip</code>），会导致深层的图层事件链断裂，直接导致地图彻底卡死无法拖拽。</p>

<p>因此我们采用了更优雅的“幽灵化”方案：
首先，通过 <code class="language-plaintext highlighter-rouge">pointer-events: none</code> 让鼠标事件强制穿透 Tooltip 注记，防止浏览器将其视作拖拽目标。
其次，在顶层容器动态绑定 <code class="language-plaintext highlighter-rouge">is-dragging</code> 类名。当处于拖拽状态时，利用 CSS 的 <code class="language-plaintext highlighter-rouge">opacity: 0</code> 和 <code class="language-plaintext highlighter-rouge">visibility: hidden</code> 瞬间隐藏所有注记。拖拽结束后状态解除，注记无缝恢复。</p>

<h2 id="结语">结语</h2>

<p>通过 Vue3 结合 Leaflet、shpjs 与 chroma-js，我们成功在浏览器端实现了一套不依赖后端的处方图渲染闭环。这种纯前端的处理方式极大地降低了后端的并发压力和存储成本，对于白矖这类需要支持多农场、高频次数据查看的生态系统来说，是一个高性价比的方案。目前，RxView已经成功地在白矖（Baixi）系统的生产环境中部署，为用户提供了稳定、高效的处方图渲染服务。</p>]]></content><author><name>yucol</name></author><category term="tech" /><category term="处方图" /><category term="Shapefile" /><category term="Leaflet" /><summary type="html"><![CDATA[纯前端实现基于 Vue3 + Leaflet 的变量施肥处方图渲染系统]]></summary></entry><entry><title type="html">实战 FastAPI + JWT：如何优雅实现单设备登录与账号有效期控制</title><link href="https://yucol.top/tech/JWT.html" rel="alternate" type="text/html" title="实战 FastAPI + JWT：如何优雅实现单设备登录与账号有效期控制" /><published>2026-05-12T00:00:00+00:00</published><updated>2026-05-12T00:00:00+00:00</updated><id>https://yucol.top/tech/JWT</id><content type="html" xml:base="https://yucol.top/tech/JWT.html"><![CDATA[<h1 id="实战-fastapi--jwt如何优雅实现单设备登录与账号有效期控制">实战 FastAPI + JWT：如何优雅实现单设备登录与账号有效期控制</h1>

<p>在开发多平台接入的综合管理系统时，统一的账号认证是整个架构的安全基石。对于前后端分离的项目，JSON Web Token (JWT) 是目前最主流的鉴权方案。</p>

<p>然而，标准的 JWT 存在一个天然的痛点：<strong>无状态</strong>。一旦服务端签发了 Token，在它自然过期之前，服务端很难直接主动废弃它。这会导致在处理“多端互踢（单设备登录）”以及“临时账号到期管控”时遇到麻烦。</p>

<p>本文将基于 FastAPI 和 PostgreSQL，详细拆解一套在真实生产环境中运行的权限控制方案。这套方案通过引入 <code class="language-plaintext highlighter-rouge">session_version</code>（会话版本号）机制，巧妙地融合了无状态的轻量感与有状态的强管控。</p>

<h2 id="一-核心设计思路">一 核心设计思路</h2>

<p>为了打破纯 JWT 的无状态限制，我们在数据库的用户表中引入一个核心字段：<code class="language-plaintext highlighter-rouge">session_version</code>（会话版本号）。</p>

<ol>
  <li><strong>登录埋点</strong>：用户每次成功登录时，服务端生成一个当前的时间戳作为新的 <code class="language-plaintext highlighter-rouge">session_version</code>，更新到数据库中，并将这个版本号一并打包写入 JWT 的 Payload 中。</li>
  <li><strong>鉴权校验</strong>：用户携带 Token 请求接口时，路由守卫不仅会校验 Token 的合法性和有效期，还会从数据库中查出该用户最新的 <code class="language-plaintext highlighter-rouge">session_version</code>。</li>
  <li><strong>互踢裁决</strong>：如果 Token 携带的版本号与数据库中的最新版本号<strong>不一致</strong>，说明该账号在其他地方进行了登录（旧 Token 被“顶号”），服务端直接拒绝请求。</li>
  <li><strong>安全退出</strong>：用户主动退出时，只需将数据库中的 <code class="language-plaintext highlighter-rouge">session_version</code> 重置为 0，该用户持有的所有旧 Token 即可瞬间失效。</li>
</ol>

<h2 id="二-基础安全与环境配置">二 基础安全与环境配置</h2>

<p>首先，集中管理我们的全局配置和密钥。在系统根目录下，我们需要定义 JWT 的加密密钥、算法以及 Token 的默认存活周期。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># core/config.py
</span><span class="kn">from</span> <span class="n">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>

<span class="c1"># JWT 与安全配置
</span><span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="sh">"</span><span class="s">your-super-secret-key</span><span class="sh">"</span>  <span class="c1"># 生产环境务必使用复杂随机字符串并从环境变量读取
</span><span class="n">ALGORITHM</span> <span class="o">=</span> <span class="sh">"</span><span class="s">HS256</span><span class="sh">"</span>
<span class="n">ACCESS_TOKEN_EXPIRE_MINUTES</span> <span class="o">=</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span> <span class="c1"># Token 有效期：24小时
</span>
</code></pre></div></div>

<h2 id="三-登录接口签发门禁卡与版本号更迭">三 登录接口：签发门禁卡与版本号更迭</h2>

<p>登录接口承担着多项校验任务。除了常规的密码比对（推荐使用 bcrypt），还需要处理账号的禁用状态以及临时账号的到期拦截。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># api/auth.py
</span><span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">APIRouter</span><span class="p">,</span> <span class="n">HTTPException</span><span class="p">,</span> <span class="n">Depends</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">import</span> <span class="n">time</span>
<span class="kn">from</span> <span class="n">core.database</span> <span class="kn">import</span> <span class="n">get_dash_db</span>
<span class="kn">from</span> <span class="n">core.security</span> <span class="kn">import</span> <span class="n">verify_password</span><span class="p">,</span> <span class="n">create_access_token</span>

<span class="n">router</span> <span class="o">=</span> <span class="nc">APIRouter</span><span class="p">()</span>

<span class="nd">@router.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/login</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">login</span><span class="p">(</span><span class="n">login_data</span><span class="p">:</span> <span class="n">LoginRequest</span><span class="p">):</span>
    <span class="n">conn</span> <span class="o">=</span> <span class="nf">get_dash_db</span><span class="p">()</span>
    <span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span>
    <span class="n">cursor</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="sh">"</span><span class="s">SELECT * FROM users WHERE username = %s</span><span class="sh">"</span><span class="p">,</span> <span class="p">(</span><span class="n">login_data</span><span class="p">.</span><span class="n">username</span><span class="p">,))</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">cursor</span><span class="p">.</span><span class="nf">fetchone</span><span class="p">()</span>
    
    <span class="c1"># 1. 验证用户和密码
</span>    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span> <span class="ow">or</span> <span class="ow">not</span> <span class="nf">verify_password</span><span class="p">(</span><span class="n">login_data</span><span class="p">.</span><span class="n">password</span><span class="p">,</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">password</span><span class="sh">'</span><span class="p">]):</span>
        <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">用户名或密码错误</span><span class="sh">"</span><span class="p">)</span>
        
    <span class="c1"># 2. 检查账户启用状态
</span>    <span class="k">if</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">:</span>
        <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">403</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">账户已被禁用，请联系管理员</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># 3. 检查账户有效期 (针对临时账号)
</span>    <span class="k">if</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">expire_time</span><span class="sh">'</span><span class="p">]:</span>
        <span class="n">current_utc_now</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">)</span>
        <span class="n">expire_time</span> <span class="o">=</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">expire_time</span><span class="sh">'</span><span class="p">]</span>
        <span class="c1"># 容错处理：确保时间对象具备时区信息，避免时区天真(naive)导致的比对异常
</span>        <span class="k">if</span> <span class="n">expire_time</span><span class="p">.</span><span class="n">tzinfo</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">expire_time</span> <span class="o">=</span> <span class="n">expire_time</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="n">tzinfo</span><span class="o">=</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">)</span>
   
        <span class="k">if</span> <span class="n">expire_time</span> <span class="o">&lt;</span> <span class="n">current_utc_now</span><span class="p">:</span>
            <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
            <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">403</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">账户已过期，请联系管理员</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># 4. 生成并写入新的会话版本号，实现“顶号”逻辑
</span>    <span class="n">session_version</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">())</span>
    <span class="n">cursor</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span>
        <span class="sh">"</span><span class="s">UPDATE users SET session_version = %s WHERE id = %s</span><span class="sh">"</span><span class="p">,</span> 
        <span class="p">(</span><span class="n">session_version</span><span class="p">,</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">])</span>
    <span class="p">)</span>
    <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
    <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

    <span class="c1"># 5. 将 session_version 塞进 JWT 的 payload 中
</span>    <span class="n">access_token_expires</span> <span class="o">=</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="n">ACCESS_TOKEN_EXPIRE_MINUTES</span><span class="p">)</span>
    <span class="n">access_token</span> <span class="o">=</span> <span class="nf">create_access_token</span><span class="p">(</span>
        <span class="n">data</span><span class="o">=</span><span class="p">{</span>
            <span class="sh">"</span><span class="s">sub</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">username</span><span class="sh">'</span><span class="p">],</span> 
            <span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">],</span> 
            <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">role</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">user</span><span class="sh">'</span><span class="p">),</span>
            <span class="sh">"</span><span class="s">session_version</span><span class="sh">"</span><span class="p">:</span> <span class="n">session_version</span>  
        <span class="p">},</span>
        <span class="n">expires_delta</span><span class="o">=</span><span class="n">access_token_expires</span>
    <span class="p">)</span>

    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">success</span><span class="sh">"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">access_token</span><span class="sh">"</span><span class="p">:</span> <span class="n">access_token</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">token_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">bearer</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">],</span> <span class="sh">"</span><span class="s">username</span><span class="sh">"</span><span class="p">:</span> <span class="n">user</span><span class="p">[</span><span class="sh">'</span><span class="s">username</span><span class="sh">'</span><span class="p">]}</span>
    <span class="p">}</span>

</code></pre></div></div>

<p>值得注意的是，在时间处理上，后端必须保持高度的一致性。Python 原生的 <code class="language-plaintext highlighter-rouge">datetime</code> 容易出现 Naive Time（无时区时间）与 Aware Time（带时区时间）比较报错的问题，使用 <code class="language-plaintext highlighter-rouge">replace(tzinfo=timezone.utc)</code> 强制对齐时区是一个极其稳妥的工程实践。</p>

<h2 id="四-路由守卫拦截器中的核心裁决">四 路由守卫：拦截器中的核心裁决</h2>

<p>有了带版本号的 Token，接下来需要在所有受保护的路由前加装一层守卫（FastAPI 的 Depends 机制）。</p>

<p>这里的关键在于：不仅要解密 Token，还要回源数据库比对版本号。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># core/security.py
</span><span class="kn">import</span> <span class="n">jwt</span>
<span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">HTTPException</span><span class="p">,</span> <span class="n">Depends</span>
<span class="kn">from</span> <span class="n">fastapi.security</span> <span class="kn">import</span> <span class="n">HTTPBearer</span><span class="p">,</span> <span class="n">HTTPAuthorizationCredentials</span>
<span class="kn">from</span> <span class="n">core.config</span> <span class="kn">import</span> <span class="n">SECRET_KEY</span><span class="p">,</span> <span class="n">ALGORITHM</span>
<span class="kn">from</span> <span class="n">core.database</span> <span class="kn">import</span> <span class="n">get_dash_db</span>

<span class="n">security</span> <span class="o">=</span> <span class="nc">HTTPBearer</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">get_current_user</span><span class="p">(</span><span class="n">credentials</span><span class="p">:</span> <span class="n">HTTPAuthorizationCredentials</span> <span class="o">=</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">security</span><span class="p">)):</span>
    <span class="n">token</span> <span class="o">=</span> <span class="n">credentials</span><span class="p">.</span><span class="n">credentials</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># 解密 Token 提取 Payload
</span>        <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">SECRET_KEY</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">[</span><span class="n">ALGORITHM</span><span class="p">])</span>
        <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">payload</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">token_version</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">payload</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">session_version</span><span class="sh">"</span><span class="p">)</span> 
        
        <span class="k">if</span> <span class="n">user_id</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">无效的认证凭证</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># 查库比对版本号
</span>        <span class="n">conn</span> <span class="o">=</span> <span class="nf">get_dash_db</span><span class="p">()</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span>
            <span class="n">cursor</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="sh">"</span><span class="s">SELECT session_version FROM users WHERE id = %s</span><span class="sh">"</span><span class="p">,</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,))</span>
            <span class="n">row</span> <span class="o">=</span> <span class="n">cursor</span><span class="p">.</span><span class="nf">fetchone</span><span class="p">()</span>
        <span class="k">finally</span><span class="p">:</span>
            <span class="n">cursor</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
            <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
        
        <span class="c1"># 核心裁决：如果数据库里的版本号跟 Token 里的对不上
</span>        <span class="k">if</span> <span class="ow">not</span> <span class="n">row</span> <span class="ow">or</span> <span class="n">row</span><span class="p">[</span><span class="sh">'</span><span class="s">session_version</span><span class="sh">'</span><span class="p">]</span> <span class="o">!=</span> <span class="n">token_version</span><span class="p">:</span>
            <span class="c1"># 抛出特定的 401 详情，前端可捕获 'KICKED_OUT' 来弹窗提示“您的账号已在别处登录”
</span>            <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">KICKED_OUT</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">payload</span>
        
    <span class="k">except</span> <span class="n">jwt</span><span class="p">.</span><span class="n">ExpiredSignatureError</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">登录已过期，请重新登录</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">jwt</span><span class="p">.</span><span class="n">InvalidTokenError</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">无效的 Token</span><span class="sh">"</span><span class="p">)</span>

</code></pre></div></div>

<p>前端在全局请求拦截器中，只需捕获 HTTP 401 且 <code class="language-plaintext highlighter-rouge">detail === 'KICKED_OUT'</code> 的响应，就可以平滑地清除本地缓存并弹出友好的互踢提示。</p>

<h2 id="五-安全退出物理级失效">五 安全退出：物理级失效</h2>

<p>在纯 JWT 架构中，退出登录通常只能依赖前端删除本地存储的 Token，服务端无能为力。但在我们的版本号机制下，服务端的退出接口变得极具掌控力。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># api/auth.py
</span><span class="nd">@router.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/logout</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">logout</span><span class="p">(</span><span class="n">current_user</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">=</span> <span class="nc">Depends</span><span class="p">(</span><span class="n">get_current_user</span><span class="p">)):</span>
    <span class="n">user_id</span> <span class="o">=</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">user_id</span><span class="p">:</span>
        <span class="n">conn</span> <span class="o">=</span> <span class="nf">get_dash_db</span><span class="p">()</span>
        <span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span>
        <span class="c1"># 将会话版本号置为 0，让该用户流通在外的所有 Token 瞬间失去校验资格
</span>        <span class="n">cursor</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="sh">"</span><span class="s">UPDATE users SET session_version = 0 WHERE id = %s</span><span class="sh">"</span><span class="p">,</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,))</span>
        <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
        <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">success</span><span class="sh">"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span> <span class="sh">"</span><span class="s">message</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">已安全退出</span><span class="sh">"</span><span class="p">}</span>

</code></pre></div></div>

<h2 id="六-总结与进阶思考">六 总结与进阶思考</h2>

<p>这套基于 <code class="language-plaintext highlighter-rouge">session_version</code> 的鉴权方案，完美解决了 JWT 无法主动失效的顽疾，同时也实现了对多平台登录状态的精确控制。</p>

<p><strong>性能考量：</strong>
当前的实现中，路由守卫每次拦截请求都需要进行一次 PostgreSQL 数据库查询。在常规的并发量下，数据库连接池加上单字段主键查询的速度完全可以胜任。但如果系统流量进一步膨胀，遇到高频次的空间数据接口或频繁的瓦片请求，鉴权层的查库操作可能会成为瓶颈。</p>

<p><strong>进阶优化方向：</strong>
可以将 <code class="language-plaintext highlighter-rouge">user_id : session_version</code> 的键值对缓存到 Redis 中。登录时更新 Redis，鉴权时直接读 Redis，从而将 I/O 成本降到最低，进一步释放系统在高并发环境下的性能潜力。</p>]]></content><author><name>yucol</name></author><category term="tech" /><category term="FastAPI" /><category term="JWT" /><category term="单设备登录" /><category term="账号有效期控制" /><summary type="html"><![CDATA[实战 FastAPI + JWT：如何优雅实现单设备登录与账号有效期控制]]></summary></entry><entry><title type="html">我的现场编年史：不只是听歌！</title><link href="https://yucol.top/life/concert.html" rel="alternate" type="text/html" title="我的现场编年史：不只是听歌！" /><published>2026-02-25T04:00:00+00:00</published><updated>2026-02-25T04:00:00+00:00</updated><id>https://yucol.top/life/concert</id><content type="html" xml:base="https://yucol.top/life/concert.html"><![CDATA[<h1 id="-我的演唱会流浪计划">🚀 我的演唱会流浪计划</h1>

<hr />
<h3 id="-no001--房东的猫---世界青年-2024巡回演唱会">🎵 NO.[001] | [房东的猫] - [世界/青年 2024巡回演唱会]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2024 年 06 月 29 日<br />
<strong>📍 地点：</strong> [北京] · [首都体育馆]<br />
<strong>🎫 座位：</strong> [ 看台山顶 ]
<strong>🏷️ 关键词：</strong> #第一次演唱会 #合唱 #太治愈了</p>
</blockquote>

<h4 id="1--content">1. 📜 Content</h4>

<p>到北京上学后，有机会看到了人生中的第一次演唱会。那个周末是最开心的时候（周六听房东的猫，周日看成果🥳），从此激起了对演唱会的热爱。<del>直到现在工作后的平淡无趣</del></p>

<h4 id="2--memory">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/03c35fc6b5402510d671a28704338784.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/b08933f2fec09709c5e4ce5c72c7b710.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/6dc51b2e507b801b2d4e1237202e5d50.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/4f880cf7da00e9bf01004dac6542c814.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/ecfa47753c2c0d15679f02a36afb924c.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/eb23d7c5e5d14d200732fbcd4574e803.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/6af2fb0d633f677461f94a63cc649083.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/52158116f13f17fac1f55f5cc1313a47.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/04d872bbc0a16cfc83e5431853dbd6e4.webp" alt="" /></p>

<h4 id="3--个人评分">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>歌手状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />
<h3 id="-no002--成果---海王星飞船">🎵 NO.[002] | [成果] - [海王星飞船]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2024 年 06 月 30 日<br />
<strong>📍 地点：</strong> [北京] · [晓剧场]<br />
<strong>🎫 座位：</strong> [ 3排 16号 ]<br />
<strong>🏷️ 关键词：</strong> #音乐 #舞台剧 #合影（线下真实版）</p>
</blockquote>

<h4 id="1--content-1">1. 📜 Content</h4>

<p>狗哥的第一次舞台剧（成年儿童版😄），虽然不是演唱会，但却是第一次线下见面。</p>

<h4 id="2--memory-1">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/09010902d25eba6b5e0d4c03d192dc18.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/3a61aeeadff176eade7bf7c7a1ffafbe.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/71176807134ee73c254b0ae91fe8f165.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/25f11ae3934295c98afea56033a64f2c.webp" alt="" /></p>

<h4 id="3--个人评分-1">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>演员状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />

<h3 id="-no003--夏日入侵企画---温暖人心的冬日聚会-不插电特别专场">🎵 NO.[003] | [夏日入侵企画] - [温暖人心的冬日聚会-不插电特别专场]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2024 年 12 月 26 日<br />
<strong>📍 地点：</strong> [北京] · [福浪LIVEHOUSE-福]<br />
<strong>🎫 座位：</strong> [ Live House ]
<strong>🏷️ 关键词：</strong> #欢乐 #蹦迪 #近距离</p>
</blockquote>

<h4 id="1--content-2">1. 📜 Content</h4>

<p>首次参加live house，夏企的魅力太足了，从开头蹦到结尾！我去的比较早，所以抢到了比较靠前的位置 <del>（早去早罚站）</del>。</p>

<h4 id="2--memory-2">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/d30bfd4a68f4dc0b0ae8ea722758091e.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/9ce5697d3c3a05e611248f2a1756fb61.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/a07dfafb65d1067e68bacb9cb98fb3e2.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/9214b0d4ecb795c553c7a016c6e0502f.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/0423acd25e4b2249b3d44caadcbe9d6a.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/510149ef4a309514aba2e355bde0f8e8.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/fb7ee9c7157b2475f11d861938ce9642.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/e550a42cc6d94c5b7866bfebdd73e16e.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/15218fcce6159777c7985751a353f7b6.webp" alt="" /></p>

<h4 id="3--个人评分-2">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>歌手状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />

<h3 id="-no004--双笙---经过时你我相遇">🎵 NO.[004] | [双笙] - [经过时你我相遇]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2024 年 12 月 30 日<br />
<strong>📍 地点：</strong> [北京] · [北京展览馆剧场]<br />
<strong>🎫 座位：</strong> [ 一层 27排 74号 ]
<strong>🏷️ 关键词：</strong> #国风 #老粉</p>
</blockquote>

<h4 id="1--content-3">1. 📜 Content</h4>

<p>大学时特别喜欢<strong>双笙</strong>的歌曲，声音清甜，歌曲时而温柔，时而清脆。这次演唱会开到了北京，终于有机会可以现场听她唱的歌。</p>

<p>现场的人数没有满座，但是也有很多人来捧场，上次关注双笙的时间我已经记不清了，我的记忆里还是她之前唱过的歌，所以演唱会上很多歌我都不太熟悉，算是个小遗憾吧。</p>

<h4 id="2--memory-3">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/57e8bbeebc68f673226fc632cdf1af5e.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/7dc997c6941d520ae27eaa5c99089d9d.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/9d43b58f0aabe1f2cf45910caa1d968f.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/bf662e347ad4267f56099d337ac1e3d1.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/5cbd4184524954e55f8c0aa403f5a525.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/3d11d108aab3971c2f81224e7eedbd07.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/9b495142e3b4af457bd9bdcd9123036a.webp" alt="" /></p>

<h4 id="3--个人评分-3">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐</li>
  <li><strong>歌手状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />

<h3 id="-no005--张杰---未live---开往1982">🎵 NO.[005] | [张杰] - [未·LIVE - 开往1982]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2025 年 04 月 19 日<br />
<strong>📍 地点：</strong> [北京] · [国家体育场-鸟巢]<br />
<strong>🎫 座位：</strong> [ G区 529通道 五层 19排 25号 ]
<strong>🏷️ 关键词：</strong> #逆战 #哪个男孩子不想现场听张杰唱逆战 #直接给到夯</p>
</blockquote>

<h4 id="1--content-4">1. 📜 Content</h4>

<p>遥想12年的夏天，我拥有了人生中的第一台电脑，第一件事就是把《逆战》（游戏）下载下来。无他，因为<strong>逆战</strong>这首歌在当时就是现在小学生心目中的<strong>孤勇者</strong>。</p>

<p><strong>张杰</strong>的演唱会真的太顶了，不论是氛围、嗓音、歌曲、编舞、音效还是灯光都是无敌的存在！</p>

<p>还有个小插曲，演唱会结束后发现我的一位初中同学和另一位朋友竟然和我一块在现场。</p>

<h4 id="2--memory-4">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/b28c1685eb4743eb21ab2986f0750d45.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/842b5b707b6db0d659e13a7d292d19b5.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/e99d4ffbce3959249a29e2a1045b4c42.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/fcc01ffc42829688b50d782019e66d6b.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/059de7b30040024af60e69b08e08bfb7.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/a7d38cca39226b693c41a854492baf27.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/aff90d71d1257b3f8520bd1b93f4010e.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/4b18796a1d5883e642d1f1ea8e291307.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/f675fd3bdd93dd46098a61e6e71990fd.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/e29cb6bd399aafca0fb923fe7606e1d2.webp" alt="" /></p>

<h4 id="3--个人评分-4">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>歌手状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />

<h3 id="-no006--凤凰传奇---吉祥如意">🎵 NO.[006] | [凤凰传奇] - [吉祥如意]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2025 年 05 月 24 日<br />
<strong>📍 地点：</strong> [北京] · [国家体育场-鸟巢]<br />
<strong>🎫 座位：</strong> [ H区 635通道 六层 26排 7~10号 ]
<strong>🏷️ 关键词：</strong> #组队看演唱会 #蹦迪</p>
</blockquote>

<h4 id="1--content-5">1. 📜 Content</h4>

<p><strong>这一次，四人终于抵达了他们忠诚的鸟巢！</strong></p>

<h4 id="2--memory-5">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/a54d93c781ef47f0f8ddb1637ebb7a2b.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/94c0b136f85d70fb02f49b208070b8d5.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/f2ed607b6e326f2e5b7fe388a869bce0.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/4a0e4c8c8e75c6e242ebb87f62eaf6e5.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/07b83a6cf11f23014e9c68704b1b2baa.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/f657397dad165ca1f5e1d670ef46e4a4.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/5b56bd318c476f65dce021c7b070b500.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/5fdc7c1d644c05f1148a86bbcdf9e8fb.webp" alt="" /></p>

<h4 id="3--个人评分-5">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>歌手状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />

<h3 id="-no007--房东的猫---世界青年---2025巡回演唱会--南京站">🎵 NO.[007] | [房东的猫] - [世界/青年 - 2025巡回演唱会- 南京站]</h3>

<blockquote>
  <p><strong>📅 时间：</strong> 2025 年 06 月 14 日<br />
<strong>📍 地点：</strong> [南京] · [南京奥体中心国缘V9体育馆]<br />
<strong>🎫 座位：</strong> [ 看台六十五区 看台6F 3排 4座 ]
<strong>🏷️ 关键词：</strong> #毕业旅行</p>
</blockquote>

<h4 id="1--content-6">1. 📜 Content</h4>

<p>临近毕业的阶段，买到了南京站的🎫（卡点抢的票，结果后面打折卖，可恶的大麦！）。下午去参加粉丝活动的时候认识了一位南林大的新朋友，从而使这段旅程没有太过“安静”。晚上演唱会结束后下起了暴雨⛈️，没有带伞，而免费雨披也送完了，所以就做了一次“雨中追雨的少年”，回到酒店后衣服都湿透了。</p>

<p>之后在南京玩了三天，美丽的南京，去了还想去。“旅途”最后两天去了上海找我同学，25岁第一次踏上了我曾梦想去的城市-上海。</p>

<p><strong>无论在何处，祝你美梦成真</strong></p>

<h4 id="2--memory-6">2. 📸 Memory</h4>

<p><img src="https://img.yucol.uk/2026/02/ad24a56a290d4ba114b755465661b09c.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/2dd6aa01f510c8f605d047e7eebe001d.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/ab9644a46148cdcdd090204fbb04c5fd.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/224605f108ac8205dd767e3e2648de3a.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/7cd6790b2ac8938c8946256479b8900c.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/7a6ee0db182e98c183e92d16f8789ac4.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/9e7136d0f5a64e093ad0de3ef28b3e00.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/7c16fa33c0b4fa5d95c7426d335a7dbf.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/e4283321f950066abf9d43c24fdd106b.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/51341cf870e66bd1fdf1bd95a36b4795.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/1beaa9477f2f0363afb028cec83299f5.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/c92e27acfc7c79442ea914015072e09c.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/71e01ee963fa31b66f11df3247abed7a.webp" alt="" />
<img src="https://img.yucol.uk/2026/02/4ddd9f16a8e1802cf77421aa868e0b05.webp" alt="" /></p>

<h4 id="3--个人评分-6">3. ⭐ 个人评分</h4>

<ul>
  <li><strong>曲目编排：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>舞美视听：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>歌手状态：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>推荐指数：</strong> ⭐⭐⭐⭐⭐</li>
  <li><strong>综合评分：</strong> ⭐⭐⭐⭐⭐</li>
</ul>

<hr />]]></content><author><name>yucol</name></author><category term="life" /><category term="演唱会" /><category term="房东的猫" /><category term="成果" /><category term="夏日入侵企画" /><category term="双笙" /><category term="张杰" /><category term="凤凰传奇" /><summary type="html"><![CDATA[🚀 我的演唱会流浪计划]]></summary></entry><entry><title type="html">从零到一：阿里云 200M 带宽服务器的全栈基建（1Panel + Docker + GitHub Actions）</title><link href="https://yucol.top/server/server-setup.html" rel="alternate" type="text/html" title="从零到一：阿里云 200M 带宽服务器的全栈基建（1Panel + Docker + GitHub Actions）" /><published>2026-02-25T00:00:00+00:00</published><updated>2026-02-25T00:00:00+00:00</updated><id>https://yucol.top/server/server-setup</id><content type="html" xml:base="https://yucol.top/server/server-setup.html"><![CDATA[<h1 id="1-前言为什么放弃手动搬运">1 前言：为什么放弃“手动搬运”？</h1>

<p>在拥有了自己的服务器后，第一反应是手动上传文件。但这存在三个弊端：</p>

<ul>
  <li><strong>效率低</strong>：改一个字也要开一遍 FTP。</li>
  <li><strong>易出错</strong>：容易漏传或传错目录。</li>
  <li><strong>无版本控制</strong>：无法回溯历史。
本篇记录如何利用 <strong>GitHub Actions</strong> 打造一条自动化流水线，实现“本地代码一推，服务器即刻更新”。</li>
</ul>

<hr />

<h1 id="2-环境说明">2 环境说明</h1>

<ul>
  <li><strong>服务器</strong>：阿里云 2核2G / 200M 带宽 / Ubuntu 系统。</li>
  <li><strong>运维面板</strong>：1Panel（基于 Docker 的现代化管理面板）。</li>
  <li><strong>静态引擎</strong>：Jekyll (Ruby 3.4.4)。</li>
  <li><strong>部署通道</strong>：SSH + Rsync。</li>
</ul>

<hr />

<h1 id="3-核心步骤详解">3 核心步骤详解</h1>

<h2 id="第一步服务器基建-1panel">第一步：服务器基建 (1Panel)</h2>

<ol>
  <li>安装 1Panel 并在应用商店安装 <strong>OpenResty</strong>（Nginx 的增强版）。</li>
  <li>在“网站”菜单下创建一个静态网站。</li>
</ol>

<ul>
  <li><strong>路径</strong>：<code class="language-plaintext highlighter-rouge">/opt/1panel/apps/openresty/openresty/www/sites/test/index</code>（此路径为后期同步的目标 <code class="language-plaintext highlighter-rouge">TARGET</code>）。</li>
</ul>

<ol>
  <li>确保服务器防火墙已开启 80/443 端口，以及 SSH 通讯所需的 22 端口。</li>
</ol>

<h2 id="第二步建立信任关系-ssh-key">第二步：建立“信任关系” (SSH Key)</h2>

<p>为了让 GitHub 机器人能直接登录我们的服务器，需要配置免密登录：</p>

<ol>
  <li><strong>本地生成密钥对</strong>：
<code class="language-plaintext highlighter-rouge">ssh-keygen -t rsa -b 4096 -f id_rsa_github</code></li>
  <li><strong>分发公钥</strong>：将 <code class="language-plaintext highlighter-rouge">id_rsa_github.pub</code> 的内容追加到服务器的 <code class="language-plaintext highlighter-rouge">/root/.ssh/authorized_keys</code> 中。</li>
  <li><strong>权限加固</strong>：确保 <code class="language-plaintext highlighter-rouge">.ssh</code> 目录权限为 <code class="language-plaintext highlighter-rouge">700</code>，<code class="language-plaintext highlighter-rouge">authorized_keys</code> 权限为 <code class="language-plaintext highlighter-rouge">600</code>。</li>
</ol>

<h2 id="第三步配置-github-secrets">第三步：配置 GitHub Secrets</h2>

<p>在 GitHub 仓库的 <code class="language-plaintext highlighter-rouge">Settings -&gt; Secrets and variables -&gt; Actions</code> 中配置四个关键变量：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">REMOTE_HOST</code>: 服务器公网 IP。</li>
  <li><code class="language-plaintext highlighter-rouge">REMOTE_USER</code>: <code class="language-plaintext highlighter-rouge">root</code>。</li>
  <li><code class="language-plaintext highlighter-rouge">SSH_PRIVATE_KEY</code>: <code class="language-plaintext highlighter-rouge">id_rsa_github</code> 文件里的完整私钥内容。</li>
  <li><code class="language-plaintext highlighter-rouge">REMOTE_TARGET</code>: 服务器上的静态目录路径。</li>
</ul>

<h2 id="第四步编写自动化流水线-yaml">第四步：编写自动化流水线 (YAML)</h2>

<p>在 <code class="language-plaintext highlighter-rouge">.github/workflows/</code> 下创建 <code class="language-plaintext highlighter-rouge">jekyll.yml</code>。核心逻辑是利用 <code class="language-plaintext highlighter-rouge">easingthemes/ssh-deploy</code> 插件执行 <code class="language-plaintext highlighter-rouge">rsync</code>。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 关键配置摘要</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to Aliyun Server</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">easingthemes/ssh-deploy@v5.1.0</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">SSH_PRIVATE_KEY</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">ARGS</span><span class="pi">:</span> <span class="s2">"</span><span class="s">-rltzvi</span><span class="nv"> </span><span class="s">--delete"</span> <span class="c1"># 增量同步，删除服务器多余文件</span>
    <span class="na">SOURCE</span><span class="pi">:</span> <span class="s2">"</span><span class="s">_site/"</span>         <span class="c1"># Jekyll 构建后的产物目录</span>
    <span class="na">REMOTE_HOST</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">REMOTE_USER</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">TARGET</span><span class="pi">:</span> <span class="s">$</span>

</code></pre></div></div>

<hr />

<h1 id="4-外部资源集成">4 外部资源集成：</h1>

<p>为了追求极致的性能和稳定性，我并没有将所有功能都死磕在自己的服务器上，而是引入了更成熟的云原生方案：</p>

<h2 id="a-评论系统waline--vercel--neon">A. 评论系统：Waline + Vercel + Neon</h2>

<p>为了不让评论数据的存储和处理占用阿里云的 2G 内存，我采用了 <strong>Serverless（无服务器）</strong> 架构：</p>

<ul>
  <li><strong>后端部署 (Vercel)</strong>：利用 Vercel 的免费额度托管 Waline 后端程序。</li>
  <li><strong>数据库 (Neon)</strong>：所有的评论内容存储在 Neon 的云数据库中，安全且免费。</li>
  <li><strong>集成方式</strong>：在 Jekyll 模板中引入 Waline 的 JS 脚本，通过 API 与 Vercel 通信。</li>
  <li><strong>深度心得</strong>：这种方案实现了“数据随人走”，即便我以后重装服务器系统，评论数据也不会丢失。</li>
</ul>

<h2 id="b-极致图床cloudflare-r2--picgo">B. 极致图床：Cloudflare R2 + PicGo</h2>

<p>虽然服务器有 200M 带宽，但为了节省宝贵的公网流量以及应对未来可能的并发访问，我搭建了专业的对象存储图床：</p>

<ul>
  <li><strong>存储底座 (Cloudflare R2)</strong>：利用 Cloudflare 的 R2 存储（S3 兼容模式），免流量费且自带全球 CDN 加速。</li>
  <li><strong>自动化工具 (PicGo)</strong>：在本地配置 PicGo 插件，实现“截图 -&gt; 上传 -&gt; 自动生成 Markdown 链接”的一键流。</li>
  <li><strong>安全加固 (V2 签名)</strong>：使用 V2 签名机制确保上传接口的安全，防止图床被恶意盗刷。</li>
</ul>

<hr />

<h1 id="5-未来扩展">5 未来扩展：</h1>

<p>目前的基建仅完成了博客的部署。后续计划：</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>后端 API</strong>：在 1Panel 中使用 Docker 部署 Python (FastAPI) 容器。</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>移动端联调</strong>：Kotlin 开发的安卓 App 通过 HTTP 请求调用服务器 IP 上的 Python 接口，实现数据上云。</li>
</ul>]]></content><author><name>yucol</name></author><category term="server" /><category term="服务器" /><category term="GitHub Actions" /><category term="blog" /><summary type="html"><![CDATA[1 前言：为什么放弃“手动搬运”？]]></summary></entry><entry><title type="html">我的第一款酒局神器App“摇了么”</title><link href="https://yucol.top/app/PartyDice.html" rel="alternate" type="text/html" title="我的第一款酒局神器App“摇了么”" /><published>2026-02-22T13:20:00+00:00</published><updated>2026-02-22T13:20:00+00:00</updated><id>https://yucol.top/app/PartyDice</id><content type="html" xml:base="https://yucol.top/app/PartyDice.html"><![CDATA[<h1 id="-我的第一款酒局神器app摇了么诞生记">🚀 我的第一款酒局神器App“摇了么”诞生记！</h1>

<blockquote>
  <p>源代码、安装包及效果图在文章结尾处。</p>
</blockquote>

<p>一直想做一个既好玩又实用的东西。终于，在AI的帮助下，我的第一款 Android 独立应用——<strong>“摇了么”</strong>，正式诞生啦！</p>

<p>这是一款专为酒局、聚会打造的摇骰子小工具。虽然市面上有很多类似的小程序，但作为一个强迫症+体验控，决定亲自操刀，给它注入真正的“灵魂”。</p>

<h3 id="-为什么叫摇了么">🎲 为什么叫“摇了么”？</h3>

<p>在酒桌上，最常听到的一句话可能就是“该你了，摇了么？”。这个名字既是对朋友的催促，也是这款 App 核心交互的体现——别点屏幕了，直接拿起手机摇吧！</p>

<h3 id="-核心亮点把细节死磕到底">✨ 核心亮点：把细节死磕到底</h3>

<p>在体验上主要有以下亮点：</p>

<ul>
  <li><strong>告别反人类UI，单手掌控全局：</strong>
抛弃了早期版本中需要呼出软键盘来输入骰子数量的繁琐设计。利用线性布局（LinearLayout）的权重属性，将核心视觉区（骰子展示）固定在中上方，而在底部手指出没的最舒适区域，加入了丝滑的 <code class="language-plaintext highlighter-rouge">-</code> 和 <code class="language-plaintext highlighter-rouge">+</code> 按钮。1到15个骰子，单手一秒切换。</li>
  <li><strong>注入灵魂的“物理外挂”（摇一摇）：</strong>
酒局游戏，怎么能只用手指点按钮呢？调用了 Android 的底层硬件——<strong>加速度传感器 (Accelerometer)</strong>。通过计算 X、Y、Z 三个维度的综合重力加速度，只要你像握着真实骰盅一样用力甩动手机，代码就会自动捕捉你的动作并触发掷骰子！</li>
  <li><strong>沉浸式的声感与触觉反馈：</strong>
为了模拟真实的物理反馈，加入了双重感官刺激：
    <ol>
      <li><strong>听觉</strong>：接入 <code class="language-plaintext highlighter-rouge">MediaPlayer</code>，摇晃时伴随清脆的“哗啦哗啦”骰子碰撞声。</li>
      <li><strong>触觉</strong>：调用 <code class="language-plaintext highlighter-rouge">Vibrator</code> 震动马达。在骰子滚动的 1 秒内，配合动画发出极其高频的轻微震动，停住的瞬间来一次重震。闭上眼睛，你手里握着的仿佛就是个真骰盅！</li>
    </ol>
  </li>
  <li><strong>安全纯净无广告：</strong>
代码完全开源，没有任何联网和收集隐私信息的行为；界面干净整洁，没有任何广告及干扰信息！</li>
</ul>

<hr />

<h3 id="-核心代码-show-me-the-code">💻 核心代码 (Show Me The Code)</h3>

<p>这里分享一下“摇了么”最核心的两个逻辑实现：</p>

<h4 id="1-如何让手机听懂摇晃传感器逻辑">1. 如何让手机听懂“摇晃”？(传感器逻辑)</h4>

<p>这里用到了初中的物理和数学知识 。手机的加速度传感器会实时返回 X、Y、Z 三个轴的受力。们算出它们除以地球重力加速度后的 G 力，然后利用勾股定理  算出综合受力：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 传感器检测到手机运动时自动调用</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onSensorChanged</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="nc">SensorEvent</span><span class="p">?)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">event</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">isRolling</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">x</span> <span class="p">=</span> <span class="n">event</span><span class="p">.</span><span class="n">values</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
        <span class="kd">val</span> <span class="py">y</span> <span class="p">=</span> <span class="n">event</span><span class="p">.</span><span class="n">values</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
        <span class="kd">val</span> <span class="py">z</span> <span class="p">=</span> <span class="n">event</span><span class="p">.</span><span class="n">values</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>

        <span class="c1">// 计算加速度，除以地球重力加速度得出 G 力</span>
        <span class="kd">val</span> <span class="py">gX</span> <span class="p">=</span> <span class="n">x</span> <span class="p">/</span> <span class="nc">SensorManager</span><span class="p">.</span><span class="nc">GRAVITY_EARTH</span>
        <span class="kd">val</span> <span class="py">gY</span> <span class="p">=</span> <span class="n">y</span> <span class="p">/</span> <span class="nc">SensorManager</span><span class="p">.</span><span class="nc">GRAVITY_EARTH</span>
        <span class="kd">val</span> <span class="py">gZ</span> <span class="p">=</span> <span class="n">z</span> <span class="p">/</span> <span class="nc">SensorManager</span><span class="p">.</span><span class="nc">GRAVITY_EARTH</span>

        <span class="c1">// 勾股定理计算综合受力大小</span>
        <span class="kd">val</span> <span class="py">gForce</span> <span class="p">=</span> <span class="nf">sqrt</span><span class="p">((</span><span class="n">gX</span> <span class="p">*</span> <span class="n">gX</span> <span class="p">+</span> <span class="n">gY</span> <span class="p">*</span> <span class="n">gY</span> <span class="p">+</span> <span class="n">gZ</span> <span class="p">*</span> <span class="n">gZ</span><span class="p">).</span><span class="nf">toDouble</span><span class="p">()).</span><span class="nf">toFloat</span><span class="p">()</span>

        <span class="c1">// 阈值设定为 2.7：大约是用力甩一下手机的力度</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">gForce</span> <span class="p">&gt;</span> <span class="mf">2.7f</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">now</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">currentTimeMillis</span><span class="p">()</span>
            <span class="c1">// 设置 1 秒的冷却防抖时间</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">now</span> <span class="p">-</span> <span class="n">lastShakeTime</span> <span class="p">&gt;</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">lastShakeTime</span> <span class="p">=</span> <span class="n">now</span>
                <span class="c1">// 模拟用户点击了摇骰子按钮</span>
                <span class="n">findViewById</span><span class="p">&lt;</span><span class="nc">Button</span><span class="p">&gt;(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">btnRoll</span><span class="p">).</span><span class="nf">performClick</span><span class="p">()</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<h4 id="2-如何实现逼真的骰子翻滚与触觉反馈">2. 如何实现逼真的“骰子翻滚”与触觉反馈？</h4>

<p>为了不让结果显得太突兀，利用安卓自带的 <code class="language-plaintext highlighter-rouge">CountDownTimer</code> 写了一个 1 秒钟的动画循环。在这 1 秒内，不仅疯狂切换图片、随机倾斜角度，还同步调用了震动马达：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 开始摇骰子动画（持续1000毫秒，每100毫秒刷新一次）</span>
<span class="kd">object</span> <span class="err">: </span><span class="nc">CountDownTimer</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onTick</span><span class="p">(</span><span class="n">millisUntilFinished</span><span class="p">:</span> <span class="nc">Long</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">view</span> <span class="k">in</span> <span class="n">diceViews</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// 随机切图并赋予 -30 到 30 度的随机倾斜角</span>
            <span class="n">view</span><span class="p">.</span><span class="nf">setImageResource</span><span class="p">(</span><span class="n">diceImages</span><span class="p">[</span><span class="nc">Random</span><span class="p">.</span><span class="nf">nextInt</span><span class="p">(</span><span class="mi">6</span><span class="p">)])</span>
            <span class="n">view</span><span class="p">.</span><span class="n">rotation</span> <span class="p">=</span> <span class="nc">Random</span><span class="p">.</span><span class="nf">nextInt</span><span class="p">(-</span><span class="mi">30</span><span class="p">,</span> <span class="mi">30</span><span class="p">).</span><span class="nf">toFloat</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="c1">// 摇晃过程中：触发 20 毫秒的极轻微震动（模拟骰子撞击杯壁）</span>
        <span class="k">if</span> <span class="p">(</span><span class="nc">Build</span><span class="p">.</span><span class="nc">VERSION</span><span class="p">.</span><span class="nc">SDK_INT</span> <span class="p">&gt;=</span> <span class="nc">Build</span><span class="p">.</span><span class="nc">VERSION_CODES</span><span class="p">.</span><span class="nc">O</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">vibrator</span><span class="p">.</span><span class="nf">vibrate</span><span class="p">(</span><span class="nc">VibrationEffect</span><span class="p">.</span><span class="nf">createOneShot</span><span class="p">(</span><span class="mi">20</span><span class="p">,</span> <span class="nc">VibrationEffect</span><span class="p">.</span><span class="nc">DEFAULT_AMPLITUDE</span><span class="p">))</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">vibrator</span><span class="p">.</span><span class="nf">vibrate</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onFinish</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">view</span> <span class="k">in</span> <span class="n">diceViews</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// 定格最终点数，并将角度摆正</span>
            <span class="n">view</span><span class="p">.</span><span class="nf">setImageResource</span><span class="p">(</span><span class="n">diceImages</span><span class="p">[</span><span class="nc">Random</span><span class="p">.</span><span class="nf">nextInt</span><span class="p">(</span><span class="mi">6</span><span class="p">)])</span>
            <span class="n">view</span><span class="p">.</span><span class="n">rotation</span> <span class="p">=</span> <span class="mf">0f</span> 
        <span class="p">}</span>
        <span class="c1">// 摇晃结束：触发 100 毫秒的重震（模拟落桌定音）</span>
        <span class="k">if</span> <span class="p">(</span><span class="nc">Build</span><span class="p">.</span><span class="nc">VERSION</span><span class="p">.</span><span class="nc">SDK_INT</span> <span class="p">&gt;=</span> <span class="nc">Build</span><span class="p">.</span><span class="nc">VERSION_CODES</span><span class="p">.</span><span class="nc">O</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">vibrator</span><span class="p">.</span><span class="nf">vibrate</span><span class="p">(</span><span class="nc">VibrationEffect</span><span class="p">.</span><span class="nf">createOneShot</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="nc">VibrationEffect</span><span class="p">.</span><span class="nc">DEFAULT_AMPLITUDE</span><span class="p">))</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">vibrator</span><span class="p">.</span><span class="nf">vibrate</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="n">isRolling</span> <span class="p">=</span> <span class="k">false</span> 
    <span class="p">}</span>
<span class="p">}.</span><span class="nf">start</span><span class="p">()</span>

</code></pre></div></div>

<h3 id="-写在最后">💡 写在最后</h3>

<p>如果你也想体验一下这款小工具，或者对源码感兴趣，欢迎讨论与交流！</p>

<ul>
  <li>项目地址：<a href="https://github.com/JMbaozi/PartyDice">https://github.com/JMbaozi/PartyDice</a></li>
  <li>安装包地址：<a href="https://github.com/JMbaozi/PartyDice/releases/tag/v1.0">摇了么 v1.0</a></li>
</ul>

<p><img src="https://youke.xn--y7xa690gmna.cn/s1/2026/02/22/699b03b332e82.webp" alt="home.jpg" /></p>]]></content><author><name>yucol</name></author><category term="app" /><category term="安卓" /><category term="骰子" /><category term="游戏" /><category term="独立开发" /><summary type="html"><![CDATA[🚀 我的第一款酒局神器App“摇了么”诞生记！]]></summary></entry><entry><title type="html">从 H2O 到 H2O-ac：我的博客迁移与填坑笔记</title><link href="https://yucol.top/blog/blog-migration-notes.html" rel="alternate" type="text/html" title="从 H2O 到 H2O-ac：我的博客迁移与填坑笔记" /><published>2026-02-22T00:00:00+00:00</published><updated>2026-02-22T00:00:00+00:00</updated><id>https://yucol.top/blog/blog-migration-notes</id><content type="html" xml:base="https://yucol.top/blog/blog-migration-notes.html"><![CDATA[<h2 id="前言">前言</h2>

<p>博客最初的采用的模板是由<a href="https://github.com/kaeyleo">kaeyleo</a>基于<a href="https://jekyllrb.com/">jekyll</a>开发的<a href="https://github.com/kaeyleo/jekyll-theme-H2O">H2O</a>主题。我在此基础上添加了评论区、友链、归档、音乐播放器、访客记录、目录等功能。但是限于能力有限，写的代码一堆BUG。最近我浏览到了一个基于<a href="https://github.com/kaeyleo/jekyll-theme-H2O">H2O</a>修改的<a href="https://github.com/zhonger/jekyll-theme-H2O-ac">H2O-ac</a>主题，其内容丰富，视觉效果和阅读体验非常好。</p>

<p>所以我决定对博客进行了一次“大手术”，将原本一直使用的H2O模板替换成了目前视觉效果非常出色的 <strong>H2O-ac</strong> 主题。</p>

<p>虽然 H2O-ac 带来了极佳的阅读体验和学术风界面，但在迁移和配置自动化的过程中，我遇到了不少令人头大的 Bug。</p>

<p>为了方便日后查阅，也为了给同样使用该主题的小伙伴提供参考，特写下这篇笔记。</p>

<h2 id="迁移过程从手动到全自动">迁移过程：从手动到全自动</h2>

<p>最初的迁移并不顺利，旧模板留下的残留文件（如 Azure 的部署脚本等）频繁导致构建失败。</p>

<h3 id="1-自动化-cicd-的重建">1. 自动化 CI/CD 的重建</h3>
<p>为了实现“推送即发布”，我弃用了原本混乱的部署脚本，重新编写了 <code class="language-plaintext highlighter-rouge">.github/workflows/jekyll.yml</code>。通过以下逻辑确保了环境的纯净：</p>
<ul>
  <li><strong>环境初始化</strong>：使用 Ruby 3.4.4 环境。</li>
  <li><strong>强制清理</strong>：在编译前运行 <code class="language-plaintext highlighter-rouge">bundle exec jekyll clean</code>。</li>
  <li><strong>强制覆盖</strong>：发布到 <code class="language-plaintext highlighter-rouge">gh-pages</code> 分支时使用 <code class="language-plaintext highlighter-rouge">force_orphan: true</code>，彻底杜绝旧文件缓存。</li>
</ul>

<h3 id="2-版权与开源协议">2. 版权与开源协议</h3>
<p>由于 H2O-ac 是基于 H2O 主题的 fork 版本，遵循 <strong>MIT 开源协议</strong>。在迁移过程中，我保留了原作者的版权信息。</p>

<h2 id="遇到的问题与-bug-排查重点">遇到的问题与 Bug 排查（重点）</h2>

<p>这次迁移最核心的挑战在于：文章在标签（Tags）页能看，但在首页（Blog）和归档（Archives）页却神秘失踪。经过反复排查，我定位到了以下几个“元凶”：</p>

<h3 id="bug-1pin-false-逻辑陷阱">Bug 1：<code class="language-plaintext highlighter-rouge">pin: false</code> 逻辑陷阱</h3>
<p>这是最难发现的一个点。H2O-ac 主题自带了置顶功能，但我发现：</p>
<ul>
  <li>当我在文章头部显式写下 <code class="language-plaintext highlighter-rouge">pin: false</code> 时，首页和归档页会完全无法显示该文章。</li>
  <li><strong>解决方案</strong>：普通文章直接<strong>删除 <code class="language-plaintext highlighter-rouge">pin</code> 属性</strong>即可。如果不置顶，就不要写这一行，写成 <code class="language-plaintext highlighter-rouge">false</code> 反而会被主题逻辑误判并过滤掉。</li>
</ul>

<h3 id="bug-2分支挂载错位">Bug 2：分支挂载错位</h3>
<p>在 GitHub Pages 设置中，如果将部署源选为 <code class="language-plaintext highlighter-rouge">master</code>，可能无法看到经过 Actions 机器人编译后的成品。</p>
<ul>
  <li><strong>解决方法</strong>：将 GitHub Pages 的部署分支切换到 <strong><code class="language-plaintext highlighter-rouge">gh-pages</code></strong>，这才是机器人存放 HTML 成品的地方。</li>
</ul>

<h2 id="参考文档">参考文档</h2>

<ul>
  <li><a href="https://github.com/zhonger/jekyll-theme-H2O-ac">H2O-ac主题仓库</a></li>
  <li><a href="https://lisz.me/tech/new-theme-h2o-ac">H2O-ac主题介绍</a></li>
  <li><a href="https://h2o-ac-doc.lisz.me/">H2O-ac主题文档</a></li>
</ul>

<h2 id="结语">结语</h2>

<p>感谢 H2O 系列主题的作者们提供的优秀代码。</p>]]></content><author><name>yucol</name></author><category term="blog" /><category term="Jekyll" /><category term="GitHub Actions" /><category term="填坑" /><category term="blog" /><summary type="html"><![CDATA[前言]]></summary></entry><entry><title type="html">HTTP 状态码</title><link href="https://yucol.top/tech/HTTP-%E7%8A%B6%E6%80%81%E7%A0%81.html" rel="alternate" type="text/html" title="HTTP 状态码" /><published>2026-01-06T00:00:00+00:00</published><updated>2026-01-06T00:00:00+00:00</updated><id>https://yucol.top/tech/HTTP%20%E7%8A%B6%E6%80%81%E7%A0%81</id><content type="html" xml:base="https://yucol.top/tech/HTTP-%E7%8A%B6%E6%80%81%E7%A0%81.html"><![CDATA[<h1 id="http-状态码完全指南详解版">HTTP 状态码完全指南（详解版）</h1>

<blockquote>
  <p>一篇面向 <strong>Web / 后端 / 前端 / GIS Web 应用开发者</strong> 的 HTTP 状态码系统性参考文章。</p>
</blockquote>

<hr />

<h2 id="一什么是-http-状态码">一、什么是 HTTP 状态码</h2>

<p><strong>HTTP 状态码（HTTP Status Code）</strong> 是服务器在响应客户端（浏览器、APP、程序）请求时，返回的一个 <strong>三位数字</strong>，用于说明：</p>

<blockquote>
  <p>👉 <strong>这次请求发生了什么、结果如何、是否需要进一步操作</strong></p>
</blockquote>

<p>HTTP 状态码是 <strong>HTTP 协议语义的核心组成部分</strong>，对于：</p>

<ul>
  <li>前后端接口设计</li>
  <li>RESTful API 规范</li>
  <li>错误处理与调试</li>
  <li>网络性能与安全分析</li>
</ul>

<p>都具有非常重要的意义。</p>

<hr />

<h2 id="二http-状态码的五大类">二、HTTP 状态码的五大类</h2>

<p>HTTP 状态码按照 <strong>首位数字</strong> 分为五大类：</p>

<table>
  <thead>
    <tr>
      <th>分类</th>
      <th>范围</th>
      <th>含义</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1xx</td>
      <td>100–199</td>
      <td>信息性响应（请求已接收）</td>
    </tr>
    <tr>
      <td>2xx</td>
      <td>200–299</td>
      <td>成功（请求已成功处理）</td>
    </tr>
    <tr>
      <td>3xx</td>
      <td>300–399</td>
      <td>重定向（需要进一步操作）</td>
    </tr>
    <tr>
      <td>4xx</td>
      <td>400–499</td>
      <td>客户端错误</td>
    </tr>
    <tr>
      <td>5xx</td>
      <td>500–599</td>
      <td>服务器错误</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="三1xx信息性状态码informational">三、1xx：信息性状态码（Informational）</h2>

<blockquote>
  <p>表示请求已被接收，正在处理，<strong>很少在业务代码中直接使用</strong>。</p>
</blockquote>

<h3 id="100-continue">100 Continue</h3>

<ul>
  <li>含义：客户端可以继续发送请求体</li>
  <li>场景：大文件上传前的确认机制</li>
</ul>

<h3 id="101-switching-protocols">101 Switching Protocols</h3>

<ul>
  <li>含义：服务器同意切换协议</li>
  <li>场景：HTTP → WebSocket</li>
</ul>

<hr />

<h2 id="四2xx成功状态码success">四、2xx：成功状态码（Success）</h2>

<blockquote>
  <p><strong>最重要、最常用的一类状态码</strong>，表示请求被正确处理。</p>
</blockquote>

<h3 id="200-ok">200 OK</h3>

<ul>
  <li>含义：请求成功</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>GET 查询成功</li>
      <li>PUT / PATCH 更新成功</li>
    </ul>
  </li>
</ul>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /api/fields/123
</span><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
</code></pre></div></div>

<hr />

<h3 id="201-created">201 Created</h3>

<ul>
  <li>含义：资源创建成功</li>
  <li>场景：POST 新建资源</li>
  <li>建议：返回新资源的 URL</li>
</ul>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST /api/fields
</span><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">201</span> <span class="ne">Created</span>
<span class="na">Location</span><span class="p">:</span> <span class="s">/api/fields/456</span>
</code></pre></div></div>

<hr />

<h3 id="202-accepted">202 Accepted</h3>

<ul>
  <li>含义：请求已接收，但尚未处理完成</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>异步任务</li>
      <li>后台计算（如 GIS 插值、模型分析）</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="204-no-content">204 No Content</h3>

<ul>
  <li>含义：请求成功，但无返回内容</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>删除成功</li>
      <li>仅触发动作的接口</li>
    </ul>
  </li>
</ul>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">DELETE /api/fields/123
</span><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">204</span> <span class="ne">No Content</span>
</code></pre></div></div>

<hr />

<h2 id="五3xx重定向状态码redirection">五、3xx：重定向状态码（Redirection）</h2>

<blockquote>
  <p>表示资源位置发生变化，需要客户端采取进一步操作。</p>
</blockquote>

<h3 id="301-moved-permanently">301 Moved Permanently</h3>

<ul>
  <li>含义：永久重定向</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>域名迁移</li>
      <li>SEO 优化</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="302-found">302 Found</h3>

<ul>
  <li>含义：临时重定向（历史遗留）</li>
  <li>注意：可能被浏览器当作 GET 请求处理</li>
</ul>

<hr />

<h3 id="303-see-other">303 See Other</h3>

<ul>
  <li>含义：使用 GET 访问另一个 URI</li>
  <li>场景：POST 后跳转页面</li>
</ul>

<hr />

<h3 id="304-not-modified">304 Not Modified</h3>

<ul>
  <li>含义：资源未修改</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>浏览器缓存</li>
      <li>ETag / If-Modified-Since</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="六4xx客户端错误client-error">六、4xx：客户端错误（Client Error）</h2>

<blockquote>
  <p><strong>表示问题出在客户端请求本身</strong>。</p>
</blockquote>

<h3 id="400-bad-request">400 Bad Request</h3>

<ul>
  <li>含义：请求格式错误</li>
  <li>
    <p>常见原因：</p>

    <ul>
      <li>JSON 语法错误</li>
      <li>参数缺失</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="401-unauthorized">401 Unauthorized</h3>

<ul>
  <li>含义：未认证</li>
  <li>
    <p>特点：</p>

    <ul>
      <li>需要登录</li>
      <li>通常配合 <code class="language-plaintext highlighter-rouge">WWW-Authenticate</code></li>
    </ul>
  </li>
</ul>

<blockquote>
  <p>⚠️ 注意：401 ≠ 权限不足</p>
</blockquote>

<hr />

<h3 id="403-forbidden">403 Forbidden</h3>

<ul>
  <li>含义：已认证，但无权限</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>普通用户访问管理员接口</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="404-not-found">404 Not Found</h3>

<ul>
  <li>含义：资源不存在</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>URL 错误</li>
      <li>资源被删除</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="405-method-not-allowed">405 Method Not Allowed</h3>

<ul>
  <li>含义：请求方法不被允许</li>
  <li>示例：</li>
</ul>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST /api/fields/123
</span><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">405</span> <span class="ne">Method Not Allowed</span>
<span class="na">Allow</span><span class="p">:</span> <span class="s">GET, PUT</span>
</code></pre></div></div>

<hr />

<h3 id="409-conflict">409 Conflict</h3>

<ul>
  <li>含义：资源冲突</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>重复创建</li>
      <li>乐观锁冲突</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="422-unprocessable-entity">422 Unprocessable Entity</h3>

<ul>
  <li>含义：语义正确，但业务校验失败</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>表单校验</li>
      <li>数据规则不满足</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="七5xx服务器错误server-error">七、5xx：服务器错误（Server Error）</h2>

<blockquote>
  <p><strong>表示服务器在处理请求时发生异常</strong>。</p>
</blockquote>

<h3 id="500-internal-server-error">500 Internal Server Error</h3>

<ul>
  <li>含义：服务器内部错误</li>
  <li>
    <p>原因：</p>

    <ul>
      <li>未捕获异常</li>
      <li>代码 Bug</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="502-bad-gateway">502 Bad Gateway</h3>

<ul>
  <li>含义：上游服务器返回无效响应</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>反向代理</li>
      <li>微服务调用失败</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="503-service-unavailable">503 Service Unavailable</h3>

<ul>
  <li>含义：服务暂不可用</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>服务维护</li>
      <li>系统过载</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="504-gateway-timeout">504 Gateway Timeout</h3>

<ul>
  <li>含义：上游服务超时</li>
  <li>
    <p>场景：</p>

    <ul>
      <li>GIS 大规模计算</li>
      <li>数据库响应过慢</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="八restful-api-状态码设计建议">八、RESTful API 状态码设计建议</h2>

<h3 id="1️⃣-成功不要只用-200">1️⃣ 成功不要只用 200</h3>

<table>
  <thead>
    <tr>
      <th>场景</th>
      <th>推荐状态码</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>创建资源</td>
      <td>201</td>
    </tr>
    <tr>
      <td>删除成功</td>
      <td>204</td>
    </tr>
    <tr>
      <td>异步处理</td>
      <td>202</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="2️⃣-错误状态码要语义准确">2️⃣ 错误状态码要“语义准确”</h3>

<table>
  <thead>
    <tr>
      <th>错误场景</th>
      <th>正确做法</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>未登录</td>
      <td>401</td>
    </tr>
    <tr>
      <td>无权限</td>
      <td>403</td>
    </tr>
    <tr>
      <td>参数错误</td>
      <td>400 / 422</td>
    </tr>
    <tr>
      <td>资源不存在</td>
      <td>404</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="3️⃣-状态码--错误信息结构示例">3️⃣ 状态码 + 错误信息结构示例</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"code"</span><span class="p">:</span><span class="w"> </span><span class="mi">422</span><span class="p">,</span><span class="w">
  </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Validation failed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"details"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"area"</span><span class="p">:</span><span class="w"> </span><span class="s2">"must be greater than 0"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="九常见误区总结">九、常见误区总结</h2>

<p>❌ 所有错误都返回 200</p>

<p>❌ 用 500 表示业务校验失败</p>

<p>❌ 401 和 403 混用</p>

<p>❌ 前端只判断 code，不看 HTTP 状态码</p>

<hr />

<h2 id="十结语">十、结语</h2>

<p>HTTP 状态码不仅是 <strong>网络协议的一部分</strong>，更是 <strong>接口设计质量的重要体现</strong>。</p>

<p>一个设计良好的接口，应该做到：</p>

<ul>
  <li><strong>状态码语义清晰</strong></li>
  <li><strong>前后端理解一致</strong></li>
  <li><strong>错误可定位、可处理</strong></li>
</ul>]]></content><author><name>yucol</name></author><category term="tech" /><category term="HTTP" /><category term="前端" /><category term="后端" /><summary type="html"><![CDATA[HTTP 状态码完全指南（详解版）]]></summary></entry><entry><title type="html">土壤采样点</title><link href="https://yucol.top/tech/%E5%9C%9F%E5%A3%A4%E9%87%87%E6%A0%B7%E7%82%B9.html" rel="alternate" type="text/html" title="土壤采样点" /><published>2025-11-17T00:00:00+00:00</published><updated>2025-11-17T00:00:00+00:00</updated><id>https://yucol.top/tech/%E5%9C%9F%E5%A3%A4%E9%87%87%E6%A0%B7%E7%82%B9</id><content type="html" xml:base="https://yucol.top/tech/%E5%9C%9F%E5%A3%A4%E9%87%87%E6%A0%B7%E7%82%B9.html"><![CDATA[<blockquote>
  <p>以 <strong>ArcGIS Pro 3.0.2</strong> 版本为例，制作某区域土壤采样点。
投影坐标系：WGS-84墨卡托</p>
</blockquote>

<h1 id="目视采样">目视采样</h1>

<p>按照能谱或其他数据（本文所有“异常点”采集样本均为<strong>能谱总计数Total</strong>）进行空间插值，对高/低值进行目视解译，根据需求选取合适数量的土壤采样点。</p>

<p>该方法过于简单，不再赘述。</p>

<h1 id="完全随机采样">完全随机采样</h1>

<h2 id="全局完全随机采样">全局完全随机采样</h2>

<p>将空间插值后的栅格数据进行<code class="language-plaintext highlighter-rouge">栅格转点</code>，之后使用<code class="language-plaintext highlighter-rouge">值提取至点</code>或<code class="language-plaintext highlighter-rouge">多值提取至点</code>。使用<code class="language-plaintext highlighter-rouge">数据管理工具</code>–&gt;<code class="language-plaintext highlighter-rouge">采样</code>–&gt;<code class="language-plaintext highlighter-rouge">创建随机点</code>，根据数据设置参数。</p>

<p>当前方法是在整个区域内进行采样，只考虑数量，不考虑任何其它因素。虽然使用了<code class="language-plaintext highlighter-rouge">值提取至点</code>或<code class="language-plaintext highlighter-rouge">多值提取至点</code>，但仅是为了后续制图时可以借助参考。</p>

<h2 id="探测器行进轨迹上随机采样">探测器行进轨迹上随机采样</h2>

<p>与上述方法不同的点在于，随机分为不是全局，而是探测器（无人机、探测车等）的<strong>行进轨迹</strong>。</p>

<p>根据数据点的<code class="language-plaintext highlighter-rouge">采集时间</code>字段生成轨迹路线：<code class="language-plaintext highlighter-rouge">数据管理工具</code>–&gt;<code class="language-plaintext highlighter-rouge">要素</code>–&gt;<code class="language-plaintext highlighter-rouge">点集转线</code>，<strong>排序字段</strong>选择<code class="language-plaintext highlighter-rouge">采集时间</code>字段。</p>

<p>之后，再将轨迹路线转为点要素：<code class="language-plaintext highlighter-rouge">数据管理工具</code>–&gt;<code class="language-plaintext highlighter-rouge">采样</code>–&gt;<code class="language-plaintext highlighter-rouge">沿线生成点</code>。</p>
<blockquote>
  <p>谨记：切勿使用<code class="language-plaintext highlighter-rouge">要素转点</code>工具，该工具只会生成一个点（中心点）。</p>
</blockquote>

<p>使用<code class="language-plaintext highlighter-rouge">创建随机点</code>，根据数据设置参数。</p>

<h1 id="沿线性要素的分层随机采样">沿线性要素的分层随机采样</h1>

<p>该方法是<strong>ArcGIS Pro</strong>环境下的随机土壤采样点方式之一，其考虑了探测器行进路径及不同属性高低值的因素。</p>

<p>使用<code class="language-plaintext highlighter-rouge">值提取至点</code>或<code class="language-plaintext highlighter-rouge">多值提取至点</code>，将空间插值数据（Total）提取至轨迹点中。打开点数据的<code class="language-plaintext highlighter-rouge">属性表</code>，<strong>添加</strong>一个新字段，用以对当前点进行分类，例如字段<code class="language-plaintext highlighter-rouge">class</code>，值有<code class="language-plaintext highlighter-rouge">coldPoint</code>、<code class="language-plaintext highlighter-rouge">hotPoint</code>、<code class="language-plaintext highlighter-rouge">normalPoint</code>。随后选中对应的分类，使用<code class="language-plaintext highlighter-rouge">创建随机点</code>，根据数据设置参数。这里三种分类各自的数量可以为5/5/3，最小间隔距离为100米。</p>

<p><img src="https://img14.360buyimg.com/ddimg/jfs/t1/368946/2/1327/174372/691aec6eF98883664/f1f40b9a051b4d08.jpg" alt="Sampling_Point.png" /></p>]]></content><author><name>yucol</name></author><category term="tech" /><category term="智慧农业" /><category term="测土" /><category term="土壤采样" /><summary type="html"><![CDATA[以 ArcGIS Pro 3.0.2 版本为例，制作某区域土壤采样点。 投影坐标系：WGS-84墨卡托]]></summary></entry></feed>