<?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, 08 Feb 2026 13:53:09 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[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>
        <item>
            <title><![CDATA[谷歌 Agent2Agent（A2A）协议深度调研报告]]></title>
            <link>https://justin3go.com/posts/2025/04/10-in-depth-research-report-google-agent2agent-a2a-protocol</link>
            <guid>https://justin3go.com/posts/2025/04/10-in-depth-research-report-google-agent2agent-a2a-protocol</guid>
            <pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="谷歌-agent2agent-a2a-协议深度调研报告" tabindex="-1">谷歌 Agent2Agent（A2A）协议深度调研报告 <a class="header-anchor" href="#谷歌-agent2agent-a2a-协议深度调研报告" aria-label="Permalink to &quot;谷歌 Agent2Agent（A2A）协议深度调研报告&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<p>2025年4月，Google在 Cloud Next 2025 大会上正式发布了全新的 <strong>Agent2Agent (A2A) 协议</strong>。A2A是一个开放的互操作性协议，旨在打破不同AI代理框架和供应商之间的壁垒，实现跨平台的安全高效协作。该协议专为企业环境设计，通过标准化代理之间的通信，解决各类数据孤岛问题，使复杂工作流程的自动化成为可能并提升生产力。</p>
<p>A2A发布时即获得超过50家行业领先企业的支持（如Atlassian、Box、Salesforce、SAP、ServiceNow、MongoDB等），为AI智能体提供了一个通用框架，让不同供应商或架构的代理能够安全交换信息、协调操作，并无缝集成到企业业务中。</p>
<p>这一合作阵容体现出业界对AI代理互操作性的共同愿景：未来无论底层技术如何，AI智能体都能像网络服务一样自由“对话”和协同工作。</p>
<!-- DESC SEP -->
<h2 id="技术原理" tabindex="-1">技术原理 <a class="header-anchor" href="#技术原理" aria-label="Permalink to &quot;技术原理&quot;">&ZeroWidthSpace;</a></h2>
<h3 id="核心架构" tabindex="-1">核心架构 <a class="header-anchor" href="#核心架构" aria-label="Permalink to &quot;核心架构&quot;">&ZeroWidthSpace;</a></h3>
<p>基于HTTP+JSON的AI代理通信协议，主要包含以下模块：</p>
<ol>
<li>
<p><strong>Agent Card</strong></p>
<ul>
<li>位置：<code>/.well-known/agent.json</code></li>
<li>作用：JSON格式的代理能力说明书（含API端点、技能、认证方式）</li>
</ul>
</li>
<li>
<p><strong>角色模型</strong></p>
<ul>
<li>服务端：提供API接口（必须实现<code>tasks/send</code>等标准方法）</li>
<li>客户端：通过HTTP调用其他代理</li>
</ul>
</li>
<li>
<p><strong>任务流程</strong></p>
<ul>
<li>生命周期：submitted → working → (input-required) → completed/failed</li>
<li>交互单元：
<ul>
<li><strong>消息</strong>：由多类型Part组成（文本/文件/结构化数据）</li>
<li><strong>工件</strong>：任务产出的结构化结果</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>通信机制</strong></p>
<ul>
<li>基础模式：HTTP + JSON-RPC风格接口</li>
<li>高级功能：
<ul>
<li>实时推送：SSE协议（<code>tasks/sendSubscribe</code>）</li>
<li>异步通知：Webhook回调</li>
<li>多模态支持：通过Part类型实现</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>UX协商</strong></p>
<ul>
<li>代理间预先协商交互形式（如文本/语音/表单）</li>
</ul>
</li>
</ol>
<h3 id="基础框架与依赖" tabindex="-1">基础框架与依赖 <a class="header-anchor" href="#基础框架与依赖" aria-label="Permalink to &quot;基础框架与依赖&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A协议本身不依赖特定的AI模型或框架，但其出现背景与大型语言模型（LLM）驱动的自治代理趋势密切相关。</p>
<p>协议的结构借鉴了分布式系统和微服务的思想，将每个AI代理视作一个独立服务，通过标准接口通信。这意味着任何基于LLM的代理开发框架（如LangChain、LangGraph、Google的Agent Developer Kit <strong>ADK</strong>、CrewAI、Genkit等）都可以实现A2A接口，从而与其它代理协同。</p>
<p>Google强调A2A能够适配“<strong>不透明的（opaque）</strong>”智能体——即无需暴露内部推理过程或内存状态的代理。这对许多企业级应用至关重要，因为涉及安全和合规时，各代理更倾向通过明确定义的输入输出进行交互，而不共享内部细节。</p>
<p>总的来说，A2A提供的是一套<strong>应用层协议</strong>规范，开发者可以用任何技术栈来构建符合该规范的代理（例如后端用Python、Node.js等皆可），只要遵循HTTP+JSON的接口契约，即可与其他代理互联互通。</p>
<h2 id="协议意义-设计目标及多智能体协作价值" tabindex="-1">协议意义：设计目标及多智能体协作价值 <a class="header-anchor" href="#协议意义-设计目标及多智能体协作价值" aria-label="Permalink to &quot;协议意义：设计目标及多智能体协作价值&quot;">&ZeroWidthSpace;</a></h2>
<h3 id="设计初衷" tabindex="-1">设计初衷 <a class="header-anchor" href="#设计初衷" aria-label="Permalink to &quot;设计初衷&quot;">&ZeroWidthSpace;</a></h3>
<p>Agent2Agent协议的推出，源自于企业在大规模部署AI代理时遇到的实际痛点。随着企业中不同部门和团队引入各式各样的AI驱动代理（有的用于客服，有的用于办公自动化等），如何让不同厂商、不同框架构建的代理彼此通信、协同完成更复杂的任务，成为一大挑战。</p>
<p>传统上，企业可能需要为每对交互的代理定制集成接口或编写“胶水代码”，这不但成本高昂且难以扩展。A2A协议的设计目标正是为<strong>异构AI代理</strong>建立一个共同语言，让它们可以<strong>开箱即用地互相协作</strong>，而无需针对每种组合单独开发适配层。Google在内部大规模代理系统部署方面积累了经验，并抽象出这些通用挑战，从而牵头制定了A2A这一开放标准，以供整个行业采用。</p>
<h3 id="开放与中立" tabindex="-1">开放与中立 <a class="header-anchor" href="#开放与中立" aria-label="Permalink to &quot;开放与中立&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A定位为一个<strong>开放协议标准</strong>而非Google私有接口，这一点具有重要意义。首先，开放意味着任何组织或个人都可根据公开规范实现自己的A2A代理，Google本身也将协议以Apache 2.0许可证在GitHub上开源。超过50家科技公司、SaaS提供商和咨询机构在协议制定初期即参与其中，共同贡献需求和标准制定。</p>
<p>这种多方合作确保了A2A具有<strong>中立性和通用性</strong>，避免一家厂商对标准的垄断。这与互联网早期的HTTP协议类似——作为开放标准被广泛认可，促进了不同系统之间的互联互通。同样，A2A协议也被寄望成为“<strong>代理之间的HTTP</strong>”，为AI代理打造出一个开放统一的交流层。在推出之时，就有观点将其视为行业走向<strong>AI代理互操作标准化</strong>的重要信号。</p>
<h3 id="mcp的补充" tabindex="-1">MCP的补充 <a class="header-anchor" href="#mcp的补充" aria-label="Permalink to &quot;MCP的补充&quot;">&ZeroWidthSpace;</a></h3>
<p>值得注意的是，Google明确将A2A定位为Anthropic公司提出的<strong>模型上下文协议（Model Context Protocol, MCP）<strong>的补充而非替代。MCP协议主要解决的是</strong>单个代理如何使用工具和获取外部上下文</strong>的问题，即为AI模型调用API、检索数据库等提供一致的接口。而A2A关注的是<strong>代理与代理之间的对话与协作</strong>。</p>
<p>Google形象地比喻道：“如果说MCP是<strong>扳手</strong>，为代理提供使用工具的手段；那么A2A就是<strong>技工之间的对话</strong>，让多个代理像技工团队一样交流诊断问题。” 也就是说，A2A并不是与MCP竞争，而是各司其职——MCP赋能单个代理连接外部工具和数据，A2A则让多个独立智能体彼此连接成更大的协作网络。两者相辅相成，共同构筑一个<strong>更强大的自治代理生态</strong>。</p>
<h3 id="提升自治与协同效能" tabindex="-1">提升自治与协同效能 <a class="header-anchor" href="#提升自治与协同效能" aria-label="Permalink to &quot;提升自治与协同效能&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A协议对多智能体系统的价值在于<strong>提高了代理自治协作的上限</strong>。通过A2A，不同能力的智能体可以动态组成“<strong>数字劳动力团队</strong>”，协同处理单个代理无法完成的复杂任务。例如，一个销售AI助手可以直接请求另一个财务AI代理生成报价方案；客服机器人可以与仓储AI代理沟通库存信息。</p>
<p>这种直接代理对接节省了人工协调或额外中间层，从而<strong>提升任务完成的效率和成功率</strong>。有研究表明，在企业场景下多智能体协作可使目标完成率较单代理方案提升显著幅度。同时，A2A的标准化方式也降低了引入新代理的边际成本——企业可以随时增添新的专能代理加入流程，它马上就能与现有代理“对话”，无需额外开发，实现<strong>即插即用</strong>的扩展性。</p>
<h3 id="安全与管控" tabindex="-1">安全与管控 <a class="header-anchor" href="#安全与管控" aria-label="Permalink to &quot;安全与管控&quot;">&ZeroWidthSpace;</a></h3>
<p>在强调开放协作的同时，A2A也注重企业所关心的安全和治理问题。协议设计内置了认证和授权机制，Agent Card中明确列出代理服务所需的身份认证方式。企业可通过统一的策略管理，控制哪些代理有权限互相通信，交换何种数据。</p>
<p>这种<strong>标准化管理</strong>方式比起过去各接口各自为政，更有利于审计和风险控制。此外，因为A2A只要求共享任务所需的输入输出，而不涉及代理内部推理过程，企业可以在<strong>保护机密算法和数据</strong>的前提下，让代理参与协作。从长期来看，随着更多企业采用A2A，他们也能够获得<strong>跨平台管理代理</strong>的统一视图，对整个AI代理“编队”进行监控和优化。这种标准化、可管可控的特性，是A2A能够在企业环境落地的重要原因之一。</p>
<h2 id="应用场景-自动化、多代理协作、智能助手与企业通信" tabindex="-1">应用场景：自动化、多代理协作、智能助手与企业通信 <a class="header-anchor" href="#应用场景-自动化、多代理协作、智能助手与企业通信" aria-label="Permalink to &quot;应用场景：自动化、多代理协作、智能助手与企业通信&quot;">&ZeroWidthSpace;</a></h2>
<p>A2A协议作为多智能体协作的桥梁，在众多业务与产品场景中具有广泛的应用潜力。以下列举几个典型场景及案例，以展示A2A的价值：</p>
<ul>
<li>
<p><strong>企业流程自动化</strong><br>
在大型企业中，日常业务流程往往涉及多个系统和角色。通过A2A，不同职能的AI代理可以协同完成端到端的自动化流程。例如，在IT运维场景中，一个负责资产管理的代理可以向另一个负责采购的代理发送请求，自动完成“为新员工订购笔记本”这样的任务。</p>
</li>
<li>
<p><strong>多智能体协作</strong><br>
A2A最直接的用途就是让多个AI智能体组成协作团队，分工完成复杂任务。在人力资源招聘流程中，企业可能部署了不同智能体：一个负责筛选简历，另一个负责安排面试时间，还有一个负责回答候选人常见问题。通过A2A，这些代理可以互相通知进度并共享信息。</p>
</li>
<li>
<p><strong>智能助理与个人助手</strong><br>
现代的智能虚拟助理（无论面向个人用户还是职场办公）都在变得越来越复杂。通过A2A协议，智能助理可以不再是单一的AI模型，而是前端形象背后调用一组专能代理的集成体。例如，一个个人智能助手在帮用户规划海外旅行时，可以调用“航班预订代理”获取机票方案，再调用“行程优化代理”制定旅行日程，甚至与“翻译代理”协作获取当地语言帮助。</p>
</li>
<li>
<p><strong>企业通信与协同办公</strong><br>
在大型组织里，不同部门可能各自采用了AI工具来提升工作效率。如果没有统一标准，这些代理各自为政，信息难以共享。借助A2A协议，企业能够实现跨部门AI代理的协同，如销售AI发现某客户有技术问题，可以直接通过A2A通知客服AI跟进。</p>
</li>
<li>
<p><strong>其他场景</strong><br>
除上述典型应用外，A2A还可用于客户服务、电子商务物流、金融风控等诸多领域。随着AI代理在各行业的深入应用，几乎所有需要多个智能体协同工作的场景都可能成为A2A的用武之地。</p>
</li>
</ul>
<h2 id="行业影响-生态系统、竞品比较与产业趋势" tabindex="-1">行业影响：生态系统、竞品比较与产业趋势 <a class="header-anchor" href="#行业影响-生态系统、竞品比较与产业趋势" aria-label="Permalink to &quot;行业影响：生态系统、竞品比较与产业趋势&quot;">&ZeroWidthSpace;</a></h2>
<h3 id="生态系统构建" tabindex="-1">生态系统构建 <a class="header-anchor" href="#生态系统构建" aria-label="Permalink to &quot;生态系统构建&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A协议的推出对AI开发生态产生了立竿见影的推动作用。首先，众多合作伙伴的加入（如上图所示）表明业内主要玩家正携手打造统一的代理通信标准。</p>
<p>对于开发者而言，这意味着日后构建AI应用可以更方便地集成来自不同供应商的智能组件，而不用担心兼容性问题。</p>
<h3 id="对竞争格局的影响" tabindex="-1">对竞争格局的影响 <a class="header-anchor" href="#对竞争格局的影响" aria-label="Permalink to &quot;对竞争格局的影响&quot;">&ZeroWidthSpace;</a></h3>
<p>作为首个公开的多智能体通信协议标准, A2A也对业界主要AI平台厂商产生压力和影响。一方面，Google通过开放标准凝聚业界资源，巩固了其在企业AI领域的领导地位；另一方面，其他巨头如Microsoft、OpenAI、IBM等如果没有类似标准，需要考虑是否支持A2A以融入这一生态。</p>
<p>未来客户很可能要求AI产品具备A2A兼容性以保护其投资。这将促使更多厂商加入支持行列，避免被生态排除。</p>
<h3 id="对开发者与企业的意义" tabindex="-1">对开发者与企业的意义 <a class="header-anchor" href="#对开发者与企业的意义" aria-label="Permalink to &quot;对开发者与企业的意义&quot;">&ZeroWidthSpace;</a></h3>
<p>从开发者角度看，A2A的出现极大简化了多代理应用的开发。以前，实现两个不同框架AI系统的交互需要开发者精通双方接口并编写适配层；现在只需双方均实现A2A接口，即可直接调用。</p>
<p>这降低了企业部署AI代理的运营成本和技术风险，让更多传统IT部门也愿意尝试引入AI代理协作来改善业务。</p>
<h3 id="产业趋势展望" tabindex="-1">产业趋势展望 <a class="header-anchor" href="#产业趋势展望" aria-label="Permalink to &quot;产业趋势展望&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A协议的诞生契合了AI产业向更自主、更协同发展的趋势。一方面，大型语言模型能力的提升催生了大量自治Agent的应用需求，单体智能逐渐无法满足复杂业务，这推动架构从“单智能体+工具”走向“多智能体协作”。</p>
<p>A2A提供了这一演进所需的标准底座。另一方面，企业对于数据主权和系统掌控的要求越来越高，他们希望能自由选购最合适的AI模块，而非被锁定在单一厂商生态中。A2A通过开放互通满足了这种去中心化、多样化需求，符合产业开放合作的大方向。</p>
<h2 id="相关资料-论文、博客、演示与代码库" tabindex="-1">相关资料：论文、博客、演示与代码库 <a class="header-anchor" href="#相关资料-论文、博客、演示与代码库" aria-label="Permalink to &quot;相关资料：论文、博客、演示与代码库&quot;">&ZeroWidthSpace;</a></h2>
<h3 id="官方发布与技术博客" tabindex="-1">官方发布与技术博客 <a class="header-anchor" href="#官方发布与技术博客" aria-label="Permalink to &quot;官方发布与技术博客&quot;">&ZeroWidthSpace;</a></h3>
<p>Google 团队在发布A2A协议的同时提供了详尽的官方说明和博客文章。其中最主要的是2025年4月9日发表于 <em>Google Developers Blog</em> 的《Announcing the Agent2Agent Protocol (A2A)》一文。</p>
<h3 id="技术文档与规范" tabindex="-1">技术文档与规范 <a class="header-anchor" href="#技术文档与规范" aria-label="Permalink to &quot;技术文档与规范&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A协议的完整技术规范已经开源发布在GitHub仓库上。该仓库包含协议的JSON接口定义、示例代码和文档。</p>
<h3 id="开源代码库与sdk" tabindex="-1">开源代码库与SDK <a class="header-anchor" href="#开源代码库与sdk" aria-label="Permalink to &quot;开源代码库与SDK&quot;">&ZeroWidthSpace;</a></h3>
<p>在GitHub的A2A仓库中，除了规范文档，还提供了多种语言的示例实现和开发工具包。开发者可以自由试用、参考并贡献改进。</p>
<h3 id="演示与视频资料" tabindex="-1">演示与视频资料 <a class="header-anchor" href="#演示与视频资料" aria-label="Permalink to &quot;演示与视频资料&quot;">&ZeroWidthSpace;</a></h3>
<p>为了方便理解A2A的实际效果，Google官方提供了一些演示案例。例如，在发布会上展示了跨平台代理协作的情景。</p>
<h3 id="社区讨论与分析文章" tabindex="-1">社区讨论与分析文章 <a class="header-anchor" href="#社区讨论与分析文章" aria-label="Permalink to &quot;社区讨论与分析文章&quot;">&ZeroWidthSpace;</a></h3>
<p>A2A发布后引起了AI技术社区的广泛讨论。业界分析人士在博客和媒体上发表了许多见解，这些资料有助于从不同角度理解A2A协议的意义、应用前景和可能的挑战。</p>
<h3 id="是否有学术论文" tabindex="-1">是否有学术论文 <a class="header-anchor" href="#是否有学术论文" aria-label="Permalink to &quot;是否有学术论文&quot;">&ZeroWidthSpace;</a></h3>
<p>截至目前（2025年4月），A2A协议主要以工业界发布为主，并无专门的学术论文公布其细节。不过，其理念契合多智能体系统和分布式AI的研究方向。</p>
<h2 id="总结" tabindex="-1">总结 <a class="header-anchor" href="#总结" aria-label="Permalink to &quot;总结&quot;">&ZeroWidthSpace;</a></h2>
<p>Agent2Agent (A2A) 协议作为Google携手业界推出的开放标准，开创了AI代理直接对话协作的新时代。其清晰的技术架构和面向企业的设计填补了多智能体系统缺乏通用通信规范的空白。</p>
<p>A2A的出现将AI代理从孤岛走向网络：不同能力的智能体可以像互联网服务那样彼此发现、通信和协调，在更大范围内实现自治智能。对于企业，它带来了跨应用AI自动化的新范式，预示着更高的效率和创新空间；对于开发者，它提供了标准工具和生态，大幅降低了多代理开发的门槛。</p>
<h3 id="参考文献" tabindex="-1">参考文献 <a class="header-anchor" href="#参考文献" aria-label="Permalink to &quot;参考文献&quot;">&ZeroWidthSpace;</a></h3>
<ol>
<li><a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/#:~:text=Today%2C%20we%E2%80%99re%20launching%20a%20new%2C,be%20able%20to%20work%20across" target="_blank" rel="noreferrer">Rao Surapaneni 等, <em>Announcing the Agent2Agent Protocol (A2A)</em></a></li>
<li><a href="https://cloud.google.com/blog/topics/partners/best-agentic-ecosystem-helping-partners-build-ai-agents-next25#:~:text=,more%20in%20our%20technical%20blog" target="_blank" rel="noreferrer">Kevin Ichhpurani, <em>Building the industry’s best agentic AI ecosystem with partners</em></a></li>
<li><a href="https://github.com/google/A2A" target="_blank" rel="noreferrer">Google 开源项目, <em>Agent2Agent Protocol – README</em></a></li>
<li><a href="https://www.maginative.com/article/google-just-launched-agent2agent-an-open-protocol-for-ai-agents-to-work-directly-with-each-other/#:~:text=,time%20updates%2C%20and%20multimodal%20data" target="_blank" rel="noreferrer">Chris McKay, <em>Google just Launched Agent2Agent...</em> (Maginative, Apr 2025)</a></li>
<li><a href="https://finance.sina.com.cn/stock/relnews/us/2025-04-10/doc-inessiqx9134714.shtml#:~:text=Image%3A%20%E5%9B%BE%E6%BA%90%EF%BC%9A%E7%BD%91%E7%BB%9C%E5%9B%BE%E6%BA%90%EF%BC%9A%E7%BD%91%E7%BB%9C" target="_blank" rel="noreferrer">Ofweek维科网, <em>谷歌发布新Agent协议A2A...</em> (新浪财经转载)</a></li>
<li><a href="https://www.cnbeta.com.tw/articles/tech/1491788.htm#:~:text=A2A%20%E5%8D%8F%E8%AE%AE%E5%9F%BA%E4%BA%8E%E8%83%BD%E5%8A%9B%E5%8F%91%E7%8E%B0%E3%80%81%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86%E3%80%81%E5%8D%8F%E4%BD%9C%E5%92%8C%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C%E5%8D%8F%E5%95%86%E7%AD%89%E5%85%B3%E9%94%AE%E5%8E%9F%E5%88%99%E3%80%82%E4%BE%8B%E5%A6%82%EF%BC%8C%E4%BB%A3%E7%90%86%E5%8F%AF%E4%BB%A5%E9%80%9A%E8%BF%87%20JSON%20%E6%A0%BC%E5%BC%8F%E7%9A%84%E2%80%9C%E4%BB%A3%E7%90%86%E5%8D%A1%E2%80%9D%E5%8F%91%E5%B8%83%E5%85%B6%E8%83%BD%E5%8A%9B%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%85%81%E8%AE%B8%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BB%A3%E7%90%86%E8%AF%86%E5%88%AB%E6%9C%80%E9%80%82%E5%90%88%E4%BB%BB%E5%8A%A1%E7%9A%84%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%90%86%E3%80%82%E8%AF%A5%E5%8D%8F%E8%AE%AE%E8%BF%98%E6%9C%89%E5%8A%A9%E4%BA%8E%E4%BB%BB%E5%8A%A1%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%AE%A1%E7%90%86%EF%BC%8C%E5%AE%9E%E7%8E%B0%E4%BB%A3%E7%90%86%E4%B9%8B%E9%97%B4%E7%9A%84%E5%AE%9E%E6%97%B6%E5%90%8C%E6%AD%A5%E3%80%82A2A%20%E5%8D%8F%E8%AE%AE%E5%9F%BA%E4%BA%8E,HTTP%20%E5%92%8C%20JSON%20%E7%AD%89%E6%88%90%E7%86%9F%E6%A0%87%E5%87%86%E6%9E%84%E5%BB%BA%EF%BC%8C%E5%9C%A8%E7%A1%AE%E4%BF%9D%E4%B8%8E%E7%8E%B0%E6%9C%89%E7%B3%BB%E7%BB%9F%E5%85%BC%E5%AE%B9%E7%9A%84%E5%90%8C%E6%97%B6%EF%BC%8C%E4%BC%98%E5%85%88%E8%80%83%E8%99%91%E5%AE%89%E5%85%A8%E6%80%A7%E3%80%82" target="_blank" rel="noreferrer">cnBeta资讯, <em>Google推出Agent2Agent协议 跨平台连接AI代理</em></a></li>
</ol>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
        <item>
            <title><![CDATA[我开发了一个Hacker News上的每日产品精选]]></title>
            <link>https://justin3go.com/posts/2025/03/28-hacker-news-daily-product-picks</link>
            <guid>https://justin3go.com/posts/2025/03/28-hacker-news-daily-product-picks</guid>
            <pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="我开发了一个hacker-news上的每日产品精选" tabindex="-1">我开发了一个Hacker News上的每日产品精选 <a class="header-anchor" href="#我开发了一个hacker-news上的每日产品精选" aria-label="Permalink to &quot;我开发了一个Hacker News上的每日产品精选&quot;">&ZeroWidthSpace;</a></h1>
<blockquote>
<p>✨文章摘要（AI生成）</p>
</blockquote>
<!-- DESC SEP -->
<p>笔者开发了一个专注于 Hacker News 上&quot;Show HN&quot;板块的每日精选平台。作为一个自动化的内容聚合网站，它每天早上8:00会自动从 Hacker News 获取过去24小时内得分超过3分的优质帖子。系统使用 DeepSeek API 对内容进行智能总结，并将项目分类归档，最终生成易于阅读的 Markdown 格式报告。</p>
<p>该平台的一大特色是支持<strong>多语言翻译</strong>，可以自动将内容转换成包括中文在内的9种语言，让全球用户都能便捷获取这些优质内容。同时，通过<strong>智能分类系统</strong>，用户可以快速定位感兴趣的项目类别，比如开发者工具、生产力工具等。</p>
<blockquote>
<p>这是一个帮助用户高效获取 Hacker News 优质项目信息的平台，无论是寻找灵感还是发现好产品，都能在这里找到答案。</p>
</blockquote>
<!-- DESC SEP -->
<h2 id="简介" tabindex="-1">简介 <a class="header-anchor" href="#简介" aria-label="Permalink to &quot;简介&quot;">&ZeroWidthSpace;</a></h2>
<p>Hacker News 作为许多信息渠道的第一手信息源，上面每天都有许多非常优秀的项目诞生发布，每日关注它是非常必要的。</p>
<p>所以我做了这样一个网站，本站的目标是成为一个高效的 Show HN 内容精选平台，帮助用户快速了解每天值得关注的新项目。通过自动化的内容采集、分类和多语言翻译，让全球用户都能便捷地获取这些优质内容。</p>
<p>这里的内容是一个自动化脚本生成的，每天早上8:00从Hacker News获取得分超过3分的“Show HN”帖子，并使用DeepSeek API将其总结为Markdown格式的每日摘要。</p>
<p>访问地址：<a href="https://hunt0.com" target="_blank" rel="noreferrer">hunt0.com</a></p>
<h2 id="核心功能" tabindex="-1">核心功能 <a class="header-anchor" href="#核心功能" aria-label="Permalink to &quot;核心功能&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li>使用官方Hacker News <code>showstories</code> API直接检索Show HN类别的帖子</li>
<li>过滤出过去24小时内得分超过3分的优质内容</li>
<li>自动使用DeepSeek API总结产品/项目</li>
<li><strong>将帖子分类</strong>到预定义的类别中（例如，开发者工具、生产力等）</li>
<li>在每个帖子标题旁边显示类别，便于识别</li>
<li>以Markdown格式生成每日摘要报告</li>
<li>支持定时任务，每天早上8:00自动运行</li>
<li><strong>多语言支持</strong>：自动将摘要翻译成9种语言（中文、西班牙语、德语、日语、法语、俄语、葡萄牙语、意大利语和荷兰语）</li>
</ul>
<h2 id="预览" tabindex="-1">预览 <a class="header-anchor" href="#预览" aria-label="Permalink to &quot;预览&quot;">&ZeroWidthSpace;</a></h2>
<p>首页中英文预览（当然，支持10国语言，这里就不放出来了）：</p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250328155205.png" alt=""></p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250328155226.png" alt=""></p>
<p>详情页预览：</p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250328155243.png" alt=""></p>
<h2 id="技术实现" tabindex="-1">技术实现 <a class="header-anchor" href="#技术实现" aria-label="Permalink to &quot;技术实现&quot;">&ZeroWidthSpace;</a></h2>
<p>前面也简单介绍了一下，这里画一个流程图应该会更容易理解：</p>
<p><img src="https://oss.justin3go.com/blogs/Pasted%20image%2020250328161555.png" alt=""></p>
<p>代码就不开源了，Cursor糊的，我自己都没怎么看，功能反正实现了。</p>
<h2 id="最后" tabindex="-1">最后 <a class="header-anchor" href="#最后" aria-label="Permalink to &quot;最后&quot;">&ZeroWidthSpace;</a></h2>
<p>欢迎大家关注该日报，每天看看有什么有趣的产品发布，无论是找灵感，或者是发现好东西，都是非常方便的。</p>
]]></content:encoded>
            <author>just@justin3go.com (Justin3go)</author>
        </item>
    </channel>
</rss>