<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Justin3go</title>
        <link>https://justin3go.com</link>
        <description>坚持深耕技术领域的 T 型前端程序员, 关注独立开发，喜欢 Vuejs、Nestjs, 还会点 Python、搜索引擎、NLP、Web3、后端</description>
        <lastBuildDate>Sun, 12 Apr 2026 07:03:56 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-Hans</language>
        <image>
            <title>Justin3go</title>
            <url>https://oss.justin3go.com/justin3goAvatar.jpg</url>
            <link>https://justin3go.com</link>
        </image>
        <copyright>Copyright© 2021-present Justin3go</copyright>
        <item>
            <title><![CDATA[丢掉沉重的记忆：Codex、Claude Code 与 OpenCode 的上下文压缩术]]></title>
            <link>https://justin3go.com/posts/2026/04/09-context-compaction-in-codex-claude-code-and-opencode</link>
            <guid>https://justin3go.com/posts/2026/04/09-context-compaction-in-codex-claude-code-and-opencode</guid>
            <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="丢掉沉重的记忆-codex、claude-code-与-opencode-的上下文压缩术" tabindex="-1">丢掉沉重的记忆：Codex、Claude Code 与 OpenCode 的上下文压缩术 <a class="header-anchor" href="#丢掉沉重的记忆-codex、claude-code-与-opencode-的上下文压缩术" aria-label="Permalink to &quot;丢掉沉重的记忆：Codex、Claude Code 与 OpenCode 的上下文压缩术&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<blockquote></blockquote>
