summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--2025/05/03/二〇二五年五月三日/index.html5
-rw-r--r--2025/05/07/nestjs-bullmq-mail-business/index.html328
-rw-r--r--archives/2025/05/index.html18
-rw-r--r--archives/2025/index.html36
-rw-r--r--archives/2025/page/2/index.html18
-rw-r--r--archives/index.html36
-rw-r--r--archives/page/10/index.html36
-rw-r--r--archives/page/11/index.html36
-rw-r--r--archives/page/12/index.html38
-rw-r--r--archives/page/13/index.html18
-rw-r--r--archives/page/2/index.html38
-rw-r--r--archives/page/3/index.html36
-rw-r--r--archives/page/4/index.html36
-rw-r--r--archives/page/5/index.html36
-rw-r--r--archives/page/6/index.html36
-rw-r--r--archives/page/7/index.html36
-rw-r--r--archives/page/8/index.html36
-rw-r--r--archives/page/9/index.html36
-rw-r--r--search.xml51
-rw-r--r--tags/Technique/index.html36
-rw-r--r--tags/Technique/page/2/index.html36
-rw-r--r--tags/Technique/page/3/index.html36
-rw-r--r--tags/Technique/page/4/index.html36
-rw-r--r--tags/Technique/page/5/index.html18
24 files changed, 762 insertions, 310 deletions
diff --git a/2025/05/03/二〇二五年五月三日/index.html b/2025/05/03/二〇二五年五月三日/index.html
index 9089fe3a..97570242 100644
--- a/2025/05/03/二〇二五年五月三日/index.html
+++ b/2025/05/03/二〇二五年五月三日/index.html
@@ -201,6 +201,11 @@
<nav class="post-nav">
<div class="prev-item">
+ <div class="icon arrow-left"></div>
+ <div class="post-link">
+ <a href="/2025/05/07/nestjs-bullmq-mail-business/">Prev</a>
+ </div>
+
</div>
<div class="next-item">
diff --git a/2025/05/07/nestjs-bullmq-mail-business/index.html b/2025/05/07/nestjs-bullmq-mail-business/index.html
new file mode 100644
index 00000000..a6e47cc2
--- /dev/null
+++ b/2025/05/07/nestjs-bullmq-mail-business/index.html
@@ -0,0 +1,328 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+<meta http-equiv="X-UA-Compatible" content="ie=edge">
+
+ <meta name="author" content="韩暮秋">
+
+
+ <meta name="subtitle" content="暮秋小屋">
+
+
+ <meta name="description" content="这里是暮秋小屋,思念和灵感的寄存处">
+
+
+ <meta name="keywords" content="韩暮秋,MuqiuHan,'Muqiu Han', 'muqiu han', muqiuhan">
+
+
+
+
+ <title>
+
+ NestJS bullmq 邮件发送业务中的小 tips |
+ 暮秋小屋
+ </title>
+
+
+
+ <link rel="icon" href="/favicon.ico">
+
+
+ <style>
+ @font-face {
+ font-family: CarroisSong;
+ src: url('/fonts/CarroisSong.ttf');
+ }
+ </style>
+
+
+
+ <!-- stylesheets list from _config.yml -->
+
+ <link rel="stylesheet" href="/css/style.css">
+
+
+
+
+
+ <!-- scripts list from _config.yml -->
+
+ <script
+ src="/js/menu.js"></script>
+
+
+
+
+
+ <script
+ src="https://polyfill.alicdn.com/polyfill.js?features=es6"></script>
+ <script
+ id="MathJax-script"
+ async
+ src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/mathjax/3.2.0/es5/tex-mml-chtml.js"></script>
+
+
+
+
+ <meta name="generator" content="Hexo 6.3.0"></head>
+ <body>
+ <div class="mask-border">
+ </div>
+
+ <div class="wrapper">
+
+ <div class="header">
+ <div class="flex-container">
+ <div class="header-inner">
+ <div class="site-brand-container">
+ <a href="/">
+
+ 暮秋小屋
+
+ </a>
+ </div>
+ <div id="menu-btn" class="menu-btn" onclick="toggleMenu()">
+ Menu
+ </div>
+ <nav class="site-nav">
+ <ul class="menu-list">
+
+
+ <li class="menu-item">
+ <a href="/">
+ 主页
+ </a>
+ </li>
+
+
+
+ <li class="menu-item">
+ <a href="/categories/gallery/">
+ 日记本
+ </a>
+ </li>
+
+
+
+ <li class="menu-item">
+ <a href="/tags/Medicine/">
+ 医学
+ </a>
+ </li>
+
+
+
+ <li class="menu-item">
+ <a href="/tags/Technique/">
+ 计算机/互联网
+ </a>
+ </li>
+
+
+
+ <li class="menu-item">
+ <a href="/tags/Life/">
+ 生活
+ </a>
+ </li>
+
+
+
+ <li class="menu-item">
+ <a href="/archives">
+ 全部
+ </a>
+ </li>
+
+
+
+ <li class="menu-item">
+ <a href="/about">
+ 关于
+ </a>
+ </li>
+
+
+
+ <li class="menu-item search-btn">
+ <a href="#">Search</a>
+ </li>
+
+ </ul>
+ </nav>
+ </div>
+ </div>
+</div>
+
+ <div class="main">
+ <div class="flex-container">
+ <article id="post">
+
+
+ <div class="post-head">
+ <div class="post-info">
+ <div class="tag-list">
+
+
+ <span class="post-tag">
+ <a href="/tags/Technique/">
+ Technique
+ </a>
+ </span>
+
+
+ </div>
+ <div class="post-title">
+
+
+ NestJS bullmq 邮件发送业务中的小 tips
+
+
+ </div>
+ <span class="post-date">
+ May 7, 2025
+ </span>
+ </div>
+ <div class="post-img">
+
+ <div class="h-line-primary"></div>
+
+ </div>
+</div>
+ <div class="post-content">
+ <p>在 BullMQ(以及它在 NestJS 里包装的 <code>@Processor</code>&#x2F;<code>WorkerHost</code>)里,整个生命周期大致是这样的:</p>
+<ol>
+<li>队列(在 NestJS 里由 <code>@Processor</code> 装饰的类)会被一个底层的 <code>Worker</code> 订阅。 </li>
+<li>有新任务(job)进来时,Worker 会调用写在该类里的 <code>async process(job: Job)</code> 方法。 </li>
+<li>如果 <code>process()</code> 正常返回(即没有抛异常),Job 就被标记为 <strong>completed</strong>,然后才会去触发所有注册了 <code>@OnWorkerEvent(&#39;completed&#39;)</code> 的回调。</li>
+</ol>
+<p>也就是说:</p>
+<ul>
+<li>**<code>process</code>**:是真正“干活”的地方,收到 job 之后立刻被调用,任何主业务逻辑(发邮件/写数据库/第三方请求等)都应该放这里。 </li>
+<li><strong><code>onCompleted</code><strong>:只是一个事件监听器,</strong>在 job 已经成功完成之后</strong> 才会被触发,不会影响 job 的重试逻辑(也就是说,在这里抛错,job 已经算完成了,也不会重试)。</li>
+</ul>
+<p>而我在此处的业务目的是 “用队列来做可靠的、可重试的邮件发送”,那么<strong>一定要把发送邮件的逻辑写到 <code>process()</code> 里</strong>,这样在 <code>commandBus.execute(new SendMailCommand(...))</code> 抛错时,BullMQ 会根据创建 JOB 时的重试策略(retry、backoff 等)自动重新入队。而把它放到 <code>onCompleted()</code>,只相当于 job 成功完成后的“事后通知”,一旦失败不会再重试,也无法利用 BullMQ 的锁、超时、重试机制。</p>
+<p>举个最简化的调整示例,删掉 <code>onCompleted</code>,把真正的发信放到 <code>process</code>: </p>
+<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">// ... existing imports ...</span><br><span class="line"></span><br><span class="line">@Processor(process.env.MAILER_QUEUE_NAME || &quot;gcpm-mailer&quot;)</span><br><span class="line">export class BullMQMailerProcesser extends WorkerHost &#123;</span><br><span class="line"> constructor(</span><br><span class="line"> private readonly commandBus: CommandBus,</span><br><span class="line"> private readonly logger: LoggingService,</span><br><span class="line"> ) &#123;</span><br><span class="line"> super();</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // ① 当有新 job 拉取到时,这个方法会被调用</span><br><span class="line"> public async process(job: Job): Promise&lt;void&gt; &#123;</span><br><span class="line"> const mailAggregate = new Mail(job.data.mail);</span><br><span class="line"> try &#123;</span><br><span class="line"> await this.commandBus.execute(new SendMailCommand(mailAggregate));</span><br><span class="line"> &#125; catch (err) &#123;</span><br><span class="line"> this.logger.error(`邮件发送失败,jobId=$&#123;job.id&#125;`, err);</span><br><span class="line"> // 抛出错误,触发重试或失败</span><br><span class="line"> throw err;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // ② onCompleted 仅在 process() 正常返回后触发,</span><br><span class="line"> // 不建议在这里执行核心业务(也无法触发重试)。</span><br><span class="line"> // @OnWorkerEvent(&quot;completed&quot;)</span><br><span class="line"> // async onCompleted(job: Job) &#123; … &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
+
+<p>参考 BullMQ 官方文档:</p>
+<ul>
+<li>“Workers → Sandboxed processors”:Worker 拉到 job 就调用注册的处理函数,然后根据返回&#x2F;抛错把 job 标记成 completed 或 failed。 </li>
+<li>“Events → OnJobCompleted”:completed 事件只是一个监听钩子,不会参与重试。</li>
+</ul>
+<hr>
+<p>而 <strong>重试次数本身并没有一个硬性上限</strong>,完全由添加 Job 时通过 <code>attempts</code> 这个选项来控制:</p>
+<ul>
+<li>默认情况下,如果不传 <code>attempts</code>(或不在 <code>defaultJobOptions</code> 里配置),Job <strong>不会自动重试</strong>(相当于 <code>attempts = 0</code>)。 </li>
+<li>如果在 <code>queue.add()</code>(或全局 <code>defaultJobOptions</code>)里设置了 <code>attempts: N</code>,那么 BullMQ 最多会让该 Job 运行 N 次(也就是初始执行 + N−1 次重试,或者根据文档含义最多触发 N 次失败) ,失败后才算真正移入失败集合。 </li>
+<li><code>attempts</code> 可以是任意的正整数(受 JavaScript <code>Number</code> 范围限制),BullMQ 本身不会再做额外的上限检查。</li>
+</ul>
+<p>示例(给某封邮件最多重试 3 次):</p>
+<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123; <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(<span class="comment">/*…*/</span> ) &#125;,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">attempts</span>: <span class="number">3</span>, <span class="comment">// 最多尝试 3 次</span></span><br><span class="line"> <span class="attr">backoff</span>: &#123; <span class="comment">// 重试时的延迟策略(可选)</span></span><br><span class="line"> <span class="attr">type</span>: <span class="string">&#x27;exponential&#x27;</span>,</span><br><span class="line"> <span class="attr">delay</span>: <span class="number">1000</span>,</span><br><span class="line"> &#125;,</span><br><span class="line"> &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
+
+<hr>
+<p>还有一个需要注意的地方,在我的业务中,邮件发送的是一种时间区间报告,这个报告包含了过去二十四小时的一些系统中的事件,但如果重试有延迟策略或重试本身就有计算成本的话,这封邮件就不是 “过去二十四小时” 的了,因为重试带来了一个真空期。</p>
+<p>换言之,这个问题本质上是——<strong>重试导致「发送时刻」与「原始 24 小时窗口」错开</strong>,从而让邮件里报出来的数据不再精确。常见的解决思路就是:<strong>把「窗口定义」或者「报表内容」在调度时就固化下来,真正的队列任务只负责发送</strong>,而不再实时去重新计算时间区间。</p>
+<p>我想到了两种解决方案:</p>
+<p>一、任务参数里带上「时间区间」<br> 在 enqueue 的时候,就算出 windowStart&#x2F;windowEnd,然后把它放到 <code>job.data</code> 里。无论后面 <code>process</code> 什么时候真正跑,都是基于同一个时间区间去查询:</p>
+ <figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调度时</span></span><br><span class="line"><span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"><span class="keyword">const</span> windowStart = <span class="keyword">new</span> <span class="title class_">Date</span>(now.<span class="title function_">getTime</span>() - <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span> * <span class="number">1000</span>);</span><br><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(&#123;</span><br><span class="line"> ...options,</span><br><span class="line"> id,</span><br><span class="line"> <span class="attr">sentAt</span>: now,</span><br><span class="line"> <span class="attr">status</span>: <span class="title class_">MailStatus</span>.<span class="property">PENDING</span>,</span><br><span class="line"> windowStart,</span><br><span class="line"> <span class="attr">windowEnd</span>: now,</span><br><span class="line"> &#125;),</span><br><span class="line"> &#125;,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">attempts</span>: <span class="number">3</span>,</span><br><span class="line"> <span class="attr">backoff</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;exponential&#x27;</span>, <span class="attr">delay</span>: <span class="number">1000</span> &#125;,</span><br><span class="line"> &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// process 里</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">process</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">Job</span></span>) &#123;</span><br><span class="line"> <span class="keyword">const</span> &#123; windowStart, windowEnd &#125; = job.<span class="property">data</span>.<span class="property">mail</span>;</span><br><span class="line"> <span class="comment">// ① 只查询 [windowStart, windowEnd] 的事件</span></span><br><span class="line"> <span class="keyword">const</span> events = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">findEvents</span>(windowStart, windowEnd);</span><br><span class="line"> <span class="keyword">const</span> reportHtml = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">renderReport</span>(events);</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">commandBus</span>.<span class="title function_">execute</span>(<span class="keyword">new</span> <span class="title class_">SendMailCommand</span>(job.<span class="property">data</span>.<span class="property">mail</span>, reportHtml));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
+<p> ➜ 这样无是马上执行还是几次重试后才执行,数据规则都不会变。</p>
+<p>二、预先生成「静态报表内容」,挂到队列里<br> 如果计算成本很高,或者怕重复查询数据开销大,也可以在调度时就把最终的 HTML&#x2F;Text&#x2F;附件 都先打好,然后作为 <code>job.data</code> 传进去,真正的 <code>process()</code> 只做一次“发送”即可:<br> <figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调度时:先生成报告</span></span><br><span class="line"><span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"><span class="keyword">const</span> windowStart = <span class="keyword">new</span> <span class="title class_">Date</span>(now.<span class="title function_">getTime</span>() - <span class="number">24</span>*<span class="number">3600</span>*<span class="number">1000</span>);</span><br><span class="line"><span class="keyword">const</span> events = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">findEvents</span>(windowStart, now);</span><br><span class="line"><span class="keyword">const</span> reportHtml = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">renderReport</span>(events);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 把静态内容塞到队列</span></span><br><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(&#123; <span class="comment">/*…*/</span>, windowStart, <span class="attr">windowEnd</span>: now &#125;),</span><br><span class="line"> reportHtml, <span class="comment">// &lt;- 预渲染好的文本/HTML</span></span><br><span class="line"> <span class="attr">attachments</span>: […], <span class="comment">// &lt;- 如果有附件也一并塞</span></span><br><span class="line"> &#125;,</span><br><span class="line"> &#123; <span class="attr">attempts</span>: <span class="number">3</span>, <span class="attr">backoff</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;fixed&#x27;</span>, <span class="attr">delay</span>: <span class="number">5_000</span> &#125; &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// process 里只关注发送</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">process</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">Job</span></span>) &#123;</span><br><span class="line"> <span class="keyword">try</span> &#123;</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailService</span>.<span class="title function_">send</span>(&#123;</span><br><span class="line"> <span class="attr">to</span>: job.<span class="property">data</span>.<span class="property">mail</span>.<span class="property">to</span>,</span><br><span class="line"> <span class="attr">subject</span>: <span class="string">`系统 24h 报表`</span>,</span><br><span class="line"> <span class="attr">html</span>: job.<span class="property">data</span>.<span class="property">reportHtml</span>,</span><br><span class="line"> <span class="attr">attachments</span>: job.<span class="property">data</span>.<span class="property">attachments</span>,</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line"> <span class="keyword">throw</span> e; <span class="comment">// 触发重试</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br> ➜ 重试带来的任何延迟,都不影响邮件正文,始终是一份「事先约定好、并且静态化」的报告。</p>
+<p>这两种模式都能保证<strong>最终发送时的数据窗口</strong>或<strong>内容</strong>,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算&#x2F;少算”问题。</p>
+<h2 id="参考文档:"><a href="#参考文档:" class="headerlink" title="参考文档:"></a>参考文档:</h2><ul>
+<li>“Retrying failing jobs” · BullMQ Guide<br><a target="_blank" rel="noopener" href="https://docs.bullmq.io/guide/retrying-failing-jobs">https://docs.bullmq.io/guide/retrying-failing-jobs</a></li>
+<li>BullMQ Guide &amp; Patterns · Process Step Jobs (completed event only fires after process resolves)<br><a target="_blank" rel="noopener" href="https://docs.bullmq.io/patterns/process-step-jobs">https://docs.bullmq.io/patterns/process-step-jobs</a></li>
+</ul>
+
+</div>
+
+<script>
+ window.onload = detectors();
+</script>
+ <div class="post-footer">
+ <div class="h-line-primary"></div>
+ <nav class="post-nav">
+ <div class="prev-item">
+
+ </div>
+ <div class="next-item">
+
+ <div class="icon arrow-right"></div>
+ <div class="post-link">
+ <a href="/2025/05/03/%E4%BA%8C%E3%80%87%E4%BA%8C%E4%BA%94%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%89%E6%97%A5/">Next</a>
+ </div>
+
+ </div>
+ </nav>
+</div>
+
+
+ <div class="post-comment">
+
+
+
+
+
+
+
+</div>
+
+
+</article>
+ </div>
+ </div>
+
+ <div class="footer">
+ <div class="flex-container">
+ <div class="footer-text">
+
+
+ 韩暮秋 |
+
+
+ 希望路过的人可以添点柴火让这里暖和点
+
+ </div>
+ </div>
+</div>
+
+ </div>
+
+
+ <div class="search-popup">
+ <div class="search-popup-overlay">
+ </div>
+ <div class="search-popup-window" >
+ <div class="search-header">
+ <div class="search-input-container">
+ <input autocomplete="off" autocapitalize="off" maxlength="80"
+ placeholder="Search Anything" spellcheck="false"
+ type="search" class="search-input">
+ </div>
+ <div class="search-close-btn">
+ <div class="icon close-btn"></div>
+ </div>
+ </div>
+ <div class="search-result-container">
+ </div>
+ </div>
+</div>
+
+<script>
+ const searchConfig = {
+ path : "/search.xml",
+ top_n_per_article: "1",
+ unescape : "false",
+ trigger: "auto",
+ preload: "false"
+ }
+</script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/search.js"></script>
+<script src="/js/search.js"></script>
+
+
+
+ </body>
+</html>
diff --git a/archives/2025/05/index.html b/archives/2025/05/index.html
index ae5be25d..06a07bc1 100644
--- a/archives/2025/05/index.html
+++ b/archives/2025/05/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/05/07/nestjs-bullmq-mail-business/">
+
+ NestJS bullmq 邮件发送业务中的小 tips
+
+ </a>
+ </div>
+
+ <span class="post-date">May 7, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/05/03/%E4%BA%8C%E3%80%87%E4%BA%8C%E4%BA%94%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%89%E6%97%A5/">
二〇二五年五月三日
diff --git a/archives/2025/index.html b/archives/2025/index.html
index b7954627..e2773b2c 100644
--- a/archives/2025/index.html
+++ b/archives/2025/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/05/07/nestjs-bullmq-mail-business/">
+
+ NestJS bullmq 邮件发送业务中的小 tips
+
+ </a>
+ </div>
+
+ <span class="post-date">May 7, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/05/03/%E4%BA%8C%E3%80%87%E4%BA%8C%E4%BA%94%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%89%E6%97%A5/">
二〇二五年五月三日
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2025/03/19/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E8%81%9A%E5%90%88%E6%A0%B9%E6%8C%81%E4%B9%85%E5%8C%96%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%8F%91%E5%B8%83%E5%8F%AF%E8%83%BD%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98/">
-
- 领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题
-
- </a>
- </div>
-
- <span class="post-date">Mar 19, 2025</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/2025/page/2/index.html b/archives/2025/page/2/index.html
index baf46af6..05e87a0b 100644
--- a/archives/2025/page/2/index.html
+++ b/archives/2025/page/2/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/03/19/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E8%81%9A%E5%90%88%E6%A0%B9%E6%8C%81%E4%B9%85%E5%8C%96%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%8F%91%E5%B8%83%E5%8F%AF%E8%83%BD%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98/">
+
+ 领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 19, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/03/17/%E4%BA%8C%E3%80%87%E4%BA%8C%E4%BA%94%E5%B9%B4%E4%B8%89%E6%9C%88%E5%8D%81%E4%B8%83%E6%97%A5/">
二〇二五年三月十七日
diff --git a/archives/index.html b/archives/index.html
index f3c19691..e8524151 100644
--- a/archives/index.html
+++ b/archives/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/05/07/nestjs-bullmq-mail-business/">
+
+ NestJS bullmq 邮件发送业务中的小 tips
+
+ </a>
+ </div>
+
+ <span class="post-date">May 7, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/05/03/%E4%BA%8C%E3%80%87%E4%BA%8C%E4%BA%94%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%89%E6%97%A5/">
二〇二五年五月三日
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2025/03/19/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E8%81%9A%E5%90%88%E6%A0%B9%E6%8C%81%E4%B9%85%E5%8C%96%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%8F%91%E5%B8%83%E5%8F%AF%E8%83%BD%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98/">
-
- 领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题
-
- </a>
- </div>
-
- <span class="post-date">Mar 19, 2025</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/10/index.html b/archives/page/10/index.html
index a7654d3b..957b6e8b 100644
--- a/archives/page/10/index.html
+++ b/archives/page/10/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/06/17/%E5%8F%91%E7%83%AD%E5%8F%8A%E8%B6%85%E9%AB%98%E7%83%AD%E5%8D%B1%E8%B1%A1/">
+
+ 发热及超高热危象
+
+ </a>
+ </div>
+
+ <span class="post-date">Jun 17, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/06/10/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AD%E6%9C%88%E5%8D%81%E6%97%A5/">
二零二三年六月十日
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/03/14/%E5%91%8B%E5%A1%9E%E7%B1%B3/">
-
- 呋塞米
-
- </a>
- </div>
-
- <span class="post-date">Mar 14, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/11/index.html b/archives/page/11/index.html
index a66cd450..518de872 100644
--- a/archives/page/11/index.html
+++ b/archives/page/11/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/03/14/%E5%91%8B%E5%A1%9E%E7%B1%B3/">
+
+ 呋塞米
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 14, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/03/13/%E6%9B%BF%E7%B1%B3%E6%B2%99%E5%9D%A6/">
替米沙坦
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/02/02/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E4%BA%8C%E6%9C%88%E4%BA%8C%E6%97%A5/">
-
- 二零二三年二月二日
-
- </a>
- </div>
-
- <span class="post-date">Feb 2, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/12/index.html b/archives/page/12/index.html
index 9a23289e..d1cb156e 100644
--- a/archives/page/12/index.html
+++ b/archives/page/12/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/02/02/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E4%BA%8C%E6%9C%88%E4%BA%8C%E6%97%A5/">
+
+ 二零二三年二月二日
+
+ </a>
+ </div>
+
+ <span class="post-date">Feb 2, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/02/01/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E4%BA%8C%E6%9C%88%E4%B8%80%E6%97%A5/">
二零二三年二月一日
@@ -327,26 +345,6 @@
</div>
-
-
-
-
- <div class="year-title">2021</div>
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2021/03/27/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%E5%B9%B4%E4%B8%89%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%83%E6%97%A5/">
-
- 二零二一年三月二十七日
-
- </a>
- </div>
-
- <span class="post-date">Mar 27, 2021</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/13/index.html b/archives/page/13/index.html
index 6c120e3a..2ad6e3ed 100644
--- a/archives/page/13/index.html
+++ b/archives/page/13/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2021/03/27/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%E5%B9%B4%E4%B8%89%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%83%E6%97%A5/">
+
+ 二零二一年三月二十七日
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 27, 2021</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2021/03/15/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%80%E5%B9%B4%E4%B8%89%E6%9C%88%E5%8D%81%E4%BA%94%E6%97%A5/">
二零二一年三月十五日
diff --git a/archives/page/2/index.html b/archives/page/2/index.html
index 2c2f3d47..c551b531 100644
--- a/archives/page/2/index.html
+++ b/archives/page/2/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/03/19/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E8%81%9A%E5%90%88%E6%A0%B9%E6%8C%81%E4%B9%85%E5%8C%96%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%8F%91%E5%B8%83%E5%8F%AF%E8%83%BD%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98/">
+
+ 领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 19, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/03/17/%E4%BA%8C%E3%80%87%E4%BA%8C%E4%BA%94%E5%B9%B4%E4%B8%89%E6%9C%88%E5%8D%81%E4%B8%83%E6%97%A5/">
二〇二五年三月十七日
@@ -325,26 +343,6 @@
</div>
-
-
-
-
- <div class="year-title">2024</div>
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/12/18/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%8D%81%E4%BA%8C%E6%9C%88%E5%8D%81%E5%85%AB%E6%97%A5/">
-
- 二〇二四年十二月十八日
-
- </a>
- </div>
-
- <span class="post-date">Dec 18, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/3/index.html b/archives/page/3/index.html
index 34264fea..cb8d1350 100644
--- a/archives/page/3/index.html
+++ b/archives/page/3/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/12/18/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%8D%81%E4%BA%8C%E6%9C%88%E5%8D%81%E5%85%AB%E6%97%A5/">
+
+ 二〇二四年十二月十八日
+
+ </a>
+ </div>
+
+ <span class="post-date">Dec 18, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/12/15/OCaml-News-2024-7/">
OCaml News 2024-7
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/09/15/%E8%82%A9%E5%91%A8%E7%82%8E%E5%92%8C%E8%82%A9%E8%A2%96%E6%8D%9F%E4%BC%A4%E7%9A%84%E5%8C%BA%E5%88%AB/">
-
- 肩周炎和肩袖损伤的区别
-
- </a>
- </div>
-
- <span class="post-date">Sep 15, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/4/index.html b/archives/page/4/index.html
index 716ef52d..c713d295 100644
--- a/archives/page/4/index.html
+++ b/archives/page/4/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/09/15/%E8%82%A9%E5%91%A8%E7%82%8E%E5%92%8C%E8%82%A9%E8%A2%96%E6%8D%9F%E4%BC%A4%E7%9A%84%E5%8C%BA%E5%88%AB/">
+
+ 肩周炎和肩袖损伤的区别
+
+ </a>
+ </div>
+
+ <span class="post-date">Sep 15, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/09/15/Advanced-C-binding-using-ocaml-ctypes-and-dune/">
Advanced C binding using ocaml-ctypes and dune
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/07/23/OCaml-News-2024-6/">
-
- OCaml News 2024-6
-
- </a>
- </div>
-
- <span class="post-date">Jul 23, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/5/index.html b/archives/page/5/index.html
index 6c82c27a..87330e25 100644
--- a/archives/page/5/index.html
+++ b/archives/page/5/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/07/23/OCaml-News-2024-6/">
+
+ OCaml News 2024-6
+
+ </a>
+ </div>
+
+ <span class="post-date">Jul 23, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/07/09/%E7%A5%9E%E7%BB%8F%E7%B3%BB%E7%BB%9F%E9%87%8D%E7%82%B9/">
内护神经系统重点考点
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/04/08/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%9B%9B%E6%9C%88%E5%85%AB%E6%97%A5/">
-
- 二〇二四年四月八日
-
- </a>
- </div>
-
- <span class="post-date">Apr 8, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/6/index.html b/archives/page/6/index.html
index 029aee4a..dc59824d 100644
--- a/archives/page/6/index.html
+++ b/archives/page/6/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/04/08/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%9B%9B%E6%9C%88%E5%85%AB%E6%97%A5/">
+
+ 二〇二四年四月八日
+
+ </a>
+ </div>
+
+ <span class="post-date">Apr 8, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/04/03/shadow-cljs-proxy/">
shadow-cljs 怎么设置代理
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/01/18/OCaml-News-2024-2/">
-
- OCaml News 2024-2
-
- </a>
- </div>
-
- <span class="post-date">Jan 18, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/7/index.html b/archives/page/7/index.html
index 3d05c30a..02fe542c 100644
--- a/archives/page/7/index.html
+++ b/archives/page/7/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/01/18/OCaml-News-2024-2/">
+
+ OCaml News 2024-2
+
+ </a>
+ </div>
+
+ <span class="post-date">Jan 18, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/01/08/OCaml-News-2024-1/">
OCaml News 2024-1
@@ -327,24 +345,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/10/25/%E8%82%BA%E7%82%8E%E6%94%AF%E5%8E%9F%E4%BD%93%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/">
-
- 肺炎支原体注意事项
-
- </a>
- </div>
-
- <span class="post-date">Oct 25, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/8/index.html b/archives/page/8/index.html
index ae097edc..1fc94df2 100644
--- a/archives/page/8/index.html
+++ b/archives/page/8/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/10/25/%E8%82%BA%E7%82%8E%E6%94%AF%E5%8E%9F%E4%BD%93%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/">
+
+ 肺炎支原体注意事项
+
+ </a>
+ </div>
+
+ <span class="post-date">Oct 25, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/10/22/%E5%86%AC%E5%AD%A3%E5%92%B3%E5%97%BD%E8%BE%A8%E5%88%AB%E6%8C%87%E5%8D%97/">
冬季咳嗽辨别指南
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/08/21/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AB%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%80%E6%97%A5/">
-
- 二零二三年八月二十一日
-
- </a>
- </div>
-
- <span class="post-date">Aug 21, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/9/index.html b/archives/page/9/index.html
index 51ac715e..47d45afc 100644
--- a/archives/page/9/index.html
+++ b/archives/page/9/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/08/21/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AB%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%80%E6%97%A5/">
+
+ 二零二三年八月二十一日
+
+ </a>
+ </div>
+
+ <span class="post-date">Aug 21, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/08/15/tick-thread%E5%9C%A8Multicore-OCaml%E4%B8%AD%E7%9A%84%E4%BD%9C%E7%94%A8/">
tick thread在Multicore OCaml中的作用
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/06/17/%E5%8F%91%E7%83%AD%E5%8F%8A%E8%B6%85%E9%AB%98%E7%83%AD%E5%8D%B1%E8%B1%A1/">
-
- 发热及超高热危象
-
- </a>
- </div>
-
- <span class="post-date">Jun 17, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/search.xml b/search.xml
index dcfcaa88..b1597b59 100644
--- a/search.xml
+++ b/search.xml
@@ -2428,6 +2428,57 @@
</tags>
</entry>
<entry>
+ <title>NestJS bullmq 邮件发送业务中的小 tips</title>
+ <url>/2025/05/07/nestjs-bullmq-mail-business/</url>
+ <content><![CDATA[<p>在 BullMQ(以及它在 NestJS 里包装的 <code>@Processor</code>&#x2F;<code>WorkerHost</code>)里,整个生命周期大致是这样的:</p>
+<ol>
+<li>队列(在 NestJS 里由 <code>@Processor</code> 装饰的类)会被一个底层的 <code>Worker</code> 订阅。 </li>
+<li>有新任务(job)进来时,Worker 会调用写在该类里的 <code>async process(job: Job)</code> 方法。 </li>
+<li>如果 <code>process()</code> 正常返回(即没有抛异常),Job 就被标记为 <strong>completed</strong>,然后才会去触发所有注册了 <code>@OnWorkerEvent(&#39;completed&#39;)</code> 的回调。</li>
+</ol>
+<p>也就是说:</p>
+<ul>
+<li>**<code>process</code>**:是真正“干活”的地方,收到 job 之后立刻被调用,任何主业务逻辑(发邮件/写数据库/第三方请求等)都应该放这里。 </li>
+<li><strong><code>onCompleted</code><strong>:只是一个事件监听器,</strong>在 job 已经成功完成之后</strong> 才会被触发,不会影响 job 的重试逻辑(也就是说,在这里抛错,job 已经算完成了,也不会重试)。</li>
+</ul>
+<p>而我在此处的业务目的是 “用队列来做可靠的、可重试的邮件发送”,那么<strong>一定要把发送邮件的逻辑写到 <code>process()</code> 里</strong>,这样在 <code>commandBus.execute(new SendMailCommand(...))</code> 抛错时,BullMQ 会根据创建 JOB 时的重试策略(retry、backoff 等)自动重新入队。而把它放到 <code>onCompleted()</code>,只相当于 job 成功完成后的“事后通知”,一旦失败不会再重试,也无法利用 BullMQ 的锁、超时、重试机制。</p>
+<p>举个最简化的调整示例,删掉 <code>onCompleted</code>,把真正的发信放到 <code>process</code>: </p>
+<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// ... existing imports ...</span><br><span class="line"></span><br><span class="line">@Processor(process.env.MAILER_QUEUE_NAME || &quot;gcpm-mailer&quot;)</span><br><span class="line">export class BullMQMailerProcesser extends WorkerHost &#123;</span><br><span class="line"> constructor(</span><br><span class="line"> private readonly commandBus: CommandBus,</span><br><span class="line"> private readonly logger: LoggingService,</span><br><span class="line"> ) &#123;</span><br><span class="line"> super();</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // ① 当有新 job 拉取到时,这个方法会被调用</span><br><span class="line"> public async process(job: Job): Promise&lt;void&gt; &#123;</span><br><span class="line"> const mailAggregate = new Mail(job.data.mail);</span><br><span class="line"> try &#123;</span><br><span class="line"> await this.commandBus.execute(new SendMailCommand(mailAggregate));</span><br><span class="line"> &#125; catch (err) &#123;</span><br><span class="line"> this.logger.error(`邮件发送失败,jobId=$&#123;job.id&#125;`, err);</span><br><span class="line"> // 抛出错误,触发重试或失败</span><br><span class="line"> throw err;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // ② onCompleted 仅在 process() 正常返回后触发,</span><br><span class="line"> // 不建议在这里执行核心业务(也无法触发重试)。</span><br><span class="line"> // @OnWorkerEvent(&quot;completed&quot;)</span><br><span class="line"> // async onCompleted(job: Job) &#123; … &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
+
+<p>参考 BullMQ 官方文档:</p>
+<ul>
+<li>“Workers → Sandboxed processors”:Worker 拉到 job 就调用注册的处理函数,然后根据返回&#x2F;抛错把 job 标记成 completed 或 failed。 </li>
+<li>“Events → OnJobCompleted”:completed 事件只是一个监听钩子,不会参与重试。</li>
+</ul>
+<hr>
+<p>而 <strong>重试次数本身并没有一个硬性上限</strong>,完全由添加 Job 时通过 <code>attempts</code> 这个选项来控制:</p>
+<ul>
+<li>默认情况下,如果不传 <code>attempts</code>(或不在 <code>defaultJobOptions</code> 里配置),Job <strong>不会自动重试</strong>(相当于 <code>attempts = 0</code>)。 </li>
+<li>如果在 <code>queue.add()</code>(或全局 <code>defaultJobOptions</code>)里设置了 <code>attempts: N</code>,那么 BullMQ 最多会让该 Job 运行 N 次(也就是初始执行 + N−1 次重试,或者根据文档含义最多触发 N 次失败) ,失败后才算真正移入失败集合。 </li>
+<li><code>attempts</code> 可以是任意的正整数(受 JavaScript <code>Number</code> 范围限制),BullMQ 本身不会再做额外的上限检查。</li>
+</ul>
+<p>示例(给某封邮件最多重试 3 次):</p>
+<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123; <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(<span class="comment">/*…*/</span> ) &#125;,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">attempts</span>: <span class="number">3</span>, <span class="comment">// 最多尝试 3 次</span></span><br><span class="line"> <span class="attr">backoff</span>: &#123; <span class="comment">// 重试时的延迟策略(可选)</span></span><br><span class="line"> <span class="attr">type</span>: <span class="string">&#x27;exponential&#x27;</span>,</span><br><span class="line"> <span class="attr">delay</span>: <span class="number">1000</span>,</span><br><span class="line"> &#125;,</span><br><span class="line"> &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
+
+<hr>
+<p>还有一个需要注意的地方,在我的业务中,邮件发送的是一种时间区间报告,这个报告包含了过去二十四小时的一些系统中的事件,但如果重试有延迟策略或重试本身就有计算成本的话,这封邮件就不是 “过去二十四小时” 的了,因为重试带来了一个真空期。</p>
+<p>换言之,这个问题本质上是——<strong>重试导致「发送时刻」与「原始 24 小时窗口」错开</strong>,从而让邮件里报出来的数据不再精确。常见的解决思路就是:<strong>把「窗口定义」或者「报表内容」在调度时就固化下来,真正的队列任务只负责发送</strong>,而不再实时去重新计算时间区间。</p>
+<p>我想到了两种解决方案:</p>
+<p>一、任务参数里带上「时间区间」<br> 在 enqueue 的时候,就算出 windowStart&#x2F;windowEnd,然后把它放到 <code>job.data</code> 里。无论后面 <code>process</code> 什么时候真正跑,都是基于同一个时间区间去查询:</p>
+ <figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 调度时</span></span><br><span class="line"><span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"><span class="keyword">const</span> windowStart = <span class="keyword">new</span> <span class="title class_">Date</span>(now.<span class="title function_">getTime</span>() - <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span> * <span class="number">1000</span>);</span><br><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(&#123;</span><br><span class="line"> ...options,</span><br><span class="line"> id,</span><br><span class="line"> <span class="attr">sentAt</span>: now,</span><br><span class="line"> <span class="attr">status</span>: <span class="title class_">MailStatus</span>.<span class="property">PENDING</span>,</span><br><span class="line"> windowStart,</span><br><span class="line"> <span class="attr">windowEnd</span>: now,</span><br><span class="line"> &#125;),</span><br><span class="line"> &#125;,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">attempts</span>: <span class="number">3</span>,</span><br><span class="line"> <span class="attr">backoff</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;exponential&#x27;</span>, <span class="attr">delay</span>: <span class="number">1000</span> &#125;,</span><br><span class="line"> &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// process 里</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">process</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">Job</span></span>) &#123;</span><br><span class="line"> <span class="keyword">const</span> &#123; windowStart, windowEnd &#125; = job.<span class="property">data</span>.<span class="property">mail</span>;</span><br><span class="line"> <span class="comment">// ① 只查询 [windowStart, windowEnd] 的事件</span></span><br><span class="line"> <span class="keyword">const</span> events = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">findEvents</span>(windowStart, windowEnd);</span><br><span class="line"> <span class="keyword">const</span> reportHtml = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">renderReport</span>(events);</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">commandBus</span>.<span class="title function_">execute</span>(<span class="keyword">new</span> <span class="title class_">SendMailCommand</span>(job.<span class="property">data</span>.<span class="property">mail</span>, reportHtml));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
+<p> ➜ 这样无是马上执行还是几次重试后才执行,数据规则都不会变。</p>
+<p>二、预先生成「静态报表内容」,挂到队列里<br> 如果计算成本很高,或者怕重复查询数据开销大,也可以在调度时就把最终的 HTML&#x2F;Text&#x2F;附件 都先打好,然后作为 <code>job.data</code> 传进去,真正的 <code>process()</code> 只做一次“发送”即可:<br> <figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 调度时:先生成报告</span></span><br><span class="line"><span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"><span class="keyword">const</span> windowStart = <span class="keyword">new</span> <span class="title class_">Date</span>(now.<span class="title function_">getTime</span>() - <span class="number">24</span>*<span class="number">3600</span>*<span class="number">1000</span>);</span><br><span class="line"><span class="keyword">const</span> events = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">findEvents</span>(windowStart, now);</span><br><span class="line"><span class="keyword">const</span> reportHtml = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">renderReport</span>(events);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 把静态内容塞到队列</span></span><br><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(&#123; <span class="comment">/*…*/</span>, windowStart, <span class="attr">windowEnd</span>: now &#125;),</span><br><span class="line"> reportHtml, <span class="comment">// &lt;- 预渲染好的文本/HTML</span></span><br><span class="line"> <span class="attr">attachments</span>: […], <span class="comment">// &lt;- 如果有附件也一并塞</span></span><br><span class="line"> &#125;,</span><br><span class="line"> &#123; <span class="attr">attempts</span>: <span class="number">3</span>, <span class="attr">backoff</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;fixed&#x27;</span>, <span class="attr">delay</span>: <span class="number">5_000</span> &#125; &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// process 里只关注发送</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">process</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">Job</span></span>) &#123;</span><br><span class="line"> <span class="keyword">try</span> &#123;</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailService</span>.<span class="title function_">send</span>(&#123;</span><br><span class="line"> <span class="attr">to</span>: job.<span class="property">data</span>.<span class="property">mail</span>.<span class="property">to</span>,</span><br><span class="line"> <span class="attr">subject</span>: <span class="string">`系统 24h 报表`</span>,</span><br><span class="line"> <span class="attr">html</span>: job.<span class="property">data</span>.<span class="property">reportHtml</span>,</span><br><span class="line"> <span class="attr">attachments</span>: job.<span class="property">data</span>.<span class="property">attachments</span>,</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line"> <span class="keyword">throw</span> e; <span class="comment">// 触发重试</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br> ➜ 重试带来的任何延迟,都不影响邮件正文,始终是一份「事先约定好、并且静态化」的报告。</p>
+<p>这两种模式都能保证<strong>最终发送时的数据窗口</strong>或<strong>内容</strong>,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算&#x2F;少算”问题。</p>
+<h2 id="参考文档:"><a href="#参考文档:" class="headerlink" title="参考文档:"></a>参考文档:</h2><ul>
+<li>“Retrying failing jobs” · BullMQ Guide<br><a href="https://docs.bullmq.io/guide/retrying-failing-jobs">https://docs.bullmq.io/guide/retrying-failing-jobs</a></li>
+<li>BullMQ Guide &amp; Patterns · Process Step Jobs (completed event only fires after process resolves)<br><a href="https://docs.bullmq.io/patterns/process-step-jobs">https://docs.bullmq.io/patterns/process-step-jobs</a></li>
+</ul>
+]]></content>
+ <tags>
+ <tag>Technique</tag>
+ </tags>
+ </entry>
+ <entry>
<title>使用 [@poll error] 实现线程安全的数据结构</title>
<url>/2023/08/15/poll-error-attribute-in-OCaml/</url>
<content><![CDATA[<p>OCaml的标准库提供了许多mutable的数据结构,比如Hashtbl, Queue, Stack之类的,但是这些数据结构都不是线程安全的。在 OCaml 4 和 OCaml 5 中,单个Domain中一次只能运行一个线程。换句话说,单个Domain中的线程仍然不会并行运行,除非在不同所的Domain中。</p>
diff --git a/tags/Technique/index.html b/tags/Technique/index.html
index aba6f836..fa6125b5 100644
--- a/tags/Technique/index.html
+++ b/tags/Technique/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/05/07/nestjs-bullmq-mail-business/">
+
+ NestJS bullmq 邮件发送业务中的小 tips
+
+ </a>
+ </div>
+
+ <span class="post-date">May 7, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/04/20/linux-amd-screen-boom/">
Linux 下 AMD 显卡屏幕画面闪烁问题修复方案
@@ -332,24 +350,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2025/03/13/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E7%9A%84%E2%80%9C%E8%81%9A%E5%90%88%E6%A0%B9%E2%80%9D/">
-
- 领域驱动设计中的“聚合根”
-
- </a>
- </div>
-
- <span class="post-date">Mar 13, 2025</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/2/index.html b/tags/Technique/page/2/index.html
index 0ba29ac7..e0783efe 100644
--- a/tags/Technique/page/2/index.html
+++ b/tags/Technique/page/2/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/03/13/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E7%9A%84%E2%80%9C%E8%81%9A%E5%90%88%E6%A0%B9%E2%80%9D/">
+
+ 领域驱动设计中的“聚合根”
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 13, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/02/19/Repository-pattern-in-Typescript/">
Repository pattern in Typescript
@@ -334,24 +352,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/09/27/Turborepo-%E7%AE%80%E8%BF%B0/">
-
- Turborepo 简述
-
- </a>
- </div>
-
- <span class="post-date">Sep 27, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/3/index.html b/tags/Technique/page/3/index.html
index 6a633673..d6d5efcd 100644
--- a/tags/Technique/page/3/index.html
+++ b/tags/Technique/page/3/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/09/27/Turborepo-%E7%AE%80%E8%BF%B0/">
+
+ Turborepo 简述
+
+ </a>
+ </div>
+
+ <span class="post-date">Sep 27, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/09/15/Advanced-C-binding-using-ocaml-ctypes-and-dune/">
Advanced C binding using ocaml-ctypes and dune
@@ -332,24 +350,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/01/23/G-Machine/">
-
- G-Machine
-
- </a>
- </div>
-
- <span class="post-date">Jan 23, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/4/index.html b/tags/Technique/page/4/index.html
index 60e892de..9da30b93 100644
--- a/tags/Technique/page/4/index.html
+++ b/tags/Technique/page/4/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/01/23/G-Machine/">
+
+ G-Machine
+
+ </a>
+ </div>
+
+ <span class="post-date">Jan 23, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/01/22/Rescript-React-Native/">
Rescript and React Native
@@ -334,24 +352,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/06/28/%E9%9A%90%E8%97%8F%E4%B8%80%E4%BA%9BOCaml-Effect%E7%9A%84%E6%9C%BA%E5%88%B6%EF%BC%8C%E8%AE%A9%E5%85%B6%E8%AF%AD%E6%B3%95%E5%9C%A8%E7%B2%BE%E7%A5%9E%E4%B8%8A%E6%9B%B4%E6%8E%A5%E8%BF%91delimcc/">
-
- 隐藏一些OCaml Effect的机制,让其语法在精神上更接近delimcc
-
- </a>
- </div>
-
- <span class="post-date">Jun 28, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/5/index.html b/tags/Technique/page/5/index.html
index 4208b1f9..2b953c43 100644
--- a/tags/Technique/page/5/index.html
+++ b/tags/Technique/page/5/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/06/28/%E9%9A%90%E8%97%8F%E4%B8%80%E4%BA%9BOCaml-Effect%E7%9A%84%E6%9C%BA%E5%88%B6%EF%BC%8C%E8%AE%A9%E5%85%B6%E8%AF%AD%E6%B3%95%E5%9C%A8%E7%B2%BE%E7%A5%9E%E4%B8%8A%E6%9B%B4%E6%8E%A5%E8%BF%91delimcc/">
+
+ 隐藏一些OCaml Effect的机制,让其语法在精神上更接近delimcc
+
+ </a>
+ </div>
+
+ <span class="post-date">Jun 28, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/06/28/caml-release-runtime-system%E5%AF%BC%E8%87%B4%E7%9A%84%E6%AD%BB%E9%94%81%E9%97%AE%E9%A2%98/">
caml_release_runtime_system导致的死锁问题