<p>本文以一个 15,400 tokens 的登录 Bug 修复场景为切入点，深入拆解了三款主流 CLI Agent（<strong>Codex CLI</strong>、<strong>Claude Code</strong>、<strong>OpenCode</strong>）的上下文压缩策略。Codex CLI 采用&quot;工作交接单&quot;式的单层摘要替换；Claude Code 设计了三层递进机制——工具结果修剪、<strong>Prompt Cache 友好策略</strong>和 9 部分结构化 LLM 总结；OpenCode 则以非物理删除的时间戳标记隐藏配合 5 标题 LLM 摘要实现&quot;阶梯治理&quot;。文章揭示了一个核心洞察：最好的上下文管理不是无限扩大记忆容量，而是学会<strong>精密地遗忘</strong>。</p>
<blockquote></blockquote>
<!-- DESC SEP -->
<p>在使用 AI Agent 深度参与编程任务时，你一定遇到过这种窘境：起初 AI 反应敏捷，指哪打哪；但随着对话轮次增加，它似乎开始变得越来越笨。</p>
<p>上下文快用完的时候，AI会着急完成导致效果不佳，社区中称作 Context Anxiety (上下文焦虑)，和我们人一样，着急就容易出错。为了维持对话，Agent 必须丢掉一部分记忆（压缩 - Compact）。但怎么丢、丢掉谁、丢掉后怎么补救，成了衡量一个 Agent 运行时（Runtime）是否成熟的分水岭之一。</p>
<p>今天笔者就带大家拆解三款主流 CLI Agent——<strong>Codex CLI</strong>、<strong>Claude Code</strong> 和 <strong>OpenCode</strong>。看看它们在面对同一个登录 Bug 时，是如何施展各自的&quot;压缩大法&quot;的。</p>
<blockquote>
<p>注：本文分析基于 Codex CLI 与 OpenCode 的开源仓库逻辑，以及社区逆向研究与泄露源码对 Claude Code 运行时的验证。</p>
</blockquote>
<h2 id="场景回放-一场价值-15-400-tokens-的登录修复" tabindex="-1">场景回放：一场价值 15,400 Tokens 的登录修复 <a class="header-anchor" href="#场景回放-一场价值-15-400-tokens-的登录修复" aria-label="Permalink to &quot;场景回放：一场价值 15,400 Tokens 的登录修复&quot;">&ZeroWidthSpace;</a></h2>
<p>假设你正在修复一个登录接口报 401 Unauthorized 的 Bug。你召唤了 AI Agent，并经历了一番激烈的排查。</p>
<p>下面是这段对话的完整记录：</p>
<p>| 编号 | 角色 | 内容摘要 | 预估 Token |
| :</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="丢掉沉重的记忆-codex、claude-code-与-opencode-的上下文压缩术" tabindex="-1">丢掉沉重的记忆：Codex、Claude Code 与 OpenCode 的上下文压缩术 <a class="header-anchor" href="#丢掉沉重的记忆-codex、claude-code-与-opencode-的上下文压缩术" aria-label="Permalink to &quot;丢掉沉重的记忆：Codex、Claude Code 与 OpenCode 的上下文压缩术&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<blockquote></blockquote>
<p>本文以一个 15,400 tokens 的登录 Bug 修复场景为切入点，深入拆解了三款主流 CLI Agent（<strong>Codex CLI</strong>、<strong>Claude Code</strong>、<strong>OpenCode</strong>）的上下文压缩策略。Codex CLI 采用&quot;工作交接单&quot;式的单层摘要替换；Claude Code 设计了三层递进机制——工具结果修剪、<strong>Prompt Cache 友好策略</strong>和 9 部分结构化 LLM 总结；OpenCode 则以非物理删除的时间戳标记隐藏配合 5 标题 LLM 摘要实现&quot;阶梯治理&quot;。文章揭示了一个核心洞察：最好的上下文管理不是无限扩大记忆容量，而是学会<strong>精密地遗忘</strong>。</p>
<blockquote></blockquote>
<!-- DESC SEP -->
<p>在使用 AI Agent 深度参与编程任务时，你一定遇到过这种窘境：起初 AI 反应敏捷，指哪打哪；但随着对话轮次增加，它似乎开始变得越来越笨。</p>
<p>上下文快用完的时候，AI会着急完成导致效果不佳，社区中称作 Context Anxiety (上下文焦虑)，和我们人一样，着急就容易出错。为了维持对话，Agent 必须丢掉一部分记忆（压缩 - Compact）。但怎么丢、丢掉谁、丢掉后怎么补救，成了衡量一个 Agent 运行时（Runtime）是否成熟的分水岭之一。</p>
<p>今天笔者就带大家拆解三款主流 CLI Agent——<strong>Codex CLI</strong>、<strong>Claude Code</strong> 和 <strong>OpenCode</strong>。看看它们在面对同一个登录 Bug 时，是如何施展各自的&quot;压缩大法&quot;的。</p>
<blockquote>
<p>注：本文分析基于 Codex CLI 与 OpenCode 的开源仓库逻辑，以及社区逆向研究与泄露源码对 Claude Code 运行时的验证。</p>
</blockquote>
<h2 id="场景回放-一场价值-15-400-tokens-的登录修复" tabindex="-1">场景回放：一场价值 15,400 Tokens 的登录修复 <a class="header-anchor" href="#场景回放-一场价值-15-400-tokens-的登录修复" aria-label="Permalink to &quot;场景回放：一场价值 15,400 Tokens 的登录修复&quot;">&ZeroWidthSpace;</a></h2>
<p>假设你正在修复一个登录接口报 401 Unauthorized 的 Bug。你召唤了 AI Agent，并经历了一番激烈的排查。</p>
<p>下面是这段对话的完整记录：</p>
<table tabindex="0">
<thead>
<tr>
<th style="text-align:left">编号</th>
<th style="text-align:left">角色</th>
<th style="text-align:left">内容摘要</th>
<th style="text-align:left">预估 Token</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">#1</td>
<td style="text-align:left">System</td>
<td style="text-align:left">系统提示词（含 40+ 工具定义）</td>
<td style="text-align:left">~800</td>
</tr>
<tr>
<td style="text-align:left">#2</td>
<td style="text-align:left">User</td>
<td style="text-align:left">&quot;登录页面报 401，帮我排查下&quot;</td>
<td style="text-align:left">~100</td>
</tr>
<tr>
<td style="text-align:left">#3</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;我先搜一下认证相关的逻辑&quot;</td>
<td style="text-align:left">~150</td>
</tr>
<tr>
<td style="text-align:left">#4</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>grep &quot;auth&quot; --include=&quot;*.ts&quot;</code></td>
<td style="text-align:left">~50</td>
</tr>
<tr>
<td style="text-align:left">#5</td>
<td style="text-align:left"><strong>Tool Result</strong></td>
<td style="text-align:left"><strong>(返回 50 处搜索结果)</strong></td>
<td style="text-align:left"><strong>~2,000</strong></td>
</tr>
<tr>
<td style="text-align:left">#6</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;搜到几处，我看看 auth.ts&quot;</td>
<td style="text-align:left">~150</td>
</tr>
<tr>
<td style="text-align:left">#7</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>read_file &quot;src/auth.ts&quot;</code></td>
<td style="text-align:left">~50</td>
</tr>
<tr>
<td style="text-align:left">#8</td>
<td style="text-align:left"><strong>Tool Result</strong></td>
<td style="text-align:left"><strong>(完整文件内容，约 300 行)</strong></td>
<td style="text-align:left"><strong>~3,500</strong></td>
</tr>
<tr>
<td style="text-align:left">#9</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;找到了，token 校验没处理过期&quot;</td>
<td style="text-align:left">~300</td>
</tr>
<tr>
<td style="text-align:left">#10</td>
<td style="text-align:left">User</td>
<td style="text-align:left">&quot;好，但要兼容旧的 Session 方式&quot;</td>
<td style="text-align:left">~80</td>
</tr>
<tr>
<td style="text-align:left">#11</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;明白，我再看看 middleware&quot;</td>
<td style="text-align:left">~150</td>
</tr>
<tr>
<td style="text-align:left">#12</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>read_file &quot;src/middleware.ts&quot;</code></td>
<td style="text-align:left">~50</td>
</tr>
<tr>
<td style="text-align:left">#13</td>
<td style="text-align:left"><strong>Tool Result</strong></td>
<td style="text-align:left"><strong>(middleware 完整内容)</strong></td>
<td style="text-align:left"><strong>~2,500</strong></td>
</tr>
<tr>
<td style="text-align:left">#14</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;middleware 也要改，我来处理&quot;</td>
<td style="text-align:left">~200</td>
</tr>
<tr>
<td style="text-align:left">#15</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>edit_file &quot;src/auth.ts&quot; (patch)</code></td>
<td style="text-align:left">~100</td>
</tr>
<tr>
<td style="text-align:left">#16</td>
<td style="text-align:left">Tool Result</td>
<td style="text-align:left">&quot;Successfully applied.&quot;</td>
<td style="text-align:left">~30</td>
</tr>
<tr>
<td style="text-align:left">#17</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>edit_file &quot;src/middleware.ts&quot; (patch)</code></td>
<td style="text-align:left">~100</td>
</tr>
<tr>
<td style="text-align:left">#18</td>
<td style="text-align:left">Tool Result</td>
<td style="text-align:left">&quot;Successfully applied.&quot;</td>
<td style="text-align:left">~30</td>
</tr>
<tr>
<td style="text-align:left">#19</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;代码改好了，跑个测试看看&quot;</td>
<td style="text-align:left">~100</td>
</tr>
<tr>
<td style="text-align:left">#20</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>bash &quot;npm test&quot;</code></td>
<td style="text-align:left">~50</td>
</tr>
<tr>
<td style="text-align:left">#21</td>
<td style="text-align:left"><strong>Tool Result</strong></td>
<td style="text-align:left"><strong>(3 个测试失败，含堆栈)</strong></td>
<td style="text-align:left"><strong>~3,000</strong></td>
</tr>
<tr>
<td style="text-align:left">#22</td>
<td style="text-align:left">Assistant</td>
<td style="text-align:left">&quot;有 3 个测试挂了，我修一下测试用例&quot;</td>
<td style="text-align:left">~200</td>
</tr>
<tr>
<td style="text-align:left">#23</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>edit_file &quot;src/auth.test.ts&quot; (patch)</code></td>
<td style="text-align:left">~150</td>
</tr>
<tr>
<td style="text-align:left">#24</td>
<td style="text-align:left">Tool Result</td>
<td style="text-align:left">&quot;Successfully applied.&quot;</td>
<td style="text-align:left">~30</td>
</tr>
<tr>
<td style="text-align:left">#25</td>
<td style="text-align:left">Tool Call</td>
<td style="text-align:left"><code>bash &quot;npm test&quot;</code></td>
<td style="text-align:left">~50</td>
</tr>
<tr>
<td style="text-align:left">#26</td>
<td style="text-align:left"><strong>Tool Result</strong></td>
<td style="text-align:left"><strong>(测试全部通过，含完整输出)</strong></td>
<td style="text-align:left"><strong>~1,500</strong></td>
</tr>
</tbody>
</table>
<p>看起来不过 26 条消息，但已经吃掉了约 <strong>15,400 tokens</strong>。其中加粗的五条工具结果（#5, #8, #13, #21, #26）合计约 <strong>12,500 tokens</strong>，占了 <strong>81%</strong>。这些数据在排查时至关重要，但 Bug 修好后，它们就变成了上下文里沉重的负担。如果不处理，下一轮对话可能因为窗口溢出而丢掉系统提示词或用户的核心需求。</p>
<h2 id="codex-cli-写一份干练的-工作交接单" tabindex="-1">Codex CLI：写一份干练的&quot;工作交接单&quot; <a class="header-anchor" href="#codex-cli-写一份干练的-工作交接单" aria-label="Permalink to &quot;Codex CLI：写一份干练的&quot;工作交接单&quot;&quot;">&ZeroWidthSpace;</a></h2>
<p>OpenAI 的 Codex CLI（<a href="https://github.com/openai/codex" target="_blank" rel="noreferrer">源码</a>，Rust 实现）走的是一种非常符合人类直觉的路线：<strong>总结与替换</strong>。</p>
<p>它的核心思想可以用一句话概括：<strong>把之前的全部对话交给 LLM 写一份&quot;工作交接摘要&quot;，然后用这份摘要替换掉原始历史。</strong></p>
<h3 id="双路径设计" tabindex="-1">双路径设计 <a class="header-anchor" href="#双路径设计" aria-label="Permalink to &quot;双路径设计&quot;">&ZeroWidthSpace;</a></h3>
<p>Codex 提供了两条压缩路径：</p>
<ol>
<li><strong>本地路径</strong>（<code>compact.rs</code>）：在客户端调用 LLM 生成摘要，适用于所有模型提供商。</li>
<li><strong>远程路径</strong>（<code>compact_remote.rs</code>）：直接调用 OpenAI 的内部 API 端点 <code>responses/compact</code>，让服务器完成压缩。仅限 OpenAI 自家模型。</li>
</ol>
<p>注意，这里的&quot;本地&quot;和&quot;远程&quot;指的<strong>不是</strong>是否需要调用 LLM——两条路径都需要 LLM 参与，区别在于**&quot;生成摘要&quot;这个核心步骤跑在哪里**。本地路径下，客户端自己构造摘要 Prompt（从内置模板 <code>templates/compact/prompt.md</code> 加载）、通过 <code>ModelClientSession</code> 流式调用 LLM API、再处理返回结果，整个编排流程都在你的机器上完成，所以它能对接任意模型提供商。远程路径下，客户端把准备好的对话历史和工具定义发给 OpenAI 的 <code>compact_conversation_history</code> 端点，由服务器完成摘要生成——但客户端并非&quot;甩手掌柜&quot;，它在调用前后仍然承担了大量工作：调用前要修剪过长的函数调用历史、构建包含工具规范和系统指令的完整 Prompt 对象；调用后要过滤返回结果（比如丢弃过时的 <code>developer</code> 角色消息、只保留真实的用户和助手内容）、恢复用于 <code>/undo</code> 功能的 ghost snapshots、以及重新计算 token 用量。</p>
<p>简单说，远程路径只是把**&quot;压缩&quot;这一步**外包给了 OpenAI 服务器，前处理和后处理仍由客户端完成。这种设计的优势在于：OpenAI 服务端很可能对这个端点做了专门优化（比如使用更经济的模型或内部缓存），这些是客户端走通用 API 做不到的。这体现了 OpenAI 对自家基础设施的垂直整合。</p>
<h3 id="压缩的具体流程" tabindex="-1">压缩的具体流程 <a class="header-anchor" href="#压缩的具体流程" aria-label="Permalink to &quot;压缩的具体流程&quot;">&ZeroWidthSpace;</a></h3>
<p>当走本地路径时，Codex 会先提取最近的用户消息（硬上限约 20,000 tokens），然后发送一段简短的 Summarization Prompt 给 LLM。这段 Prompt 只有 4 个核心要点：</p>
<blockquote>
<p>你正在执行一次&quot;上下文检查点压缩&quot;。请为另一个将接续任务的 LLM 生成一份交接摘要，包含：当前进展和关键决策、重要的约束和用户偏好、剩余待办事项、继续工作所需的关键数据。</p>
</blockquote>
<p>关键词是**&quot;交接&quot;（Handoff）**——它不是在写会议纪要，而是在写一份让下一个人（模型）能直接上手的工作简报。</p>
<p>用我们的登录 Bug 场景来看：</p>
<p><img src="https://oss.justin3go.com/blogs/codex-compression.png" alt="Codex CLI 压缩前后对比"></p>
<p><strong>思路拆解：</strong></p>
<p>注意看压缩前后的变化——所有消息变成了 4 条。Codex 极其尊重&quot;用户意图&quot;，它会物理删除所有的 Assistant 回复和 Tool 相关消息，但会<strong>原封不动地保留所有 User 消息</strong>（#2 和 #10）。</p>
<p>随后，它插入一条伪造的 Assistant 消息，内容是一份结构化的交接总结。这份总结包含了任务目标、已完成项、关键架构决策和剩余待办。对于新模型来说，它不需要看那些大段的文件内容和测试堆栈，它只需要知道&quot;测试已经修好了&quot;就足够了。</p>
<h3 id="自动触发与兜底" tabindex="-1">自动触发与兜底 <a class="header-anchor" href="#自动触发与兜底" aria-label="Permalink to &quot;自动触发与兜底&quot;">&ZeroWidthSpace;</a></h3>
<p>当 Token 用量接近模型上下文窗口上限时，Codex 会自动触发压缩（不需要用户手动执行 <code>/compact</code>）。如果压缩后空间还是不够，它会退而采取更激进的&quot;头部修剪&quot;——直接从最早的消息开始砍，确保对话能继续下去。</p>
<p>笔者觉得 Codex 的方案最大的优点是<strong>直觉性</strong>：交接摘要这个概念每个职场人都能理解。缺点是它比较&quot;一刀切&quot;——所有 AI 回复和工具结果都被替换成一段摘要，如果那段摘要遗漏了某个关键细节，就真的找不回来了。</p>
<h2 id="claude-code-三层递进的-精密遗忘" tabindex="-1">Claude Code：三层递进的&quot;精密遗忘&quot; <a class="header-anchor" href="#claude-code-三层递进的-精密遗忘" aria-label="Permalink to &quot;Claude Code：三层递进的&quot;精密遗忘&quot;&quot;">&ZeroWidthSpace;</a></h2>
<p>Anthropic 出品的 Claude Code 逻辑更为细腻。它不追求一步到位的物理删除，而是设计了<strong>三层逐级加强的清理机制</strong>——从轻到重，能不动 LLM 就不动 LLM。</p>
<blockquote>
<p>注：Claude Code 非开源项目，以下分析基于社区逆向工程和公开资料，具体实现可能随版本变化。</p>
</blockquote>
<h3 id="第一层-工具结果修剪-无-llm-开销" tabindex="-1">第一层：工具结果修剪（无 LLM 开销） <a class="header-anchor" href="#第一层-工具结果修剪-无-llm-开销" aria-label="Permalink to &quot;第一层：工具结果修剪（无 LLM 开销）&quot;">&ZeroWidthSpace;</a></h3>
<p>这是最频繁、也最轻量的一层。<strong>不需要调用 LLM</strong>，纯粹是本地的规则引擎。它在每次请求前都会自动执行。</p>
<p>它的逻辑很简单：</p>
<ul>
<li>始终保护最近若干个工具调用的结果（正在用的东西不能删）</li>
<li>超出保护范围的旧工具结果 → 替换为 <code>[Old tool result content cleared]</code> 占位符</li>
</ul>
<p>用我们的场景来看：</p>
<p><img src="https://oss.justin3go.com/blogs/claude-layer1.png" alt="Claude Code 第一层压缩"></p>
<p>这种做法极其聪明：它维护了 AI 的&quot;心流&quot;。AI 记得自己搜过代码（#4 的 tool_call 还在），也记得自己读过文件（#7 的 tool_call 还在），只是不记得搜到了什么、文件内容是什么。如果它之后真的需要再次查看，它会自己重新发起 <code>read_file</code>。</p>
<p>笔者认为这一层的设计极为精妙——它实现了**&quot;选择性失忆&quot;而非&quot;全面遗忘&quot;**。就像你记得去年读过一本好书，但忘了具体内容，需要的时候再翻就好。</p>
<h3 id="第二层-缓存友好策略-prompt-cache" tabindex="-1">第二层：缓存友好策略（Prompt Cache） <a class="header-anchor" href="#第二层-缓存友好策略-prompt-cache" aria-label="Permalink to &quot;第二层：缓存友好策略（Prompt Cache）&quot;">&ZeroWidthSpace;</a></h3>
<p>这是 Claude Code 的看家本领，也是三者中<strong>独有的差异化优势</strong>。</p>
<p>Anthropic 的 API 支持 Prompt Cache——如果你发给 API 的消息前缀和上一次请求相同，服务器可以复用之前的计算结果，大幅降低成本和延迟。</p>
<p>这意味着什么？在清理消息时，Claude Code 会尽量避免修改消息序列的前半部分。它采用&quot;手术式&quot;方案：只在尾部进行修整，确保消息开头部分保持绝对一致。这样做的代价是清理效率略低，但换来的是<strong>缓存命中率的最大化</strong>。</p>
<p>用我们的场景来看。假设经过第一层清理后，消息序列是 #1-#26（工具结果已替换为占位符）。现在上下文仍然超标，需要进一步裁剪。一个&quot;朴素&quot;的做法是从最早的消息开始删——但 Claude Code <strong>不这么干</strong>：</p>
<p><img src="https://oss.justin3go.com/blogs/claude-cache-strategy.png" alt="缓存策略对比"></p>
<p>左边的朴素策略虽然删掉了最旧的消息，看起来很合理，但代价是<strong>整个前缀都变了</strong>——API 缓存全部失效，下次请求要从头计算。右边的 Claude Code 策略则相反：它宁可少删一些，也要保证消息序列的<strong>前缀部分和上一次请求完全相同</strong>，让 Anthropic API 的 Prompt Cache 能够命中。</p>
<p>在长时间运行的任务中（比如你连续让 AI 帮你重构一整个模块），这种策略能带来可观的成本节省——因为每次 API 请求的大部分内容都能命中缓存，只需要为新增的尾部内容付费。</p>
<h3 id="第三层-9-部分结构化-llm-总结-最后手段" tabindex="-1">第三层：9 部分结构化 LLM 总结（最后手段） <a class="header-anchor" href="#第三层-9-部分结构化-llm-总结-最后手段" aria-label="Permalink to &quot;第三层：9 部分结构化 LLM 总结（最后手段）&quot;">&ZeroWidthSpace;</a></h3>
<p>当前两层都无法阻止上下文继续膨胀时，系统触发最终的全量总结。根据源码，自动压缩的触发阈值为 <code>有效上下文窗口 - 13,000 tokens</code>（其中有效窗口 = 模型上下文窗口 - min(最大输出 tokens, 20,000)）。</p>
<p>不过，即使达到了阈值，系统也不会直接跳到 LLM 总结。自动压缩触发时，系统会<strong>优先尝试 Session Memory Compact</strong>——利用 session memory（会话记忆）中已有的结构化信息来替代完整的 LLM 调用。这意味着大多数自动压缩甚至不需要 LLM 调用。只有当 session memory 路径不可用或不够时，系统才会回退到传统的 LLM 总结流程，生成一份<strong>包含 9 个固定部分的结构化摘要</strong>：</p>
<ol>
<li>用户的原始意图</li>
<li>核心技术概念</li>
<li>关注的文件和代码</li>
<li>遇到的错误及修复方式</li>
<li>解决问题的逻辑链</li>
<li>所有用户消息的摘要</li>
<li>待办事项</li>
<li>当前正在做什么</li>
<li>建议的下一步</li>
</ol>
<p>这份摘要的要求极其严格——Prompt 中会要求模型<strong>直接引用原文关键短语</strong>，而不是全部用自己的话改写。这是为了防止&quot;语境漂移&quot;（模型在复述过程中微妙地偏离原意）。</p>
<p>用我们的场景来看：</p>
<p><img src="https://oss.justin3go.com/blogs/claude-layer3.png" alt="Claude Code 第三层压缩"></p>
<p>压缩完成后，Claude Code 还会做一系列善后工作，笔者把它叫做**&quot;状态重构&quot;**：</p>
<ul>
<li>在新对话开头注入引导语（&quot;本次会话延续自上一段对话...&quot;）</li>
<li><strong>自动重新读取</strong>最近编辑过的文件（最多 5 个文件，总预算 50,000 tokens，单文件上限 5,000 tokens），确保 AI 手里有最新代码</li>
<li>重新声明工具和技能定义</li>
<li><code>CLAUDE.md</code> 中的项目规范作为系统提示语的一部分，始终常驻，不受压缩影响</li>
</ul>
<p>用户还可以在手动压缩时附加自定义指令，比如 <code>/compact Focus on API changes</code>，引导压缩侧重于特定方向。</p>
<p>此外，系统还有一条<strong>被动兜底路径</strong>：当 API 返回 <code>prompt_too_long</code> 错误时，系统会自动启动一次反应式压缩并重试请求，确保用户不会因为上下文溢出而直接遇到错误中断。同时，为防止压缩反复失败导致的死循环，连续 3 次自动压缩失败后系统会暂停自动压缩功能。</p>
<p>Claude Code 的方案是三者中最复杂的，但也是最&quot;省钱&quot;的——大多数时候它只需要执行第一层的规则引擎清理，或者通过 Session Memory 路径完成压缩，根本不需要额外的 LLM 调用。</p>
<h2 id="opencode-先修剪-再摘要的-阶梯治理" tabindex="-1">OpenCode：先修剪，再摘要的&quot;阶梯治理&quot; <a class="header-anchor" href="#opencode-先修剪-再摘要的-阶梯治理" aria-label="Permalink to &quot;OpenCode：先修剪，再摘要的&quot;阶梯治理&quot;&quot;">&ZeroWidthSpace;</a></h2>
<p>开源界的新秀 OpenCode（<a href="https://github.com/anomalyco/opencode" target="_blank" rel="noreferrer">源码</a>，TypeScript + Effect-TS 实现）则提供了一种更为平衡的策略。它在 <code>session/compaction.ts</code> 中实现了一套阶梯式的治理流程：<strong>先用低成本手段尽可能腾空间，实在不够再动用 LLM。</strong></p>
<h3 id="第一步-prune-标记隐藏-非物理删除" tabindex="-1">第一步：Prune（标记隐藏，非物理删除） <a class="header-anchor" href="#第一步-prune-标记隐藏-非物理删除" aria-label="Permalink to &quot;第一步：Prune（标记隐藏，非物理删除）&quot;">&ZeroWidthSpace;</a></h3>
<p>OpenCode 的第一个动作不是删除，而是&quot;标记&quot;。它的规则非常清晰：</p>
<ul>
<li>只有当修剪能释放超过 20,000 tokens 时才执行（小修小补不值得折腾）</li>
<li>始终保留最近的 40,000 tokens 作为&quot;安全垫&quot;（正在进行的工作不能动）</li>
<li><code>skill</code> 类型的工具输出永远不修剪（因为里面包含操作指令）</li>
<li>保护最近 2 个用户回合的完整内容</li>
</ul>
<p><strong>关键设计</strong>：和 Claude Code 的占位符替换不同，OpenCode 的修剪<strong>不是物理删除</strong>，而是给旧消息打上一个 <code>compacted = Date.now()</code> 的时间戳标记，让它们在后续请求中&quot;不可见&quot;。数据其实还在数据库里，只是被隐藏了。</p>
<p><img src="https://oss.justin3go.com/blogs/opencode-prune.png" alt="OpenCode Prune"></p>
<p><strong>关键点：</strong> 数据并没有真正丢掉。这为未来可能的历史回溯功能留下了空间——如果开发者需要审计，或者 Agent 触发了某种回溯逻辑，这些数据是可以被重新拉回上下文的。这是一个很有前瞻性的设计。</p>
<h3 id="第二步-llm-5-标题摘要" tabindex="-1">第二步：LLM 5 标题摘要 <a class="header-anchor" href="#第二步-llm-5-标题摘要" aria-label="Permalink to &quot;第二步：LLM 5 标题摘要&quot;">&ZeroWidthSpace;</a></h3>
<p>如果 Prune 之后还是太臃肿，OpenCode 会用一个隐藏的、专门的 Agent（不干扰用户当前的交互）来调用 LLM 生成一份摘要。这份摘要有一个固定的 5 标题结构：</p>
<p><img src="https://oss.justin3go.com/blogs/opencode-summary.png" alt="OpenCode LLM 摘要"></p>
<p>OpenCode 在摘要后有一个非常温馨的设计：它会自动<strong>重放最后一条用户消息</strong>。这能确保 Agent 的最后记忆点始终停留在用户的最新指令上，而不是停留在一段冷冰冰的摘要总结里。用户完全感知不到压缩的发生——你说的最后一句话会被重新发送，AI 继续回答，好像什么都没发生过。</p>
<p>另一个亮点：<strong>OpenCode 会跟随用户的语言</strong>。如果你一直用中文交流，它的摘要也会是中文的。这对非英语母语的开发者来说，是一个很友好的设计。</p>
<p>笔者觉得 OpenCode 的方案在三者中最&quot;开发者友好&quot;——代码全开源（TypeScript），架构现代（Effect-TS），非物理删除的设计为扩展留足空间。如果你想深度定制压缩行为，OpenCode 是最容易上手的。</p>
<h2 id="三剑客同台竞技" tabindex="-1">三剑客同台竞技 <a class="header-anchor" href="#三剑客同台竞技" aria-label="Permalink to &quot;三剑客同台竞技&quot;">&ZeroWidthSpace;</a></h2>
<p>我们将三者的方案放在一起并排观察：</p>
<p>输入：26 条消息, ~15,400 tokens（同一个&quot;修登录 bug&quot;场景）</p>
<p><img src="https://oss.justin3go.com/blogs/three-comparison.png" alt="三剑客对比"></p>
<table tabindex="0">
<thead>
<tr>
<th style="text-align:left">维度</th>
<th style="text-align:left">Codex CLI</th>
<th style="text-align:left">Claude Code</th>
<th style="text-align:left">OpenCode</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left"><strong>压缩层次</strong></td>
<td style="text-align:left">单层（摘要）</td>
<td style="text-align:left">三层（修剪/缓存/摘要）</td>
<td style="text-align:left">两层（隐藏/摘要）</td>
</tr>
<tr>
<td style="text-align:left"><strong>LLM 调用</strong></td>
<td style="text-align:left">必须</td>
<td style="text-align:left">仅在第三层</td>
<td style="text-align:left">仅在第二步</td>
</tr>
<tr>
<td style="text-align:left"><strong>用户消息</strong></td>
<td style="text-align:left">永久保留原始内容</td>
<td style="text-align:left">摘要化（第三层）</td>
<td style="text-align:left">摘要化 + 重放最后一条</td>
</tr>
<tr>
<td style="text-align:left"><strong>工具结果处理</strong></td>
<td style="text-align:left">物理删除</td>
<td style="text-align:left">占位符替换</td>
<td style="text-align:left">时间戳标记隐藏</td>
</tr>
<tr>
<td style="text-align:left"><strong>缓存优化</strong></td>
<td style="text-align:left">无特殊设计</td>
<td style="text-align:left">深度集成 Prompt Cache</td>
<td style="text-align:left">侧重减少重复读取</td>
</tr>
<tr>
<td style="text-align:left"><strong>压缩后行为</strong></td>
<td style="text-align:left">被动等待</td>
<td style="text-align:left">主动重读相关文件</td>
<td style="text-align:left">自动重放最后指令</td>
</tr>
</tbody>
</table>
<h3 id="一些值得展开说的差异" tabindex="-1">一些值得展开说的差异 <a class="header-anchor" href="#一些值得展开说的差异" aria-label="Permalink to &quot;一些值得展开说的差异&quot;">&ZeroWidthSpace;</a></h3>
<p><strong>关于&quot;要不要保留用户原话&quot;</strong>：Codex 选择保留用户消息、只压缩模型回复，这样做的好处是 AI 永远能回看你说过什么，但代价是当用户消息本身很长时，压缩效率会打折扣。Claude Code 和 OpenCode 则选择全部压缩为摘要，更激进但更节省空间。</p>
<p><strong>关于缓存</strong>：这是 Claude Code 最独特的优势。其他两家在压缩后，API 请求的内容会发生很大变化，之前的缓存基本作废。而 Claude Code 刻意维持消息前缀的稳定性，使得压缩后的请求依然能复用之前的缓存。在长时间运行的任务中，这意味着可观的成本节省。</p>
<p><strong>关于&quot;非物理删除&quot;</strong>：OpenCode 的时间戳标记方式是个很有前瞻性的设计。虽然当前版本并没有实现历史回溯功能，但数据没有真正丢失，为未来留下了可能性。而 Codex 和 Claude Code 的压缩都是不可逆的。</p>
<h2 id="最后" tabindex="-1">最后 <a class="header-anchor" href="#最后" aria-label="Permalink to &quot;最后&quot;">&ZeroWidthSpace;</a></h2>
<p>如果用一个类比来形容这三位：</p>
<ul>
<li><strong>Codex CLI</strong> 像是一个写<strong>交接单</strong>的资深员工。他直接撕掉之前的草稿纸，给你一张写的清清楚楚的现状说明，虽然简单粗暴，但非常有效。</li>
<li><strong>Claude Code</strong> 像是一个拥有<strong>精密遗忘</strong>能力的学者。他优先划掉书上的细碎批注，只有在书架实在堆不下时，才会把整本书浓缩成一页大纲。他非常在意翻书的效率（缓存）。</li>
<li><strong>OpenCode</strong> 像是一个务实的<strong>阶梯治理</strong>者。他先给旧文件打包贴上标签（隐藏），实在不行才做总结。他最贴心的地方在于，总结完后还会提醒你：&quot;你刚才最后说的是这件事对吧？&quot;</li>
</ul>
<p>归根结底，在 2026 年，最好的上下文管理并不是无止境地扩大 LLM 的记忆容量，而是学会如何<strong>精密地遗忘</strong>。毕竟，一个什么都记得住的 Agent，往往也最容易被噪音干扰。</p>
<hr>
<p><strong>参考来源：</strong></p>
<ul>
<li>Codex CLI: <a href="https://github.com/openai/codex" target="_blank" rel="noreferrer">openai/codex</a> (参考 <code>codex-rs/core/src/compact.rs</code>)</li>
<li>Claude Code 社区资料：
<ul>
<li><a href="https://github.com/Piebald-AI/claude-code-system-prompts" target="_blank" rel="noreferrer">Claude Code System Prompts</a></li>
<li><a href="https://gist.github.com/sam-saffron-jarvis/9d8e291c4e696ac7948702d6c4884448" target="_blank" rel="noreferrer">Sam Saffron's Gist</a></li>
<li><a href="https://barazany.dev/blog/claude-codes-compaction-engine" target="_blank" rel="noreferrer">Claude Code's Compaction Engine</a></li>
<li><a href="https://github.com/yasasbanukaofficial/claude-code" target="_blank" rel="noreferrer">泄露源码仓库</a>（非官方泄露，用于验证修订）</li>
</ul>
</li>
<li>OpenCode: <a href="https://github.com/anomalyco/opencode" target="_blank" rel="noreferrer">anomalyco/opencode</a> (参考 <code>packages/opencode/src/session/compaction.ts</code>)</li>
</ul>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
        <item>
            <title><![CDATA[我把 Harness Engineering 也提炼成了 SKILL]]></title>
            <link>https://justin3go.com/posts/2026/04/03-harness-engineering-distilled-into-a-skill</link>
            <guid>https://justin3go.com/posts/2026/04/03-harness-engineering-distilled-into-a-skill</guid>
            <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="我把-harness-engineering-也提炼成了-skill" tabindex="-1">我把 Harness Engineering 也提炼成了 SKILL <a class="header-anchor" href="#我把-harness-engineering-也提炼成了-skill" aria-label="Permalink to &quot;我把 Harness Engineering 也提炼成了 SKILL&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<blockquote></blockquote>
<p>笔者分享了将 Harness Engineering 知识提炼为可复用 Agent Skill 的经验。在系统阅读了 Anthropic、OpenAI、Martin Fowler、LangChain 等来源的文章后，提炼出 Harness 设计的七个核心层：项目搭建、上下文工程、约束与防护、多 Agent 架构、评估与反馈、长时间任务、诊断。最终产出的 <code>harness-engineering</code> 技能覆盖三大场景——新项目搭建、Agent 行为诊断、持续改进，采用渐进式披露架构。定量评估显示有技能时断言通过率 100%，无技能时 83%。核心洞察：<strong>Agent 表现不好，80% 的原因不在模型，在 Harness。</strong></p>
<blockquote></blockquote>
<!-- DESC SEP -->
<h2 id="为什么写这个" tabindex="-1">为什么写这个 <a class="header-anchor" href="#为什么写这个" aria-label="Permalink to &quot;为什么写这个&quot;">&ZeroWidthSpace;</a></h2>
<p>最近两年，笔者在使用各种AI编码助手（Claude Code、Cursor、Copilot等）的过程中，反复遇到一个问题：<strong>Agent时好时坏</strong>，虽然整体来说随着模型能力进步是向好的，但是向好的过程是曲折波动的。</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201320478.png" alt=""></p>
<p>有时候它写的代码完美契合项目风格，有时候它像个第一天入职的实习生——不知道项目结构、不遵守约定、还把之前商量好的决策忘得一干二净。</p>
<p>然后开始从 Prompt Engineering 中使用结构化、few shot、few example 等技巧，来让 AI 的输出更加稳定。
后面又使用 Context Engineering 来让 Agent 的上下文更加丰富，来让 Agent 的表现更加稳定。</p>
<p>最近几周，一个更系统的词汇出现了：Harness Engineering。</p>
<blockquote>
<p><strong>Agent表现不好，80%的原因不在模型，在Harness。</strong> - Anthropic</p>
</blockquote>
<p>什么是Harness？简单说：</p>
<ul>
<li><strong>模型 = CPU</strong>（算力本身）</li>
<li><strong>上下文窗口 = RAM</strong>（工作记忆）</li>
<li><strong>Harness = 操作系统</strong>（调度、约束、反馈、文件系统——一切让CPU有效工作的基础设施）</li>
</ul>
<p>你不会指望一个CPU在没有操作系统的裸机上高效运行。同理，你也不该指望一个模型在没有Harness的项目里稳定输出。</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201256128.png" alt=""></p>
<h2 id="我学到了什么" tabindex="-1">我学到了什么 <a class="header-anchor" href="#我学到了什么" aria-label="Permalink to &quot;我学到了什么&quot;">&ZeroWidthSpace;</a></h2>
<p>笔者系统阅读了以下来源的文章：</p>
<ul>
<li><strong>Anthropic</strong> — 构建高效Agent、多Agent研究系统、长时间运行Agent的Harness设计</li>
<li><strong>OpenAI</strong> — AGENTS.md设计模式、Context Engineering最佳实践</li>
<li><strong>Martin Fowler</strong> — Harness Engineering的工程哲学（&quot;Relocating Rigor&quot;）</li>
<li><strong>LangChain</strong> — Agent框架 vs 运行时 vs Harness的分类学</li>
<li><strong>philschmid</strong> — 2026年Agent Harness的重要性</li>
<li><strong>独立开发者实践</strong> — Hermes Agent的自演化、Vue Lynx的设计笔记驱动开发</li>
<li><strong>学术论文</strong> — 自然语言Agent Harness的形式化研究</li>
</ul>
<p>读完之后，我发现这些文章虽然角度各异，但核心思想收敛到了<strong>七个层</strong>：</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201232141.png" alt=""></p>
<p>| 层级 | 解决什么问题 | 一句话总结 |
|</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="我把-harness-engineering-也提炼成了-skill" tabindex="-1">我把 Harness Engineering 也提炼成了 SKILL <a class="header-anchor" href="#我把-harness-engineering-也提炼成了-skill" aria-label="Permalink to &quot;我把 Harness Engineering 也提炼成了 SKILL&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<blockquote></blockquote>
<p>笔者分享了将 Harness Engineering 知识提炼为可复用 Agent Skill 的经验。在系统阅读了 Anthropic、OpenAI、Martin Fowler、LangChain 等来源的文章后，提炼出 Harness 设计的七个核心层：项目搭建、上下文工程、约束与防护、多 Agent 架构、评估与反馈、长时间任务、诊断。最终产出的 <code>harness-engineering</code> 技能覆盖三大场景——新项目搭建、Agent 行为诊断、持续改进，采用渐进式披露架构。定量评估显示有技能时断言通过率 100%，无技能时 83%。核心洞察：<strong>Agent 表现不好，80% 的原因不在模型，在 Harness。</strong></p>
<blockquote></blockquote>
<!-- DESC SEP -->
<h2 id="为什么写这个" tabindex="-1">为什么写这个 <a class="header-anchor" href="#为什么写这个" aria-label="Permalink to &quot;为什么写这个&quot;">&ZeroWidthSpace;</a></h2>
<p>最近两年，笔者在使用各种AI编码助手（Claude Code、Cursor、Copilot等）的过程中，反复遇到一个问题：<strong>Agent时好时坏</strong>，虽然整体来说随着模型能力进步是向好的，但是向好的过程是曲折波动的。</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201320478.png" alt=""></p>
<p>有时候它写的代码完美契合项目风格，有时候它像个第一天入职的实习生——不知道项目结构、不遵守约定、还把之前商量好的决策忘得一干二净。</p>
<p>然后开始从 Prompt Engineering 中使用结构化、few shot、few example 等技巧，来让 AI 的输出更加稳定。
后面又使用 Context Engineering 来让 Agent 的上下文更加丰富，来让 Agent 的表现更加稳定。</p>
<p>最近几周，一个更系统的词汇出现了：Harness Engineering。</p>
<blockquote>
<p><strong>Agent表现不好，80%的原因不在模型，在Harness。</strong> - Anthropic</p>
</blockquote>
<p>什么是Harness？简单说：</p>
<ul>
<li><strong>模型 = CPU</strong>（算力本身）</li>
<li><strong>上下文窗口 = RAM</strong>（工作记忆）</li>
<li><strong>Harness = 操作系统</strong>（调度、约束、反馈、文件系统——一切让CPU有效工作的基础设施）</li>
</ul>
<p>你不会指望一个CPU在没有操作系统的裸机上高效运行。同理，你也不该指望一个模型在没有Harness的项目里稳定输出。</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201256128.png" alt=""></p>
<h2 id="我学到了什么" tabindex="-1">我学到了什么 <a class="header-anchor" href="#我学到了什么" aria-label="Permalink to &quot;我学到了什么&quot;">&ZeroWidthSpace;</a></h2>
<p>笔者系统阅读了以下来源的文章：</p>
<ul>
<li><strong>Anthropic</strong> — 构建高效Agent、多Agent研究系统、长时间运行Agent的Harness设计</li>
<li><strong>OpenAI</strong> — AGENTS.md设计模式、Context Engineering最佳实践</li>
<li><strong>Martin Fowler</strong> — Harness Engineering的工程哲学（&quot;Relocating Rigor&quot;）</li>
<li><strong>LangChain</strong> — Agent框架 vs 运行时 vs Harness的分类学</li>
<li><strong>philschmid</strong> — 2026年Agent Harness的重要性</li>
<li><strong>独立开发者实践</strong> — Hermes Agent的自演化、Vue Lynx的设计笔记驱动开发</li>
<li><strong>学术论文</strong> — 自然语言Agent Harness的形式化研究</li>
</ul>
<p>读完之后，我发现这些文章虽然角度各异，但核心思想收敛到了<strong>七个层</strong>：</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201232141.png" alt=""></p>
<table tabindex="0">
<thead>
<tr>
<th>层级</th>
<th>解决什么问题</th>
<th>一句话总结</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>项目搭建</strong></td>
<td>Agent不知道项目是什么</td>
<td>AGENTS.md是目录，不是百科全书</td>
</tr>
<tr>
<td><strong>上下文工程</strong></td>
<td>Agent看到的信息不对</td>
<td>给地图，不给手册</td>
</tr>
<tr>
<td><strong>约束与防护</strong></td>
<td>Agent犯重复的错</td>
<td>每犯一次错，加一条规则</td>
</tr>
<tr>
<td><strong>多Agent架构</strong></td>
<td>单Agent搞不定复杂任务</td>
<td>分工明确，协议清晰</td>
</tr>
<tr>
<td><strong>评估与反馈</strong></td>
<td>不知道Agent做得好不好</td>
<td>让AI检查AI</td>
</tr>
<tr>
<td><strong>长时间任务</strong></td>
<td>Agent跑着跑着就走偏了</td>
<td>进度文件 + 上下文重置</td>
</tr>
<tr>
<td><strong>诊断</strong></td>
<td>用户骂Agent不好用</td>
<td>问题在Harness，不在模型</td>
</tr>
</tbody>
</table>
<h2 id="所以我做了个技能" tabindex="-1">所以我做了个技能 <a class="header-anchor" href="#所以我做了个技能" aria-label="Permalink to &quot;所以我做了个技能&quot;">&ZeroWidthSpace;</a></h2>
<p>读完这些文章，笔者意识到这些模式完全是<strong>可复用</strong>的。不管你的项目是React前端、Python后端还是Rust CLI工具——Harness的设计原则是通用的。</p>
<p>于是我把这些知识提炼成了一个 <strong>Agent Skill</strong>，名叫 <code>harness-engineering</code>。</p>
<h3 id="它做什么" tabindex="-1">它做什么 <a class="header-anchor" href="#它做什么" aria-label="Permalink to &quot;它做什么&quot;">&ZeroWidthSpace;</a></h3>
<p>这个技能有三个核心使用场景：</p>
<p><strong>场景一：新项目搭建</strong></p>
<p>当你启动一个新项目，告诉Agent&quot;帮我搭建Harness工程&quot;，它会：</p>
<ol>
<li>评估你的项目类型、技术栈、团队规模</li>
<li>创建 <code>AGENTS.md</code>（表of目录式的Agent导航文件）</li>
<li>建立 <code>docs/</code> 目录（架构、约定、数据模型等）</li>
<li>配置约束层（lint规则、类型检查、pre-commit hooks）</li>
<li>设置评估与反馈机制</li>
</ol>
<p><strong>场景二：Agent表现不佳时的诊断</strong></p>
<p>这是最有意思的场景。当你开始抱怨——</p>
<ul>
<li>&quot;它怎么又犯同样的错误？&quot;</li>
<li>&quot;它根本不遵守我们的约定！&quot;</li>
<li>&quot;它写的代码质量太差了&quot;</li>
</ul>
<p>这个技能会被触发，引导Agent去诊断<strong>Harness层的缺失</strong>，而不是怪模型：</p>
<table tabindex="0">
<thead>
<tr>
<th>你的抱怨</th>
<th>大概率原因</th>
<th>修复方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>总犯同一个错</td>
<td>没有约束阻止它</td>
<td>加一条lint规则</td>
</tr>
<tr>
<td>不遵守约定</td>
<td>约定没写下来或Agent找不到</td>
<td>写入docs/，在AGENTS.md中引用</td>
</tr>
<tr>
<td>忘记之前的决定</td>
<td>跨会话上下文未持久化</td>
<td>用progress.md记录决策</td>
</tr>
<tr>
<td>代码质量差</td>
<td>没有好代码的示例</td>
<td>在DESIGN_NOTES.md中加示例</td>
</tr>
</tbody>
</table>
<p><strong>场景三：持续改进</strong></p>
<p>每次发现新的可复用Harness模式，更新到技能中，让它在其他项目中也能受益。</p>
<h3 id="它怎么组织的" tabindex="-1">它怎么组织的 <a class="header-anchor" href="#它怎么组织的" aria-label="Permalink to &quot;它怎么组织的&quot;">&ZeroWidthSpace;</a></h3>
<p>技能采用<strong>渐进式加载</strong>架构：</p>
<div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>harness-engineering/</span></span>
<span class="line"><span>├── SKILL.md              # 入口文件（&#x3C;60行），路由到具体参考文档</span></span>
<span class="line"><span>└── references/</span></span>
<span class="line"><span>    ├── 01-project-setup.md       # 项目搭建</span></span>
<span class="line"><span>    ├── 02-context-engineering.md  # 上下文工程</span></span>
<span class="line"><span>    ├── 03-constraints.md          # 约束与防护</span></span>
<span class="line"><span>    ├── 04-multi-agent.md          # 多Agent架构</span></span>
<span class="line"><span>    ├── 05-eval-feedback.md        # 评估与反馈</span></span>
<span class="line"><span>    ├── 06-long-running.md         # 长时间任务</span></span>
<span class="line"><span>    └── 07-diagnosis.md            # 诊断</span></span></code></pre>
</div><p>SKILL.md本身非常精简——它就像一个路由器，根据当前场景指引Agent去读对应的参考文档。这遵循了Harness Engineering本身的原则：<strong>渐进式披露，按需加载</strong>。</p>
<h2 id="几个让我印象深刻的模式" tabindex="-1">几个让我印象深刻的模式 <a class="header-anchor" href="#几个让我印象深刻的模式" aria-label="Permalink to &quot;几个让我印象深刻的模式&quot;">&ZeroWidthSpace;</a></h2>
<p>有几个模式特别触动笔者，感同身受，这里单独拿出来聊聊。</p>
<h3 id="给地图-不给手册" tabindex="-1">&quot;给地图，不给手册&quot; <a class="header-anchor" href="#给地图-不给手册" aria-label="Permalink to &quot;&quot;给地图，不给手册&quot;&quot;">&ZeroWidthSpace;</a></h3>
<p>这个观点从推文中看到。传统做法是给Agent写详细的分步指令（手册），但这让Agent变得脆弱——任何偏差都会导致它不知所措。</p>
<p>更好的做法是给Agent一张<strong>地图</strong>：</p>
<div class="language-markdown vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># 不好的写法（手册）</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Step 1: 打开 src/auth/login.ts</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Step 2: 找到 handleLogin 函数</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Step 3: 在第42行添加...</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># 好的写法（地图）</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Auth系统在 src/auth/。登录流程：login.ts → validate.ts → session.ts。</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">限流中间件在 src/middleware/rateLimit.ts——参考它的模式。</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">每次修改auth都要在 src/auth/</span><span style="--shiki-light:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold">__tests__</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">/ 里加测试。</span></span></code></pre>
</div><p>地图让Agent能自主导航，手册让它成为脆弱的执行机器。</p>
<h3 id="每犯一次错-加一条规则" tabindex="-1">&quot;每犯一次错，加一条规则&quot; <a class="header-anchor" href="#每犯一次错-加一条规则" aria-label="Permalink to &quot;&quot;每犯一次错，加一条规则&quot;&quot;">&ZeroWidthSpace;</a></h3>
<p>这个模式来自多篇文章的交叉验证。核心思想：</p>
<ol>
<li>Agent犯了一个错</li>
<li>你修复了这个错</li>
<li><strong>然后你加一条规则，永远阻止这类错再次发生</strong></li>
</ol>
<p>这条规则可以是lint规则、类型约束、测试用例，或者只是文档中的一条约定。随着时间推移，Harness积累了越来越多的规则，Agent的错误率对已知模式趋近于零。</p>
<p>这其实就是Martin Fowler说的 <strong>&quot;Relocating Rigor&quot;</strong>——把人类通过Code Review、经验、直觉实施的质量把关，迁移到自动化检查中。Agent在被检查的边界内自由运行。</p>
<p><img src="https://oss.justin3go.com/blogs/20260403201204929.png" alt=""></p>
<h3 id="harness-数据集" tabindex="-1">Harness = 数据集 <a class="header-anchor" href="#harness-数据集" aria-label="Permalink to &quot;Harness = 数据集&quot;">&ZeroWidthSpace;</a></h3>
<p>这个观点来自Anthropic。每次Agent交互都是一个训练信号：</p>
<ul>
<li>它尝试了什么</li>
<li>什么成功了</li>
<li>什么失败了</li>
<li>修复方案是什么</li>
</ul>
<p>这些痕迹（traces）就是你的<strong>竞争优势</strong>。它们是让你的Harness随时间越来越好的数据——不是微调模型，而是优化操作系统。</p>
<h2 id="技能评估-有没有用" tabindex="-1">技能评估：有没有用？ <a class="header-anchor" href="#技能评估-有没有用" aria-label="Permalink to &quot;技能评估：有没有用？&quot;">&ZeroWidthSpace;</a></h2>
<p>笔者遵循skill-creator的流程，对这个技能做了定量评估。设计了3组测试场景，每组跑with-skill和without-skill两个版本：</p>
<table tabindex="0">
<thead>
<tr>
<th>测试场景</th>
<th>有技能</th>
<th>无技能</th>
</tr>
</thead>
<tbody>
<tr>
<td>新项目搭建</td>
<td>6/6 ✅</td>
<td>4/6</td>
</tr>
<tr>
<td>Agent行为诊断</td>
<td>6/6 ✅</td>
<td>5/6</td>
</tr>
<tr>
<td>跨模块依赖问题</td>
<td>6/6 ✅</td>
<td>6/6</td>
</tr>
<tr>
<td><strong>合计</strong></td>
<td><strong>18/18 (100%)</strong></td>
<td><strong>15/18 (83%)</strong></td>
</tr>
</tbody>
</table>
<p>有技能的版本在所有场景下都通过了全部断言。无技能的版本在&quot;新项目搭建&quot;场景下缺失较多——它不知道要创建AGENTS.md、不知道docs/应该怎么组织、不会设置渐进式披露的上下文架构。</p>
<p>当然，17%的差距不算巨大。但关键是：有技能时Agent的输出<strong>一致且完整</strong>，无技能时看运气。对于一个工程实践类技能来说，一致性比偶尔的惊艳更有价值。</p>
<h2 id="怎么安装" tabindex="-1">怎么安装 <a class="header-anchor" href="#怎么安装" aria-label="Permalink to &quot;怎么安装&quot;">&ZeroWidthSpace;</a></h2>
<p>这个技能可通过 GitHub 安装：</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> skills</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 10xChengTu/harness-engineering</span></span></code></pre>
</div><p>安装后，当你在Claude Code、OpenCode或其他支持Skills的Agent中工作时：</p>
<ul>
<li>启动新项目 → 技能自动触发，引导搭建Harness</li>
<li>遇到Agent质量问题 → 开始抱怨时技能会介入诊断</li>
<li>主动询问 → &quot;帮我改进这个项目的Harness&quot;</li>
</ul>
<h2 id="最后" tabindex="-1">最后 <a class="header-anchor" href="#最后" aria-label="Permalink to &quot;最后&quot;">&ZeroWidthSpace;</a></h2>
<p>Harness Engineering目前还是一个非常早期的领域。模型在变强，今天需要的约束明天可能就多余了——所以这个技能本身也遵循一个核心原则：<strong>为删除而构建</strong>。</p>
<p>如果你也在用AI Agent做开发，不妨试试给你的项目加上Harness。从最简单的开始——一个<code>AGENTS.md</code>文件、几条lint规则、一个progress.md。然后观察Agent的表现变化。</p>
<p>你大概率会和笔者有同样的感受：<strong>不是模型不行，是我们没给它一个好的工作环境。</strong></p>
<blockquote>
<p>本文涉及的所有参考文章和完整技能源码，均可在<a href="https://github.com/10xChengTu/harness-engineering" target="_blank" rel="noreferrer">GitHub 仓库</a>中找到。</p>
</blockquote>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
        <item>
            <title><![CDATA[HUNT0 上线了——尽早发布，尽早发现]]></title>
            <link>https://justin3go.com/posts/2026/01/01-hunt0-is-live-ship-early-hunt-early</link>
            <guid>https://justin3go.com/posts/2026/01/01-hunt0-is-live-ship-early-hunt-early</guid>
            <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="hunt0-上线了——尽早发布-尽早发现" tabindex="-1">HUNT0 上线了——尽早发布，尽早发现 <a class="header-anchor" href="#hunt0-上线了——尽早发布-尽早发现" aria-label="Permalink to &quot;HUNT0 上线了——尽早发布，尽早发现&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<blockquote></blockquote>
<p>HUNT0 是一个面向 maker 和 indie hacker 的社区驱动发布目录，想把“发布”变成一件有仪式感、可被发现、可获得反馈的事：你可以预约发布日期，通过声望加权投票与评论拿到早期反馈，再通过榜单与 Explore（按筛选意图组织）快速发现值得关注的新产品。</p>
<blockquote></blockquote>
<p>v1.0.0 先把核心闭环跑通：<strong>launch → discover → vote → discuss → recap/reward</strong>。我们做了实时同步投票状态、结构化的提交流程（免费排队 + Premium Launch）、以及提醒/奖项等自动化机制，目标是减少噪音，让早期发现更“有章可循”。</p>
<blockquote></blockquote>
<!-- DESC SEP -->
<blockquote>
<p>一切都始于一张有点“丑”的草图：一把向前倾斜的梯子。</p>
</blockquote>
<p>这张梯子草图最终成为了 HUNT0 的 logo。它提醒我们：进步是一格一格爬出来的——与其等完美，不如先把不完美的东西发出去。</p>
<p><img src="https://storage.hunt0.com/about-logo.png" alt="那张后来变成 HUNT0 logo 的原始梯子草图"></p>
<p>这也是口号 <strong>“Ship Early, Hunt Early”</strong> 的由来：创作者尽早发布，社区才能尽早发现——去浏览、投票、讨论，帮助好产品找到第一批用户。</p>
<p>如果你在公开构建（build in public），欢迎来这里发布；如果你喜欢探索新东西，就从“hunt”开始。</p>
<h2 id="什么是-hunt0" tabindex="-1">什么是 HUNT0？ <a class="header-anchor" href="#什么是-hunt0" aria-label="Permalink to &quot;什么是 HUNT0？&quot;">&ZeroWidthSpace;</a></h2>
<p>HUNT0 是一个社区驱动的发布目录，让“发布”和“发现”同频发生：</p>
<ul>
<li>预约发布日期，把发布变成一件明确的事件</li>
<li>通过投票与评论获得早期反馈</li>
<li>用榜单与 Explore 按时间与兴趣组织“新产品”，而不是按噪音排序</li>
</ul>
<h2 id="上线-v1-0-0-我们做了什么" tabindex="-1">上线 v1.0.0：我们做了什么 <a class="header-anchor" href="#上线-v1-0-0-我们做了什么" aria-label="Permalink to &quot;上线 v1.0.0：我们做了什么&quot;">&ZeroWidthSpace;</a></h2>
<p>v1.0.0 先聚焦在核心闭环：<strong>launch → discover → vote → discuss → recap/reward</strong>。</p>
<h3 id="_1-首页榜单-今天该看什么" tabindex="-1">1）首页榜单：“今天该看什么？” <a class="header-anchor" href="#_1-首页榜单-今天该看什么" aria-label="Permalink to &quot;1）首页榜单：“今天该看什么？”&quot;">&ZeroWidthSpace;</a></h3>
<p>首页按时间窗口组织，方便你快速扫到值得关注的内容：</p>
<ul>
<li><strong>Top Products Launching Today</strong>：今天（UTC）发布的产品</li>
<li><strong>Yesterday / This Week / This Month</strong>：按更长时间窗口回顾</li>
</ul>
<p>投票状态会在页面内 <strong>实时同步</strong>：你在任意位置给某个产品投票，页面上的所有实例会立刻一起更新，无需刷新。</p>
<h3 id="_2-explore-按意图筛选-而不是靠运气" tabindex="-1">2）Explore：按意图筛选，而不是靠运气 <a class="header-anchor" href="#_2-explore-按意图筛选-而不是靠运气" aria-label="Permalink to &quot;2）Explore：按意图筛选，而不是靠运气&quot;">&ZeroWidthSpace;</a></h3>
<p>Explore 支持 <strong>分类、标签（最多 10 个）、时间范围、全文搜索</strong>，并支持分页。</p>
<p>在相同筛选条件下，<strong>Premium Launch</strong> 会在排序上优先于免费发布——在“更需要曝光”的时刻更有效。</p>
<h3 id="_3-产品页-展示、访问、讨论一站式" tabindex="-1">3）产品页：展示、访问、讨论一站式 <a class="header-anchor" href="#_3-产品页-展示、访问、讨论一站式" aria-label="Permalink to &quot;3）产品页：展示、访问、讨论一站式&quot;">&ZeroWidthSpace;</a></h3>
<p>每个产品都有独立详情页，便于更深入地了解与互动：</p>
<ul>
<li>核心信息与外链（Visit）</li>
<li>截图画廊与更长的产品介绍（About）</li>
<li>声望加权的投票与评论</li>
<li>榜单/奖项徽章（例如日榜/周榜/月榜 Top 3）</li>
</ul>
<h3 id="_4-提交-免费排队-premium-launch" tabindex="-1">4）提交：免费排队 + Premium Launch <a class="header-anchor" href="#_4-提交-免费排队-premium-launch" aria-label="Permalink to &quot;4）提交：免费排队 + Premium Launch&quot;">&ZeroWidthSpace;</a></h3>
<p>我们把“发布”设计成一个可控流程，而不只是贴个链接：</p>
<ul>
<li><strong>Free Launch</strong>：每日容量有限（默认 <strong>10 个名额/天</strong>）</li>
<li><strong>Premium Launch</strong>：通过 Stripe Checkout 付费，获得更强曝光</li>
</ul>
<p>如果 Premium 未完成支付，提交会以草稿形态保留在你的 dashboard，不会公开展示，直到支付成功。</p>
<p>提交也支持更丰富的展示信息：</p>
<ul>
<li>最多 3 个分类</li>
<li>最多 10 个标签</li>
<li>联系方式/社交链接（至少 1 个）</li>
<li>logo 与截图，让产品页更完整</li>
</ul>
<h2 id="声望系统-让贡献-有分量" tabindex="-1">声望系统：让贡献“有分量” <a class="header-anchor" href="#声望系统-让贡献-有分量" aria-label="Permalink to &quot;声望系统：让贡献“有分量”&quot;">&ZeroWidthSpace;</a></h2>
<p>投票不是固定的一人一票。HUNT0 使用 <strong>Reputation → Level → Vote Weight</strong>：</p>
<ul>
<li>通过参与获得声望（每日访问、投票、评论、发布）</li>
<li>等级越高，投票权重越大</li>
<li>榜单按加权投票聚合，更多反映可信贡献者的偏好</li>
</ul>
<p>它既是激励机制，也是在早期社区里减少噪音的实用手段。</p>
<h2 id="提醒与奖项-把发布当成-事件" tabindex="-1">提醒与奖项：把发布当成“事件” <a class="header-anchor" href="#提醒与奖项-把发布当成-事件" aria-label="Permalink to &quot;提醒与奖项：把发布当成“事件”&quot;">&ZeroWidthSpace;</a></h2>
<p>为了让发布更有“时刻感”，我们加了一些自动化：</p>
<ul>
<li><strong>发布提醒邮件</strong>：在 UTC 发布日开始前 1 小时发送</li>
<li><strong>日榜/周榜/月榜奖项</strong>：自动计算 Top 3 并通知创作者（可选公开复盘）</li>
</ul>
<h2 id="开始使用" tabindex="-1">开始使用 <a class="header-anchor" href="#开始使用" aria-label="Permalink to &quot;开始使用&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li>如果你是创作者：去 <a href="https://hunt0.com/submit" target="_blank" rel="noreferrer">Submit</a> 预约发布日期</li>
<li>如果你想发现新产品：去 <a href="https://hunt0.com/explore" target="_blank" rel="noreferrer">Explore</a> 按分类/标签筛选</li>
<li>如果你想看 logo 的故事：去 <a href="https://hunt0.com/about" target="_blank" rel="noreferrer">About</a> 看梯子的来源</li>
</ul>
<p>我们会持续迭代发现、榜单和社区激励机制。Launch something, hunt something——也欢迎告诉我们哪里可以做得更好。</p>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
        <item>
            <title><![CDATA[两年后又捣鼓了一个健康类小程序]]></title>
            <link>https://justin3go.com/posts/2025/06/21-health-mini-program-after-two-years</link>
            <guid>https://justin3go.com/posts/2025/06/21-health-mini-program-after-two-years</guid>
            <pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="两年后又捣鼓了一个健康类小程序" tabindex="-1">两年后又捣鼓了一个健康类小程序 <a class="header-anchor" href="#两年后又捣鼓了一个健康类小程序" aria-label="Permalink to &quot;两年后又捣鼓了一个健康类小程序&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<p>笔者在两年后重新捣鼓了一个健康类小程序，这次是对两年前用uniapp做的粗糙版本进行了<strong>全面重构</strong>。由于个人非常认同这个小程序的价值——帮助用户全面了解真实的自己，加上iOS支付限制等原因，最终决定<strong>完全免费</strong>提供给大家使用。</p>
<p>这次重构从UI设计开始，前端使用<strong>Vue Mini</strong>全部重写，后端也进行了不少修改。技术栈上，后端保持Nest.js、GraphQL、Prisma的组合不变，前端则大换血采用Vue Mini，主要是看中了它能用Hook+响应式简化逻辑的特性，同时避免了uniapp的各种坑。</p>
<p>开发过程中实现了：</p>
<ul>
<li><strong>GraphQL请求封装</strong></li>
<li><strong>JWT双token刷新+静默登录</strong></li>
<li><strong>小程序分包优化</strong>主包体积</li>
<li><strong>微信支付集成</strong>（虽然最后因为iOS限制被迫去掉）</li>
</ul>
<p>最让人心累的是各种<strong>审核流程</strong>：域名备案、小程序备案、个体工商户申请、主体变更、微信支付申请、算法备案等等，走了一圈发现iOS不允许虚拟商品内购，最终只能免费提供服务。</p>
<p>尽管反响不大，但总算了却了一个心愿。这个小程序通过让朋友换位思考填写问卷，结合AI智能分析，帮助用户获得更全面的自我认知，笔者认为这对个人成长方向非常重要。</p>
<!-- DESC SEP -->
<h2 id="始" tabindex="-1">始 <a class="header-anchor" href="#始" aria-label="Permalink to &quot;始&quot;">&ZeroWidthSpace;</a></h2>
<p>熟悉我的老老...老朋友应该都知道，这个小程序其实是我两年前做的，当时是用uniapp做的，做得还比较简陋，页面比较粗糙，逻辑上只能用我的流程通过，不然就有BUG那种哈哈。</p>
<p>两年前的博客在这里：<a href="https://justin3go.com/posts/2023/05/07%E4%B8%A4%E4%B8%AA%E5%A4%9A%E6%9C%88%E6%8D%A3%E9%BC%93%E4%BA%86%E4%B8%80%E4%B8%AA%E5%81%A5%E5%BA%B7%E7%B1%BB%E5%B0%8F%E7%A8%8B%E5%BA%8F" target="_blank" rel="noreferrer">两个多月捣鼓了一个健康类小程序</a></p>
<p>个人是非常认同这个小程序的价值的，确确实实希望它能够帮助到更多人全面了解真实的自己，基于此以及其他原因如支付接入iOS限制😓，所以这个小程序最终是完全免费的，如果你觉得其对你有帮助，希望能帮忙多多分享一下。</p>
<p>所以开始重构！这次从UI设计开始，前端用Vue Mini全部重新写了一遍，后端也修改了不少。</p>
<h2 id="看" tabindex="-1">看 <a class="header-anchor" href="#看" aria-label="Permalink to &quot;看&quot;">&ZeroWidthSpace;</a></h2>
<p>先展示一下最终成果：</p>
<blockquote>
<p>也可以到我顺手写的一个落地页查看功能演示：<a href="https://xin2.link" target="_blank" rel="noreferrer">xin2.link</a>，里面也是视频演示及小程序码可以访问</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250621231103.png" alt=""></p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250621231115.png" alt=""></p>
<h2 id="忆" tabindex="-1">忆 <a class="header-anchor" href="#忆" aria-label="Permalink to &quot;忆&quot;">&ZeroWidthSpace;</a></h2>
<p>简单回忆一下这个小程序的背景：</p>
<p>无论是随时间出现的互联网、新冠疫情、短视频、网络社交媒体，还是长期以往存在的如高考、宿舍氛围、人际交往、就业压力等都无时无刻考验着青少年的心理健康，据相关研究表明，出现心理健康问题的青少年不在少数，并且一些心智成熟的成年人同样也面临着心理健康问题的煎熬。</p>
<p>心理健康问题危害极大，国家高度重视，出台了相关政策完善心理健康体系。</p>
<p>但目前的情况仍然有 3 个痛点。分别是普及难、发现难、解决难。而“互联网+心理健康系统”被多篇论文及文章指出是解决心理健康问题的重要途径之一。</p>
<ul>
<li><strong>痛点 1：如何普及心理健康教育，让每一位家长、老师、学生自己都重视起来</strong>。当所有人都能正视并重视心理健康教育的时候，这个问题就能迎刃而解。目前普遍存在的问题就是父母家长教育水平偏低，不够重视心理健康教育，更加关注孩子的学业、就业情况，忽略其本身的发展。学校虽然响应国家政策，实施了一系列活动，如心理班会，心理测评、心理咨询等，但总归趣味性较低，学生参与感较少，积极度不高，即效果较差。让每一个人都重视心理健康教育是一条漫长的道路，需要坚持，不过我们仍然可以通过发现+解决的途径来加强心理健康教育，但由此又引入了痛点 2 和痛点 3 两个新的问题。</li>
<li><strong>痛点 2：如何发现心理健康问题</strong>。学生在填写相关心理调查问卷等时，填写时可能具有片面性和欺骗性。片面性是指由于学生本身缺少相关的心理专业知识，一些心理有问题的学生对于自身的真实情况了解程度也不高，误认为自己心理并没有任何问题；欺骗性本质上也是学生并不重视这方面的调查，认为填写它并不能帮助自己，或者以为自身最近出现的问题是暂时的，并且担心其他人知道自己“有病”，所以填写的结果也就敷衍了事，往好的方向填写。最终调查结果虽然令人满意，但学生的真实问题并没有被发现。</li>
<li><strong>痛点 3：解决心理健康问题。优质心理健康咨询资源不足</strong>，部分落后地区甚至根本无法享受到对应的心理健康咨询资源。心理咨询师通常需要较强的专业能力，丰富的阅历与人生经验，厚积而薄发，培养一个优秀的（能解决问题的）心理咨询师成本较高，即优秀的心理咨询师资源缺乏；并且在心理咨询过程中，<strong>可能会出现线下尴尬的情况</strong>，过度暴露隐私的情况。目前对于已经发现存在心理问题的学生，大多数是沟通能力并不突出，甚至并不愿意沟通交流，所以在对其进行心理辅导时，难以了解真实情况从而对症下药，整个心理辅导过程难以开展。</li>
</ul>
<h2 id="起" tabindex="-1">起 <a class="header-anchor" href="#起" aria-label="Permalink to &quot;起&quot;">&ZeroWidthSpace;</a></h2>
<p>功能上，主要功能保持不变，去除了以前AI对话的功能，去除了几乎没啥用的主页等等一些冗余页面，做了一些减法，即减少工作量，又不至于过多页面拖垮用户的注意力。其次，除了以前的公式计算问卷结果之外，还增加了AI智能分析问卷结果并给出积极建议的小功能。</p>
<p>工程上，大致确定了一下，这次主要的模式是重构前端，后端基本框架不变，只是跟随前端需求进行变化，技术栈也有一定变化，可以看下一章节。</p>
<p>于是，我先使用 MasterGo 画了几个主要的页面（并非全部），之所以不画全部，一方面节约时间，另一方面其他页面也可以参考这几个主要页面来做，也无需每个页面都画出来，大致效果如下：</p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250620093531.png" alt=""></p>
<p>最终效果也不是一比一还原，做的时候也有一定的微调。</p>
<h2 id="栈" tabindex="-1">栈 <a class="header-anchor" href="#栈" aria-label="Permalink to &quot;栈&quot;">&ZeroWidthSpace;</a></h2>
<p>后端技术栈保持不变（甚至都没升级版本），还是Nest.js、GraphQL、Prisma（PostgreSQL）这一套，Nest.js和Prisma不必说，Node写后端的话的经典技术栈。</p>
<p>至于GraphQL，由于我的数据结构是【用户表-&gt;问卷用户多对多表-&gt;问卷表-&gt;题目表】这种结构，嵌套起来使用GraphQL来查询数据时就会特别方便。在重构过程中，很多时候连接口都不必重写，直接改改查询语句就能满足当前的查询需求了，这个在我重构过程中的一些查询变化时特别明显。</p>
<p>前端技术栈大换血，使用了Vue Conf 2024中提到的技术栈Vue Mini，其实如果没有这个技术栈，这次技术选型也多半会直接使用原生语法进行开发，不过既然出现了Vue Mini来增强了原生语法的逻辑层，使其可以使用Hook+响应式来简化重复逻辑及心智负担，那就拍板使用Vue Mini好了。</p>
<p>至于为什么不使用Uniapp：</p>
<ul>
<li>一方面社区评价确实不好，很多坑，我怕...</li>
<li>又套了一层，创建了一个中间层来做Vue组件和小程序的同步，遇到问题排查时我是查微信小程序文档还是Uniapp文档呢，我菜啊～</li>
<li>其配套的一些UI组件库没看到喜欢的，而原生语法就有Vant和TDeisgn这两套较为优秀的组件库可以选择</li>
</ul>
<p>组件库的选择，这个Vant和TDeisgn都可以，个人更偏向于TDesign的颜值。</p>
<h2 id="做" tabindex="-1">做 <a class="header-anchor" href="#做" aria-label="Permalink to &quot;做&quot;">&ZeroWidthSpace;</a></h2>
<p>这里简单描述一下前端基础设施搭建和一些不涉及业务部分的逻辑，后端基础设施的搭建可以参考这个<a href="https://github.com/notiz-dev/nestjs-prisma-starter" target="_blank" rel="noreferrer">Nestjs开源模版</a>，不过也有两年未更新了，框架的版本并不是最新的。</p>
<h3 id="gprahql的封装" tabindex="-1">GprahQL的封装 <a class="header-anchor" href="#gprahql的封装" aria-label="Permalink to &quot;GprahQL的封装&quot;">&ZeroWidthSpace;</a></h3>
<p>没找到比较靠谱的GraphQL请求库，所以就自己封装了一个请求类：</p>
<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { handleLogin } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './handleLogin'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { BASE_URL } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/config'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 定义GraphQL查询的响应类型</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">type</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GraphQLResponse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  data</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  errors</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">message</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }>;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 定义wx.request的选项类型</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">type</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> RequestOptions</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatMiniprogram</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">RequestOption</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 定义我们的GraphQL客户端选项</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">interface</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GraphQLClientOptions</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  url</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 定义GraphQL变量的类型</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> type</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Variables</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Record</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">unknown</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>;</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Header</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> type</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Header</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Record</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GraphQLClient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> url</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  constructor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GraphQLClientOptions</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.url </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> options.url;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> query</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">query</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">variables</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Variables</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">header</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Header</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">request</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>({ query, variables }, header);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> mutate</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">mutation</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">variables</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Variables</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">header</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Header</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">request</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>({ query: mutation, variables }, header);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> request</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">payload</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">query</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">variables</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Variables</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; }, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">header</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Header</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {})</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> handleLogin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(header);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">reject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> RequestOptions</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        url: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.url,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        method: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'POST'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        data: payload,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        header: header,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        success</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">res</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatMiniprogram</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">RequestSuccessCallbackResult</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> response</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> res.data </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GraphQLResponse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (response.errors </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> response.errors.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            reject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(response.errors[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].message));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (response.data) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            resolve</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(response.data);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            reject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'GraphQL response contains no data and no errors.'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        fail</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">err</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatMiniprogram</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">GeneralCallbackResult</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">          reject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`Network error: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">err</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">errMsg</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">request</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(options);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> graphQLClient</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GraphQLClient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  url: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">BASE_URL</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> graphQLClient;</span></span></code></pre>
</div><h3 id="jwt-双token刷新-静默登录" tabindex="-1">JWT 双token刷新+静默登录 <a class="header-anchor" href="#jwt-双token刷新-静默登录" aria-label="Permalink to &quot;JWT 双token刷新+静默登录&quot;">&ZeroWidthSpace;</a></h3>
<p>基本思路就是授权token -&gt; 刷新token -&gt; 登录；放在封装好的graphql请求函数里面。</p>
<ul>
<li>如果有access token，判断它的exp字段是否过期，没有过期就直接请求</li>
<li>过期了就refresh token后再请求</li>
<li>refresh token过期就执行登录请求</li>
<li>双token的好处，登录会额外调用一次外部接口，请求会更慢，所有尽量不使用登录接口</li>
</ul>
<p>稍微需要注意的就是为了避免无限递归，需要做一个简单的标志，就是在refresh和login请求时，不执行这段逻辑，直接请求。</p>
<p>同时我这里增加了一个exp字段方便客户端直接判断token是否过期，而不是多请求一次服务器返回401才知道授权失败。</p>
<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { getExpireInPayload, getToken, setToken } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "@/utils/auth"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useUserInfo } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "@/hooks/useUserInfo"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { login, refresh } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "./auth"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { Header } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "./request"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> isSkip </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 请求拦截器：实现JWT 双token刷新+静默登录</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> handleLogin</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">header</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Header</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isSkip) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> header;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> loginAndSetData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      isSkip </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 避免递归栈溢出</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">code</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">login</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">accessToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">refreshToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> login</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ data: { code } }));</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // save token</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      header.Authorization </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `Bearer ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">accessToken</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      setToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"accessToken"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, accessToken);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      setToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"refreshToken"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, refreshToken);</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // save userInfo</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">setUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      setUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      void</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">showToast</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"登录失败，请检查网络并重试"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        icon: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"none"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'[APP ERROR] - 登录失败: '</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">finally</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      isSkip </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 0. 获取用户信息, 如果没有用户信息则登录</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">userInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">userInfo) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> loginAndSetData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> header;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> timestamp</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">); </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//获取当前的时间戳</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 1. access部分</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> accessToken</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"accessToken"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">); </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 获取身份验证令牌</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> expInAccessToken</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getExpireInPayload</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(accessToken);</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // accessToken未过期，直接加入请求头请求</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (timestamp </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> expInAccessToken) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    header.Authorization </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `Bearer ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">accessToken</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> header;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 2. refresh部分</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> lastRefreshToken</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"refreshToken"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> expInRefreshToken</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getExpireInPayload</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(lastRefreshToken);</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // refreshToken未过期，刷新Token</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (timestamp </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> expInRefreshToken) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      isSkip </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 避免递归栈溢出</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">accessToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">refreshToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> refresh</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ token: lastRefreshToken }));</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // save</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      header.Authorization </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `Bearer ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">accessToken</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      setToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"accessToken"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, accessToken);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      setToken</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"refreshToken"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, refreshToken);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      void</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">showToast</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"登录失败，请检查网络并重试"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        icon: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"none"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'[APP ERROR] - 登录失败: '</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">finally</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      isSkip </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 3. refreshToken过期，需要重新登录</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> loginAndSetData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> header;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><h3 id="小程序分包减少主包体积" tabindex="-1">小程序分包减少主包体积 <a class="header-anchor" href="#小程序分包减少主包体积" aria-label="Permalink to &quot;小程序分包减少主包体积&quot;">&ZeroWidthSpace;</a></h3>
<p>由于这个小程序依赖于echats以及一个拼音库，体积较大且不是在tab页使用的，所以分包很有必要。</p>
<p>这是我的分包设置：</p>
<div class="language-json vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "subPackages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "root"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"analytics-package"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"analytics"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "pages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/analytics/index"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "root"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"friend-package"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"friend"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "pages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/friend-select/index"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "root"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"questionnaire-package"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"questionnaire"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "pages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/questionnaire-fill/index"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/questionnaire-success/index"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/questionnaire-result/index"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "root"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"user-package"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"user"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "pages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/update-profile/index"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "pages/help-center/index"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "preloadRule"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "pages/questionnaire/index"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "network"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"all"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "packages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"questionnaire"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"analytics"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "pages/mine/index"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "network"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"all"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "packages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"user"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "analytics-package/pages/analytics/index"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "network"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"all"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "packages"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"friend"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span></code></pre>
</div><p>很简单，其实只要告诉AI你的页面依赖情况，然后它就自动给你分好了，最后再叫AI全局搜索一下路径跳转，将相应路径修改为分包之后的路径即可。</p>
<h3 id="微信支付的实现" tabindex="-1">微信支付的实现 <a class="header-anchor" href="#微信支付的实现" aria-label="Permalink to &quot;微信支付的实现&quot;">&ZeroWidthSpace;</a></h3>
<h4 id="基本流程" tabindex="-1">基本流程 <a class="header-anchor" href="#基本流程" aria-label="Permalink to &quot;基本流程&quot;">&ZeroWidthSpace;</a></h4>
<p>参考：<a href="https://pay.weixin.qq.com/static/applyment_guide/applyment_detail_miniapp.shtml" target="_blank" rel="noreferrer">小程序微信支付接入指引</a>，<a href="https://pay.weixin.qq.com/doc/v3/merchant/4012791911" target="_blank" rel="noreferrer">小程序支付API文档</a></p>
<p>不得不说对比起来Stripe的接入是真方便，微信支付首先没有NodeJS、Python等的官方SDK，所有的加密解密安全等与业务无关的代码还需要自己再写一遍，以及微信支付没有测试环境，所以测试时只能通过真实环境1分钱1分钱地测试...</p>
<p>这里引用一张官方的泳道图：</p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250621181459.png" alt=""></p>
<p>不过目前仅通过自己的测试，由于iOS虚拟商品支付的限制，导致无法通过审核，并没有真正上线测试，所以下方代码仅供参考。</p>
<h4 id="后端实现" tabindex="-1">后端实现 <a class="header-anchor" href="#后端实现" aria-label="Permalink to &quot;后端实现&quot;">&ZeroWidthSpace;</a></h4>
<p>这是我的支付service：</p>
<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  Injectable,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  Logger,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  BadRequestException,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  NotFoundException,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@nestjs/common'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { ConfigService } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@nestjs/config'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { PrismaService } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nestjs-prisma'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { CreatePaymentInput } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './dto/create-payment.input'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { UpdatePaymentInput } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './dto/update-payment.input'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { Payment } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './models/payment.models'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  WechatPaymentResponse,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  WechatPayNotifyResult,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './dto/wechat-payment.dto'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  PaymentStatus,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  PaymentType,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  PAYMENT_PRICES,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  WechatPayConfig,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './dto/payment-config'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { v4 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> uuidv4 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'uuid'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { addMonths, addYears } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'date-fns'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { HttpService } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@nestjs/axios'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> *</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> as</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> crypto </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'crypto'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { UsersService } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '../users/users.service'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { RoleEnum } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '../common/enums/role.enum'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { firstValueFrom } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'rxjs'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">@</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Injectable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> PaymentsService</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> readonly</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> logger</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Logger</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(PaymentsService.name);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> wechatConfig</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatPayConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  constructor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> prisma</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> PrismaService</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> configService</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ConfigService</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> httpService</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> HttpService</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> usersService</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> UsersService</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 初始化微信支付配置</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      mchid: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.configService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WECHAT_PAY_MCHID'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      appid: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.configService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WECHAT_PAY_APPID'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      notifyUrl: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.configService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WECHAT_PAY_NOTIFY_URL'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      apiV3Key: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.configService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WECHAT_PAY_API_V3_KEY'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      serialNo: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.configService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WECHAT_PAY_SERIAL_NO'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      privateKey: Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.configService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WECHAT_PAY_PRIVATE_KEY_BASE64'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        'base64'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 校验配置是否完整</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      !</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.mchid </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      !</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.appid </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      !</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.notifyUrl</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">warn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'微信支付配置不完整，部分功能可能无法正常工作'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 创建支付订单</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    userId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    createPaymentInput</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CreatePaymentInput</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  )</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Payment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> createPaymentInput;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 获取支付金额配置</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> priceConfig</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> PAYMENT_PRICES</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">find</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> p.type </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> type);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">priceConfig) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      throw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> BadRequestException</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'无效的支付类型'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 生成商户订单号</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> outTradeNo</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `PAY${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Date</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">()</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Math</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">(</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Math</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">random</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 创建支付记录</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> payment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      data: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        userId,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        type,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        outTradeNo,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        amount: priceConfig.amount,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        description: priceConfig.description,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        status: PaymentStatus.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">PENDING</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> payment </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Payment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 创建JSAPI支付参数</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createWechatPayment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    userId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    createPaymentInput</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CreatePaymentInput</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    openid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  )</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">WechatPaymentResponse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 创建支付记录</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> payment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(userId, createPaymentInput);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 调用微信支付接口</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createWechatJsapiPay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(payment, openid);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 如果微信支付下单失败，更新订单状态</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        where: { id: payment.id },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        data: { status: PaymentStatus.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">FAILED</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`微信支付下单失败: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">error</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">message</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error.stack);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      throw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> BadRequestException</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'支付下单失败，请稍后重试'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 调用微信JSAPI支付接口</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createWechatJsapiPay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    payment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Payment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    openid</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  )</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">WechatPaymentResponse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 构建微信支付请求数据</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> timestamp</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nonceStr</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> uuidv4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">-</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 微信支付V3接口地址</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> url</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 构建请求数据</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> requestData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      appid: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.appid,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      mchid: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.mchid,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      description: payment.description,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      out_trade_no: payment.outTradeNo,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      notify_url: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.notifyUrl,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      amount: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        total: payment.amount,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        currency: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'CNY'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      payer: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        openid: openid,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 计算请求签名</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nonce</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nonceStr;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> method</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'POST'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> body</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(requestData);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 构造签名字符串</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> message</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">method</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> URL</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">(</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">url</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">).</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">pathname</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    }</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">timestamp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">nonce</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">body</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> signature</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sign</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(message);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 构造Authorization头</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> authorization</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `WECHATPAY2-SHA256-RSA2048 mchid="${</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">wechatConfig</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">mchid</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}",nonce_str="${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">nonce</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}",signature="${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">signature</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}",timestamp="${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">timestamp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}",serial_no="${</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">wechatConfig</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">serialNo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}"`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 发送请求到微信支付API</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> response</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> firstValueFrom</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.httpService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(url, requestData, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          headers: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            'Content-Type'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'application/json'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            Accept: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'application/json'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            Authorization: authorization,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      );</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> prepay_id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> response.data.prepay_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 生成小程序调起支付的参数</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> paymentParams</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">buildMiniProgramPayment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(prepay_id);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> paymentParams;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`微信支付API调用失败: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">error</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">message</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error.stack);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      throw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> BadRequestException</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'支付请求失败，请稍后再试'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 生成小程序调起支付的参数</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> buildMiniProgramPayment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">prepayId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatPaymentResponse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> timestamp</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nonceStr</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> uuidv4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">-</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> packageStr</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `prepay_id=${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">prepayId</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 构造签名字符串</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> message</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">wechatConfig</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">appid</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">timestamp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">nonceStr</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">packageStr</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> paySign</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sign</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(message);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      appId: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.appid,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      timeStamp: timestamp,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      nonceStr: nonceStr,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      package: packageStr,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      signType: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'RSA'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      paySign: paySign,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * RSA签名</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sign</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">message</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 从私钥文件中读取私钥</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> privateKey</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.privateKey;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 创建签名对象</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> sign</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> crypto.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createSign</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'RSA-SHA256'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    sign.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(message);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 签名并返回 Base64 编码结果</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sign.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sign</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(privateKey, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'base64'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 处理微信支付回调通知</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> handlePayNotify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    headers</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Record</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    body</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> any</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  )</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">code</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">message</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 解析通知数据 - 检查 body 是否已经是对象</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> notifyData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> any</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, body);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> body </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'string'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        notifyData </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(body);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // body 已经是解析后的对象</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        notifyData </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> body;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 验证签名</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 注：实际项目中，这里需要验证微信支付通知的签名，确保通知合法性</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 由于涉及到证书和复杂的解密步骤，这里简化处理</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 验证通知信息</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> resource</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> notifyData.resource;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ciphertext</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> resource.ciphertext;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nonce</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> resource.nonce;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> associatedData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> resource.associated_data </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 解密数据</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> decryptedData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">decryptAes256Gcm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        ciphertext,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.wechatConfig.apiV3Key,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        nonce,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        associatedData</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      );</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> payResult</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(decryptedData) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatPayNotifyResult</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 查找对应的支付订单</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> payment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">findFirst</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        where: { outTradeNo: payResult.out_trade_no },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">payment) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`支付回调：找不到订单 ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">payResult</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">out_trade_no</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { code: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'FAIL'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, message: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'订单不存在'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 判断支付状态</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (payResult.trade_state </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'SUCCESS'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 更新支付状态</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">updatePaymentAndUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(payment.id, payResult);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          `支付成功：用户 ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">id</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">} 支付订单 ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">payment</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">outTradeNo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}，金额 ${</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            payResult</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">amount</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">total</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          } 元`</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        );</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { code: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'SUCCESS'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, message: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'OK'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 处理其他支付状态</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          where: { id: payment.id },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          data: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            status: PaymentStatus.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">FAILED</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">warn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          `支付未成功：订单 ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">payment</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">outTradeNo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}, 状态 ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">payResult</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">trade_state</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        );</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { code: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'SUCCESS'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, message: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'OK'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.logger.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`处理支付回调出错: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">error</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">message</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error.stack);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { code: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'FAIL'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, message: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'回调处理失败'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 更新支付记录和用户信息</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> updatePaymentAndUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    paymentId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    payResult</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> WechatPayNotifyResult</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 使用事务确保数据一致性</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$transaction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">tx</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 1. 更新支付记录</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> payment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> tx.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        where: { id: paymentId },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        data: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          status: PaymentStatus.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">SUCCESS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          paidAt: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(payResult.success_time),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">	// 略一部分，更新用户表的VIP字段</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> updatedUser;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * AES-256-GCM 解密</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  private</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> decryptAes256Gcm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    ciphertext</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    key</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    nonce</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    associatedData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  )</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 使用 base64 解码密文</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ciphertextBuffer</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(ciphertext, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'base64'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 创建解密器</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> decipher</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> crypto.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createDecipheriv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'aes-256-gcm'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      key,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(nonce, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    );</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 设置关联数据</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    decipher.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setAAD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(associatedData, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 假设密文的最后16字节是认证标签</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> authTagLength</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 16</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> authTag</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ciphertextBuffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">slice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ciphertextBuffer.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> authTagLength</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    );</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> actualCiphertext</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ciphertextBuffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">slice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ciphertextBuffer.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> authTagLength</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    );</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 设置认证标签</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    decipher.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setAuthTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(authTag);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 解密</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> decrypted </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> decipher.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(actualCiphertext);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    decrypted </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">concat</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([decrypted, decipher.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">final</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()]);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> decrypted.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 查询所有支付记录</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  findAll</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">findMany</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 查询单个支付记录</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  findOne</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">findUnique</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      where: { id },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 查询用户的支付记录</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  findByUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">userId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">findMany</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      where: { userId },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      orderBy: { createdAt: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'desc'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 更新支付记录</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">updatePaymentInput</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> UpdatePaymentInput</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      where: { id },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      data: updatePaymentInput,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  /**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   * 删除支付记录</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  remove</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.prisma.payment.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">delete</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      where: { id },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>有两个主要的接口暴露出去：</p>
<ul>
<li>一个是支付成功的回调接口，这个接口调用上述service的<code>handlePayNotify</code>进行处理即可；</li>
<li>还有一个是创建支付时的接口，这个接口调用上述service的<code>createWechatPayment</code>即可</li>
</ul>
<h4 id="前端实现" tabindex="-1">前端实现 <a class="header-anchor" href="#前端实现" aria-label="Permalink to &quot;前端实现&quot;">&ZeroWidthSpace;</a></h4>
<p>这是前端支付创建流程：</p>
<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> handlePay</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">type</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">description</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    isPayLoading.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> paymentData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createWechatPayment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        data: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          type,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          description</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      await</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">void</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">reject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">requestPayment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          appId: paymentData.appId,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          nonceStr: paymentData.nonceStr,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          package: paymentData.package,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          paySign: paymentData.paySign,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          signType: paymentData.signType </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'MD5'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'HMAC-SHA256'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'RSA'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          timeStamp: paymentData.timeStamp,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">          success</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            showSuccess</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'支付成功'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // 支付成功后刷新用户信息</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">              const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> latestUserInfo</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">              setUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(latestUserInfo);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">              console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'刷新用户信息失败:'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            resolve</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">          fail</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">err</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'支付失败:'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, err);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            showError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'支付失败'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            reject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(err.errMsg </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '支付失败'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'创建支付订单失败:'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      showError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'创建订单失败'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">finally</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      isPayLoading.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  };</span></span></code></pre>
</div><p>就两步：</p>
<ul>
<li>调用后端的创建订单接口返回一堆数据</li>
<li>然后通过这堆数据调用微信支付的拉取调用接口即可拉取微信支付</li>
</ul>
<h3 id="提交时小程序切换后台导致请求取消问题修复" tabindex="-1">提交时小程序切换后台导致请求取消问题修复 <a class="header-anchor" href="#提交时小程序切换后台导致请求取消问题修复" aria-label="Permalink to &quot;提交时小程序切换后台导致请求取消问题修复&quot;">&ZeroWidthSpace;</a></h3>
<p>这个是上线之后用户反馈的一个前端体验的问题。</p>
<p>背景是：由于问卷提交时，有较长的上下文发送给AI进行分析以及对应问卷的一个计算公式需要计算，所以在提交时花费时间较长。</p>
<p>这时候可能有一些用户就会切换小程序到后台，从而导致微信请求被取消，从而触发了保存失败的提示（实际后端已经执行成功了）</p>
<p>具体可以看微信小程序的这篇文章：<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/runtime/operating-mechanism.html" target="_blank" rel="noreferrer">微信小程序的运行机制</a></p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250621202739.png" alt=""></p>
<p>即5秒之后如果接口没有请求完成，就会被切换到挂起状态从而在前端取消对应的请求，从而触发报错信息，以及留在提交页面，保存按钮也是可以继续触发的。所以我做了一个简单的判断，以保证用户的体验：</p>
<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 因为进入问卷页时就会检查更新权限，如果这里报错，说明挂起之后重新提交了，提示用户提交成功</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">instanceof</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> error.message </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'You can only update once per day'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // showWarning('24h内你已经提交过一次了');</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">nextTick</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          void</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> wx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">reLaunch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            url: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/pages/questionnaire/index'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">            success</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">              showWarning</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'24h内你已经提交过一次了'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        showError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'保存失败，请稍后重试'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'[APP ERROR] - 保存问卷填写结果失败: '</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span></code></pre>
</div><p>此时，如果用户提交后从后台返回该小程序，如果因为挂起状态导致请求取消，再次提交时，根据后端返回是否提交过的信息，判断返回首页并友好提示。</p>
<h2 id="困" tabindex="-1">困 <a class="header-anchor" href="#困" aria-label="Permalink to &quot;困&quot;">&ZeroWidthSpace;</a></h2>
<p>其实主要就是遇到了各种审核的问题，导致走一步，等一步，然后热情被浇灭了，又得过一阵才会鼓起劲再来做一部分，从而导致了这个小程序历经两年才重构完成。</p>
<p>来，我们来数一数：</p>
<ol>
<li>首先是域名的备案，因为小程序需要https接口，所以不能直接使用ip地址进行请求（当然本身不安全，所以需要域名，理解）</li>
<li>然后自然而然需要经过腾讯云、工信局、本地公安的审核</li>
<li>接下来微信小程序本身的备案（还要花钱，毕竟是外包给其他公司的，我们理解）</li>
<li>备案过程中，打电话询问了小程序的情况，发现我有出售服务，所以被打回了，因为我是个人资质，不能出售商品</li>
<li>好，接下来继续申请个体工商户，同样的，准备各种资料，被打回了两次终于审核成功拿到营业执照</li>
<li>然后又提交一堆表单，打印了小程序主体变更书，然后提交审核将小程序的主体变更为了这个个体户</li>
<li>再然后申请微信支付</li>
<li>因为我需要用大模型来分析结果，所以又做了算法备案申请了小程序的深度合成类目</li>
<li>写代码上线</li>
<li>结果在提交了4-5个版本之后（前几个版本也是携带着微信支付的代码的），下一个版本审核失败，因为有虚拟商品的购买，iOS早在2020年左右就不允许微信小程序内购虚拟商品了</li>
<li>没有办法，走过来一场空，又把微信支付相关代码全部去掉，最终加了点封面广告和提交等待时的弹窗广告以维持Token费用，最终免费给大家使用了</li>
</ol>
<p>心累啊～</p>
<h2 id="终" tabindex="-1">终 <a class="header-anchor" href="#终" aria-label="Permalink to &quot;终&quot;">&ZeroWidthSpace;</a></h2>
<p>好在最终仅供多轮测试，成功上线，虽然反响不大，但总算了却了一个心愿，查了下git，最近1个月光是前端代码，就有近100次commit，感谢Vibe Coding啊～</p>
<p>后端不用多说，Vibe Coding时AI对于Nestjs的代码还是很熟悉的；而前端 Vibe coding 时只需告诉AI，逻辑层使用Vue3增强，视图层使用原生语法，效果就还不错～</p>
<p>整体还是非常推荐使用Vue Mini来进行开发小程序的，一个是拥有TS、Hook、响应式等现代化的写法，另外一个还是保留着原生小程序语法的生态，且没有额外封装一层导致的坑（复杂度）</p>
<p>接下来会继续小步完善该小程序的功能，比如排行榜功能等等...</p>
<p>最后，欢迎使用该小程序：<a href="https://xin2.link" target="_blank" rel="noreferrer">xin2.link</a>，里面也是视频演示及小程序码可以访问。</p>
<p>通过这个小程序，你可以让朋友换位思考在你的角度填写问卷，从而得到更加全面的自我了解（自我评价+他人评价），以及AI会给出智能分析及积极建议，让大家和朋友一起更加了解自己，了解自己究竟是什么样的一个人，我认为这对今后的成长方向是非常重要的。</p>
<p>谢谢大家看到了这么多碎碎念。</p>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
        <item>
            <title><![CDATA[GPT4o生图风格小全]]></title>
            <link>https://justin3go.com/posts/2025/04/11-gpt-4o-image-generation-guide</link>
            <guid>https://justin3go.com/posts/2025/04/11-gpt-4o-image-generation-guide</guid>
            <pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="gpt4o生图风格小全" tabindex="-1">GPT4o生图风格小全 <a class="header-anchor" href="#gpt4o生图风格小全" aria-label="Permalink to &quot;GPT4o生图风格小全&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>原文链接：<a href="https://turbo0.com/blog/gpt-4o-image-generation-guide" target="_blank" rel="noreferrer">GPT-4o Image Generation Guide</a></p>
</blockquote>
<p><a href="https://turbo0.com" target="_blank" rel="noreferrer">Turbo0</a>是一个内容创作者工具目录，你可以在<a href="https://turbo0.com" target="_blank" rel="noreferrer">Turbo0.com</a>里面找到更多有关图片编辑的工具和资源～</p>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<p>笔者整理了一份详尽的GPT-4o图像生成风格指南，涵盖了多种独特的艺术风格和应用场景。主要包括：</p>
<p><strong>经典艺术风格</strong>：吉卜力动画风格、赛博朋克风格、水彩画风格、油画风格、浮世绘风格等</p>
<p><strong>现代创意风格</strong>：3D Q版角色、手绘简笔画、像素艺术、Low Poly低多边形、手绘高亮信息卡片等</p>
<p>这些风格可应用于<em>品牌设计</em>、<em>头像生成</em>、<em>儿童绘本</em>、<em>产品原型</em>等多个实际场景。文中还分享了发现新玩法的四个秘籍：利用搜索引擎、社交媒体、专业平台以及反向分析法。通过这些方法，用户可以不断探索和创新，提升提示词使用技巧，创造出更多独特的视觉效果。</p>
<blockquote>
<p>本文为读者提供了一个系统化的GPT-4o图像生成参考指南，既可作为入门教程，也适合进阶学习使用。</p>
</blockquote>
<!-- DESC SEP -->
<h2 id="风格" tabindex="-1">风格 <a class="header-anchor" href="#风格" aria-label="Permalink to &quot;风格&quot;">&ZeroWidthSpace;</a></h2>
<h3 id="studio-ghibli风格-studio-ghibli-style" tabindex="-1">Studio Ghibli风格 (Studio Ghibli Style) <a class="header-anchor" href="#studio-ghibli风格-studio-ghibli-style" aria-label="Permalink to &quot;Studio Ghibli风格 (Studio Ghibli Style)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p><code>Redraw in the style of Ghibli anime</code>​（将照片重绘成吉卜力动画风格）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411093833.png" alt=""></p>
<h3 id="cyberpunk风格-赛博朋克风格" tabindex="-1">Cyberpunk风格 (赛博朋克风格) <a class="header-anchor" href="#cyberpunk风格-赛博朋克风格" aria-label="Permalink to &quot;Cyberpunk风格 (赛博朋克风格)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p><code>A rainy street in a future city, full of neon signs, cyberpunk style.</code>（一个充满霓虹灯的未来都市雨夜街景，赛博朋克风格）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411094013.png" alt=""></p>
<h3 id="中式婚礼" tabindex="-1"><a href="https://x.com/balconychy/status/1909418699150237917" target="_blank" rel="noreferrer">中式婚礼</a> <a class="header-anchor" href="#中式婚礼" aria-label="Permalink to &quot;[中式婚礼](https://x.com/balconychy/status/1909418699150237917)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>将照片里的两个人转换成Q版 3D人物，中式古装婚礼，大红颜色，背景“囍”字剪纸风格图案。 服饰要求：写实，男士身着长袍马褂，主体为红色，上面以金色绣龙纹图案，彰显尊贵大气 ，胸前系着大红花，寓意喜庆吉祥。女士所穿是秀禾服，同样以红色为基调，饰有精美的金色花纹与凤凰刺绣，展现出典雅华丽之感 ，头上搭配花朵发饰，增添柔美温婉气质。二者皆为中式婚礼中经典着装，蕴含着对新人婚姻美满的祝福。 头饰要求： 男士：中式状元帽，主体红色，饰有金色纹样，帽顶有精致金饰，尽显传统儒雅庄重。 女士：凤冠造型，以红色花朵为中心，搭配金色立体装饰与垂坠流苏，华丽富贵，古典韵味十足。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411111023.png" alt=""></p>
<h3 id="手绘简笔画风格" tabindex="-1"><a href="https://x.com/ZHO_ZHO_ZHO/status/1909907741948399873" target="_blank" rel="noreferrer">手绘简笔画风格</a> <a class="header-anchor" href="#手绘简笔画风格" aria-label="Permalink to &quot;[手绘简笔画风格](https://x.com/ZHO_ZHO_ZHO/status/1909907741948399873)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>先把图片人物变成手绘简笔画风格 然后把简笔画按照吐舌头、微笑、皱眉、惊讶、思考、眨眼生成一系列表情包</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411111104.png" alt=""></p>
<h3 id="手绘插画风格" tabindex="-1"><a href="https://x.com/hellokaton/status/1908338014142816690" target="_blank" rel="noreferrer">手绘插画风格</a> <a class="header-anchor" href="#手绘插画风格" aria-label="Permalink to &quot;[手绘插画风格](https://x.com/hellokaton/status/1908338014142816690)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Create concise, visually structured notes on the topic '{{topic}}'. Notes must fit clearly within a {{orientation}} layout (horizontal/vertical), featuring: - Moderate Font Size: Comfortable readability. - Clear Structure: - Main points highlighted with &quot;background colors&quot; or &quot;wavy underlines~&quot;. - Regular notes in standard ink. - Emphasis notes in a different ink color. - Illustrations: - Include relevant sketches or hand-drawn style illustrations. - Allow fountain pen-style doodles or annotations directly on illustrations. - Annotations: - Simulate notes, corrections, and additional quirky doodles resembling spontaneous annotations, using marker pen style. - Incorporate collage-style photo extracts relevant to the topic, annotated or doodled upon. - Language Text Accuracy Constraint (Strict): - When generating text in '{{language}}', abide by recognized dictionaries and standard grammar rules. - For languages like 中文 (Chinese) or others with complex scripts: - Ensure each character or symbol is correct, standard, and used appropriately. - Double-check stroke order, avoid non-existent variants, and verify usage before finalizing the notes. User Settings: - Topic: 孩子教育 - Orientation: Vertical - Language: 中文 - Color Scheme: highlight style. - Illustration Style: Detailed hand-drawn</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411111717.png" alt=""></p>
<h3 id="_3d-q版风格" tabindex="-1"><a href="https://x.com/hellokaton/status/1908338036842389992" target="_blank" rel="noreferrer">3D Q版风格</a> <a class="header-anchor" href="#_3d-q版风格" aria-label="Permalink to &quot;[3D Q版风格](https://x.com/hellokaton/status/1908338036842389992)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>将场景中的角色转化为3D Q版风格，同时保持原本的场景布置和服装造型不变。比例 2:3</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411111826.png" alt=""></p>
<h3 id="我的世界像素风格" tabindex="-1"><a href="https://x.com/hellokaton/status/1908667355426902483" target="_blank" rel="noreferrer">我的世界像素风格</a> <a class="header-anchor" href="#我的世界像素风格" aria-label="Permalink to &quot;[我的世界像素风格](https://x.com/hellokaton/status/1908667355426902483)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Make this in minecraft voxel style</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411112228.png" alt=""></p>
<h3 id="麦金塔电脑" tabindex="-1"><a href="https://x.com/hellokaton/status/1908705480672981214" target="_blank" rel="noreferrer">麦金塔电脑</a> <a class="header-anchor" href="#麦金塔电脑" aria-label="Permalink to &quot;[麦金塔电脑](https://x.com/hellokaton/status/1908705480672981214)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Create an image in pixel art style of an original Macintosh computer with keyboard. On the screen is the uploaded image opened in MacPaint, converted to black and white. ratio is 2:3</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411112631.png" alt=""></p>
<h3 id="_3d-q版弥勒佛形象" tabindex="-1"><a href="https://x.com/dotey/status/1909804614460785042" target="_blank" rel="noreferrer">3D Q版弥勒佛形象</a> <a class="header-anchor" href="#_3d-q版弥勒佛形象" aria-label="Permalink to &quot;[3D Q版弥勒佛形象](https://x.com/dotey/status/1909804614460785042)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>3D Q版弥勒佛形象</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411111155.png" alt=""></p>
<h3 id="_3d手办风格" tabindex="-1">3D手办风格 <a class="header-anchor" href="#_3d手办风格" aria-label="Permalink to &quot;3D手办风格&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>转换为3D手办风格</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411113652.png" alt=""></p>
<h3 id="工作室风格" tabindex="-1"><a href="https://x.com/FinanceYF5/status/1909821492281721269" target="_blank" rel="noreferrer">工作室风格</a> <a class="header-anchor" href="#工作室风格" aria-label="Permalink to &quot;[工作室风格](https://x.com/FinanceYF5/status/1909821492281721269)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>将右边的人物风格化为左边的图像。人物应该稍微转身，像左侧照片中的人一样。穿着黑色衬衫。被柔和的工作室灯光照亮，背景为深色，创造出焦外效果，突出显示如面部纹理或细纹等细节。他们的目光反射出平静，凝视着远方。肖像摄影。使用 Sony Alpha A7 III 和 f/2 镜头拍摄。照片真实感，8k 分辨率。方形画幅比例（1:1）。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411114705.png" alt=""></p>
<h3 id="app-icon" tabindex="-1"><a href="https://x.com/yokogaisha/status/1908047396275454011" target="_blank" rel="noreferrer">APP Icon</a> <a class="header-anchor" href="#app-icon" aria-label="Permalink to &quot;[APP Icon](https://x.com/yokogaisha/status/1908047396275454011)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Create a 3D-style chibi app icon character with a cute, toy-like appearance. The character should have large expressive eyes, a small smile, and detailed, glossy features like a real physical toy. Use a soft, vibrant lighting style to give it a polished, high-quality finish. The character should slightly exceed the app icon’s frame to enhance the 3D effect and playfulness. Style should feel collectible and adorable, like a miniature figure or Nendoroid.</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411114842.png" alt=""></p>
<h3 id="键盘微缩场景" tabindex="-1"><a href="https://x.com/op7418/status/1909489475857866810" target="_blank" rel="noreferrer">键盘微缩场景</a> <a class="header-anchor" href="#键盘微缩场景" aria-label="Permalink to &quot;[键盘微缩场景](https://x.com/op7418/status/1909489475857866810)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>一幅等距3D插画描绘了被掀开的键盘键帽下，一个微型玩家正坐在迷你驾驶舱里打游戏，并将键帽内侧当作发光的屏幕。带有轻微的 3D 质感和些许塑化感，使整个场景如同一个为游戏玩家量身定做的、极其精巧的立体模型。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411135448.png" alt=""></p>
<h3 id="微缩世界" tabindex="-1"><a href="https://x.com/op7418/status/1909447949211558281" target="_blank" rel="noreferrer">微缩世界</a> <a class="header-anchor" href="#微缩世界" aria-label="Permalink to &quot;[微缩世界](https://x.com/op7418/status/1909447949211558281)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>一幅极简风格的等距 3D 插画，描绘了一个隐藏在老旧的米色电脑主机箱（典型的 90 年代末/2000 年代初办公室风格，ATX 结构）内部的微型、极致宁静的解压绿洲——一个日式枯山水庭院。</p>
<p>外部特征： 主机箱外观是经典的米白色或淡黄色塑料，可能带有软盘驱动器插槽、光驱托盘、遍布灰尘的散热格栅、圆形的电源按钮，甚至侧面贴着一张褪色的“Property of...”资产标签。整体呈现出一种被遗忘的、充满年代感的办公室设备外观。</p>
<p>内部场景： 打开的主机箱侧板（或移除侧板后的内部空间）被改造成一个微缩的枯山水庭院。大部分空间被精心铺设的细白沙所覆盖，沙面上用迷你耙子仔细地耙出了代表水波的纹路（波纹，hamon）。几块形态各异、经过挑选的微型深色岩石（象征山岛）错落有致地摆放其间。一小片翠绿的仿真苔藓覆盖在某个角落的“岩石”边。或许还有一个用细铜线和电子元件巧妙改造的迷你“添水”（鹿威し）装置，静静地立着。</p>
<p>人物： 一个穿着皱巴巴衬衫、可能还打着松垮领带的微型“社畜”小人，正平静地跪坐在沙园的一角，手持一把极其小巧的木质耙子，眼神专注地望着眼前的沙纹，仿佛在进行一场心灵的冥想。</p>
<p>细节与氛围： 光线主要从主机箱顶部的散热口或侧面的缝隙中以光束的形式照射进来，穿过内部可能存在的、象征性的微尘，在沙面和岩石上投下清晰而柔和的阴影。光线可能是模拟办公室的冷色日光灯，但在庭院区域被内部氛围中和得更为柔和、宁静。整个场景色调以沙子的白色、岩石的深灰、苔藓的绿色以及背景中主机内部零件的金属/塑料原色为主，形成一种素雅、克制的对比。营造出一种极致的平和、专注与逃离现实的禅意氛围。</p>
<p>整体感受： 带有轻微的 3D 质感和些许塑化感，如同一个藏在冰冷科技外壳下的温暖秘密、一个精巧的桌面立体模型。这个场景在象征着工作压力与束缚的电脑主机内部，构建了一个代表内心平静与精神自由的禅意空间，形成了强烈的视觉与情感对比。非常适合需要片刻宁静的现代都市人、办公室职员、微缩模型与日式美学爱好者，以及欣赏探讨工作、生活与内心世界关系的数字艺术作品的人们。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411135707.png" alt=""></p>
<h3 id="low-poly风格-低多边形风格" tabindex="-1">Low Poly风格 (低多边形风格) <a class="header-anchor" href="#low-poly风格-低多边形风格" aria-label="Permalink to &quot;Low Poly风格 (低多边形风格)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p><code>A minimalist 3D island floating in space, low-poly design with pastel colors.</code>（一座漂浮在太空中的极简3D岛屿，低多边形设计，柔和马卡龙配色）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411093802.png" alt=""></p>
<h3 id="景区浏览笔记" tabindex="-1"><a href="https://x.com/op7418/status/1907725315025330245" target="_blank" rel="noreferrer">景区浏览笔记</a> <a class="header-anchor" href="#景区浏览笔记" aria-label="Permalink to &quot;[景区浏览笔记](https://x.com/op7418/status/1907725315025330245)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>请生成一张图片，模拟在一张略带纹理的纸张（比如米黄色或浅棕色）上手写的关于景区 请在此处替换为景区名称 的讲解笔记。图片应呈现旅行日志/拼贴画风格，包含以下元素：</p>
<p>用手写字体（比如蓝色或棕色墨水）书写景区名称、地理位置、最佳游览季节、以及一两句吸引人的标语或简介。
包含几个主要看点或特色的介绍，使用编号列表或项目符号（例如：列举2-3个具体看点，如“奇特的岩石形态”，“古老的传说”，“独特的植物”等），并配有简短的手写说明。
用红色笔迹或其他亮色圈出或用箭头指向特别推荐的地点或活动（例如 列举1-2个推荐项）。 穿插一些与景区特色相关的简单涂鸦式小图画（例如：根据景区特色想1-2个代表性图画，如山峰轮廓、特色动植物、标志性建筑等]=）。</p>
<p>点缀几张关于该景区的、看起来像是贴上去的小幅照片（可以是风景照、细节照，风格可以略显复古或像宝丽来照片）。 整体感觉要像一份由热情导游或资深游客精心制作的、生动有趣的个人导览手记。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411135905.png" alt=""></p>
<h3 id="手绘高亮信息卡片" tabindex="-1"><a href="https://x.com/dotey/status/1907903480678985784" target="_blank" rel="noreferrer">手绘高亮信息卡片</a> <a class="header-anchor" href="#手绘高亮信息卡片" aria-label="Permalink to &quot;[手绘高亮信息卡片](https://x.com/dotey/status/1907903480678985784)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>创作一张手绘风格的信息图卡片，比例为9:16竖版。卡片主题鲜明，背景为带有纸质肌理的米色或米白色，整体设计体现质朴、亲切的手绘美感。 卡片上方以红黑相间、对比鲜明的大号毛笔草书字体突出标题，吸引视觉焦点。文字内容均采用中文草书，整体布局分为2至4个清晰的小节，每节以简短、精炼的中文短语表达核心要点。字体保持草书流畅的韵律感，既清晰可读又富有艺术气息。 卡片中点缀简单、有趣的手绘插画或图标，例如人物或象征符号，以增强视觉吸引力，引发读者思考与共鸣。 整体布局注意视觉平衡，预留足够的空白空间，确保画面简洁明了，易于阅读和理解。</p>
<p><code>&lt;h1&gt;&lt;span style=&quot;color:red&quot;&gt;「认知」&lt;/span&gt;决定上限 &lt;span style=&quot;color:red&quot;&gt;「圈子」&lt;/span&gt;决定机会&lt;/h1&gt; - 你赚不到「认知」以外的钱， - 也遇不到「圈子」以外的机会。</code></p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411115106.png" alt=""></p>
<h3 id="vaporwave风格-蒸汽波风格" tabindex="-1">Vaporwave风格 (蒸汽波风格) <a class="header-anchor" href="#vaporwave风格-蒸汽波风格" aria-label="Permalink to &quot;Vaporwave风格 (蒸汽波风格)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Redo this image in the Vaporwave style, neon pinks and purples, chrome textures, digital sunset gradients, 1980s retrofuturistic aesthetic...（将此图像重新制作成蒸汽波风格，带有霓虹粉紫色、铬合金质感、数字夕阳渐变和80年代未来复古美学…）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411094439.png" alt=""></p>
<h3 id="像素艺术风格-pixel-art-style" tabindex="-1">像素艺术风格 (Pixel Art Style) <a class="header-anchor" href="#像素艺术风格-pixel-art-style" aria-label="Permalink to &quot;像素艺术风格 (Pixel Art Style)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>请生成一张8-bit像素风格的游戏场景</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411094843.png" alt=""></p>
<h3 id="水彩画风格-watercolor-style" tabindex="-1">水彩画风格 (Watercolor Style) <a class="header-anchor" href="#水彩画风格-watercolor-style" aria-label="Permalink to &quot;水彩画风格 (Watercolor Style)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Turn it into Hand-Painted Watercolor.</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411095026.png" alt=""></p>
<h3 id="innsmouth-shadow-notes" tabindex="-1">Innsmouth Shadow Notes <a class="header-anchor" href="#innsmouth-shadow-notes" aria-label="Permalink to &quot;Innsmouth Shadow Notes&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>Generate an image with an aspect ratio of 2:3 and use English for the text within the image. This image is to simulate a personal notes page created by a reader for the novel The Shadow over Innsmouth. Style: Collage/scrapbook aesthetic, hand-drawn elements mixed with pasted items on a textured paper background (e.g. like a Moleskine notebook or vellum). Please include the following elements: HANDWRITTEN QUOTES: A few classic or impactful English sentences from a novel, written in a clear but handwritten font with a personal touch. CHARACTER DRAWINGS: Simple doodle-style headshots of 2-3 main characters, not necessarily very realistic, but more like quick impressionistic captures. CHARACTER RELATIONSHIPS DIAGRAM: Arrows drawn between the character doodles with short handwritten English text labels describing their relationships (e.g. &quot;Siblings&quot;, &quot;Lovers&quot;, &quot;Mentor &amp; Student&quot;, &quot;Rivals&quot;). PASTED CORNER OF PAGE: A small, realistic-looking piece that simulates a corner torn or cut from an actual novel page (on which some printed English text can be seen) that looks like it was taped or glued to the notes page. (Optional) Annotations: Possibly small handwritten notes or question marks next to quotes or characters. Layout: Elements should be arranged organically, perhaps overlapping slightly, to create the feel of a frequently used personal journal page. OVERALL FEEL: Thought provoking, analytical, personal, visually appealing.</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411140721.png" alt=""></p>
<h3 id="油画风格-oil-painting-style" tabindex="-1">油画风格 (Oil Painting Style) <a class="header-anchor" href="#油画风格-oil-painting-style" aria-label="Permalink to &quot;油画风格 (Oil Painting Style)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p><code>A peaceful European village at sunset, in oil painting style.</code>（日落时分宁静的欧洲村庄，油画风格）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411095503.png" alt=""></p>
<h3 id="美式漫画风格-comic-book-style" tabindex="-1">美式漫画风格 (Comic Book Style) <a class="header-anchor" href="#美式漫画风格-comic-book-style" aria-label="Permalink to &quot;美式漫画风格 (Comic Book Style)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p><code>A masked hero jumping from a rooftop, comic book style with bold outlines and dialogue bubbles.</code>（一位戴面具的英雄从屋顶跃下，漫画风格，粗线条描边并带对白气泡）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411095735.png" alt=""></p>
<h3 id="日系动漫风格-anime-style" tabindex="-1">日系动漫风格 (Anime Style) <a class="header-anchor" href="#日系动漫风格-anime-style" aria-label="Permalink to &quot;日系动漫风格 (Anime Style)&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p><code>Turn it into Anime style.</code>（转换为日系动画风格）</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411100348.png" alt=""></p>
<h3 id="浮世绘风格" tabindex="-1">浮世绘风格 <a class="header-anchor" href="#浮世绘风格" aria-label="Permalink to &quot;浮世绘风格&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>将此侦探电影场景转为传统日本浮世绘版画风格，保留戏剧性阴影和人物表情，适应传统颜料调色板。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411110057.png" alt=""></p>
<h3 id="超现实主义-抽象构图" tabindex="-1">超现实主义/抽象构图 <a class="header-anchor" href="#超现实主义-抽象构图" aria-label="Permalink to &quot;超现实主义/抽象构图&quot;">&ZeroWidthSpace;</a></h3>
<blockquote>
<p>创建一个圆形图像，包含12个对象的螺旋排列，背景为深空，包括水晶沙漏、机械蝴蝶等。</p>
</blockquote>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250411110847.png" alt=""></p>
<h2 id="一些场景" tabindex="-1">一些场景 <a class="header-anchor" href="#一些场景" aria-label="Permalink to &quot;一些场景&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li>品牌设计与营销视觉：包括Logo、商品包装、宣传海报等</li>
<li>头像生成与个人形象</li>
<li>儿童绘本插图</li>
<li>产品原型与概念设计</li>
<li>插画创作与艺术设计</li>
<li>室内设计与装修布局</li>
<li>电影分镜与情节板</li>
<li>社交媒体封面图</li>
<li>游戏角色设定与草图</li>
<li>视觉风格转换与图像混合</li>
<li>教学演示与图示说明</li>
</ul>
<h2 id="如何发现-gpt-4o-画图新玩法的" tabindex="-1">如何发现 GPT-4o 画图新玩法的 <a class="header-anchor" href="#如何发现-gpt-4o-画图新玩法的" aria-label="Permalink to &quot;如何发现 GPT-4o 画图新玩法的&quot;">&ZeroWidthSpace;</a></h2>
<p>推荐看看宝玉老师的<a href="https://baoyu.io/blog/gpt-4-image-generation-new-tricks" target="_blank" rel="noreferrer">这篇博客</a></p>
<p>四个秘籍：使用搜索引擎、社交媒体、Sora.com 平台以及反向分析法。</p>
<p>这些方法帮助用户掌握提示词使用的技巧，提高敏感度和理解力，通过模仿与融合不同风格的提示词，创造出新颖的视觉效果。网页还强调了分享自己的成功案例的重要性，通过分享和交流，形成正向循环，共同探索更多可能性。</p>
<h2 id="后记-更多资源推荐" tabindex="-1">后记（更多资源推荐） <a class="header-anchor" href="#后记-更多资源推荐" aria-label="Permalink to &quot;后记（更多资源推荐）&quot;">&ZeroWidthSpace;</a></h2>
<blockquote>
<p>欢迎在评论区自荐或推荐GPT-4o绘图资源</p>
</blockquote>
<ul>
<li>2025-04-14：开始有朋友收集此类提示词并整理为github仓库了，非常推荐看看：<a href="https://github.com/jamez-bondos/awesome-gpt4o-images" target="_blank" rel="noreferrer">Awesome GPT-4o Images</a></li>
<li>2025-04-16：<a href="https://x.com/hylarucoder/status/1904866006700613760" target="_blank" rel="noreferrer">长贴演示GPT-4o绘图风格及场景</a></li>
<li>2025-04-16：有朋友整理了<a href="https://dev-qiuyu.feishu.cn/base/RKJsbYoT8aXLsOs2aaBciTmOnQh?table=tblEv0PjREXSDuSu&amp;view=vewEBIpUHH" target="_blank" rel="noreferrer">飞书多维表格</a>，以及开源成了<a href="https://github.com/iAmCorey/prompt.surf" target="_blank" rel="noreferrer">导航站</a></li>
</ul>
<h2 id="参考资料" tabindex="-1">参考资料 <a class="header-anchor" href="#参考资料" aria-label="Permalink to &quot;参考资料&quot;">&ZeroWidthSpace;</a></h2>
<p>部分引用直接在文章内部引用，其他部分参考如下</p>
<ul>
<li><a href="https://chatgpt.com/share/67f88d16-194c-8005-91ab-bdb54d1fbbc7" target="_blank" rel="noreferrer">ChatGPT DeepRearch</a></li>
<li><a href="https://x.com/i/grok/share/Zg14KbCpvsA33E8fMr8kgC4hK" target="_blank" rel="noreferrer">Grok DeeperSearch</a></li>
</ul>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
    </channel>
</rss>