diff options
| author | muqiuhan <[email protected]> | 2025-09-09 06:17:00 +0000 |
|---|---|---|
| committer | muqiuhan <[email protected]> | 2025-09-09 06:17:00 +0000 |
| commit | d5de65fdb1802cdf498d65d93397f290813c377c (patch) | |
| tree | 92fe61a76d20d1203203665d4b48e7f151f088bd /2025 | |
| parent | 48efa2dfde7c263f84ee5bb0872747034908d607 (diff) | |
| download | blog-d5de65fdb1802cdf498d65d93397f290813c377c.tar.gz | |
deploy: 9665097f0fa0dae9f92123fac54d26c0818758a5
Diffstat (limited to '2025')
27 files changed, 411 insertions, 265 deletions
diff --git a/2025/02/10/实时搜索中的防抖函数/index.html b/2025/02/10/实时搜索中的防抖函数/index.html index 4f9ecd38..2390de3e 100644 --- a/2025/02/10/实时搜索中的防抖函数/index.html +++ b/2025/02/10/实时搜索中的防抖函数/index.html @@ -193,18 +193,20 @@ </div> <div class="post-content"> <p>在实现实时搜索功能时,通常会使用输入框的事件监听器来捕获用户的输入变化,并在输入变化时发送搜索请求。为了避免过多的请求导致服务器负担过重,通常会使用“防抖”(debounce)技术来控制请求的频率。</p> -<h3 id="实现步骤"><a href="#实现步骤" class="headerlink" title="实现步骤"></a>实现步骤</h3><ol> +<h3 id="实现步骤"><a class="header-anchor" href="#实现步骤">¶</a>实现步骤</h3> +<ol> <li>监听输入框的变化:使用<code>input</code>事件监听器来捕获用户的输入变化。</li> <li>防抖处理:使用防抖函数来限制请求的频率。防抖函数会在用户停止输入一段时间后才发送请求。</li> <li>发送请求:在防抖函数中调用搜索请求。</li> </ol> -<h3 id="防抖函数示例"><a href="#防抖函数示例" class="headerlink" title="防抖函数示例"></a>防抖函数示例</h3><p>以下是一个简单的防抖函数示例:</p> +<h3 id="防抖函数示例"><a class="header-anchor" href="#防抖函数示例">¶</a>防抖函数示例</h3> +<p>以下是一个简单的防抖函数示例:</p> <figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">debounce</span>(<span class="params">func, wait</span>) {</span><br><span class="line"> <span class="keyword">let</span> timeout;</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">function</span>(<span class="params">...args</span>) {</span><br><span class="line"> <span class="keyword">const</span> context = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="built_in">clearTimeout</span>(timeout);</span><br><span class="line"> timeout = <span class="built_in">setTimeout</span>(<span class="function">() =></span> func.<span class="title function_">apply</span>(context, args), wait);</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - -<h3 id="实现实时搜索"><a href="#实现实时搜索" class="headerlink" title="实现实时搜索"></a>实现实时搜索</h3><p>假设你有一个输入框用于搜索患者:</p> +<h3 id="实现实时搜索"><a class="header-anchor" href="#实现实时搜索">¶</a>实现实时搜索</h3> +<p>假设你有一个输入框用于搜索患者:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><script></span><br><span class="line"> import { onMount } from 'svelte';</span><br><span class="line"> import { patientsStore } from '$lib/stores/patients.svelte';</span><br><span class="line"></span><br><span class="line"> let searchTerm = '';</span><br><span class="line"></span><br><span class="line"> // 防抖函数</span><br><span class="line"> function debounce(func, wait) {</span><br><span class="line"> let timeout;</span><br><span class="line"> return function(...args) {</span><br><span class="line"> const context = this;</span><br><span class="line"> clearTimeout(timeout);</span><br><span class="line"> timeout = setTimeout(() => func.apply(context, args), wait);</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // 搜索函数</span><br><span class="line"> const searchPatients = debounce(async (term) => {</span><br><span class="line"> if (term) {</span><br><span class="line"> // 发送搜索请求</span><br><span class="line"> const response = await fetch(`/api/search-patients?query=${term}`);</span><br><span class="line"> const data = await response.json();</span><br><span class="line"> patientsStore.mbglPatients = data;</span><br><span class="line"> } else {</span><br><span class="line"> // 清空搜索结果或恢复默认数据</span><br><span class="line"> patientsStore.mbglPatients = [];</span><br><span class="line"> }</span><br><span class="line"> }, 300); // 300ms 的防抖时间</span><br><span class="line"></span><br><span class="line"> // 监听输入框变化</span><br><span class="line"> function handleInput(event) {</span><br><span class="line"> searchTerm = event.target.value;</span><br><span class="line"> searchPatients(searchTerm);</span><br><span class="line"> }</span><br><span class="line"></script></span><br><span class="line"></span><br><span class="line"><input type="text" placeholder="搜索患者..." on:input={handleInput} bind:value={searchTerm} /></span><br></pre></td></tr></table></figure> - -<h3 id="请求发送间隔"><a href="#请求发送间隔" class="headerlink" title="请求发送间隔"></a>请求发送间隔</h3><ul> +<h3 id="请求发送间隔"><a class="header-anchor" href="#请求发送间隔">¶</a>请求发送间隔</h3> +<ul> <li>防抖时间:通常设置为 300ms 到 500ms 之间。这个时间足够让用户完成输入并减少不必要的请求。</li> <li>考虑用户体验:防抖时间过短可能导致过多请求,过长则可能让用户感到延迟。300ms 是一个常用的折中值。</li> </ul> diff --git a/2025/02/11/从事件风暴看领域驱动设计/index.html b/2025/02/11/从事件风暴看领域驱动设计/index.html index 7afb6836..3ef557ea 100644 --- a/2025/02/11/从事件风暴看领域驱动设计/index.html +++ b/2025/02/11/从事件风暴看领域驱动设计/index.html @@ -194,7 +194,25 @@ <div class="post-content"> <p>事件风暴(Event Storming)是一种领域驱动设计(DDD)的实践方法,由 Alberto Brandolini 提出,旨在通过团队协作的方式快速理解和建模业务领域。</p> <p>以下是事件风暴及相关领域驱动设计中的一些核心概念和知识点:</p> -<p>1. 领域(Domain):指的是业务相关知识的集合,可以进一步划分为子域。<br>2. 子域(Subdomain):是领域的一部分,可以是核心域、支撑域或通用域。<br>3. 核心域(Core Domain):指领域中最核心的部分,通常对应企业的核心业务。<br>4. 通用语言(Ubiquitous Language):团队所有成员使用的一种语言,用于确保业务和软件之间的沟通一致性。<br>5. 限界上下文(Bounded Context):定义了一组规则和协议,用于明确领域模型的适用范围。<br>6. 实体(Entity):具有唯一标识和生命周期的领域对象。<br>7. 值对象(Value Object):描述了某种特性或属性的对象,没有概念标识。<br>8. 聚合(Aggregate):一组相关对象的集合,由一个聚合根(Aggregate Root)统一管理。<br>9. 领域事件(Domain Event):领域中发生的重要事件,可以用于通知其他领域对象或跨限界上下文进行解耦和协作。<br>10. 命令(Command):表示要执行的操作,通常与事件一一对应。<br>11. 读模型(Read Model):为了优化读取操作而设计的模型,可能与写模型不同。<br>12. 决策命令(Decision Command):在事件风暴中,直接导致事件发生的命令。<br>13. 战略设计(Strategic Design):高层次的抽象和归类,包括理清上下文和子域的划分。<br>14. 战术设计(Tactical Design):对特定上下文下的模型进行详细设计,包括聚合、实体和值对象。<br>15. 贫血模型(Anemic Domain Model):领域对象只有属性及其getter/setter方法的纯数据类,业务逻辑通过服务实现。<br>16. 充血模型(Rich Domain Model):领域对象包含业务逻辑,每个对象都是活跃的。<br>17. 资源库(Repository):用于检索和持久化领域对象的机制。<br>18. 服务(Service):在模型中独立的操作,可以是领域服务或应用服务。<br>19. 固定规则(Invariant):为设计元素做出的断言,必须一直保持为真。</p> +<p>1. 领域(Domain):指的是业务相关知识的集合,可以进一步划分为子域。<br> +2. 子域(Subdomain):是领域的一部分,可以是核心域、支撑域或通用域。<br> +3. 核心域(Core Domain):指领域中最核心的部分,通常对应企业的核心业务。<br> +4. 通用语言(Ubiquitous Language):团队所有成员使用的一种语言,用于确保业务和软件之间的沟通一致性。<br> +5. 限界上下文(Bounded Context):定义了一组规则和协议,用于明确领域模型的适用范围。<br> +6. 实体(Entity):具有唯一标识和生命周期的领域对象。<br> +7. 值对象(Value Object):描述了某种特性或属性的对象,没有概念标识。<br> +8. 聚合(Aggregate):一组相关对象的集合,由一个聚合根(Aggregate Root)统一管理。<br> +9. 领域事件(Domain Event):领域中发生的重要事件,可以用于通知其他领域对象或跨限界上下文进行解耦和协作。<br> +10. 命令(Command):表示要执行的操作,通常与事件一一对应。<br> +11. 读模型(Read Model):为了优化读取操作而设计的模型,可能与写模型不同。<br> +12. 决策命令(Decision Command):在事件风暴中,直接导致事件发生的命令。<br> +13. 战略设计(Strategic Design):高层次的抽象和归类,包括理清上下文和子域的划分。<br> +14. 战术设计(Tactical Design):对特定上下文下的模型进行详细设计,包括聚合、实体和值对象。<br> +15. 贫血模型(Anemic Domain Model):领域对象只有属性及其getter/setter方法的纯数据类,业务逻辑通过服务实现。<br> +16. 充血模型(Rich Domain Model):领域对象包含业务逻辑,每个对象都是活跃的。<br> +17. 资源库(Repository):用于检索和持久化领域对象的机制。<br> +18. 服务(Service):在模型中独立的操作,可以是领域服务或应用服务。<br> +19. 固定规则(Invariant):为设计元素做出的断言,必须一直保持为真。</p> <p>事件风暴通常包括以下步骤:</p> <ul> <li>识别领域事件</li> diff --git a/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html b/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html index 0eac083b..0916a06e 100644 --- a/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html +++ b/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html @@ -195,8 +195,8 @@ <blockquote> <p>A relation field can also reference its own model, in this case the relation is called a <em>self-relation</em>. Self-relations can be of any cardinality, 1-1, 1-n and m-n.</p> </blockquote> -<h2 id="一对一"><a href="#一对一" class="headerlink" title="一对一"></a>一对一</h2><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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> successorId Int? @unique</span><br><span class="line"> successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])</span><br><span class="line"> predecessor User? @relation("BlogOwnerHistory")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - +<h2 id="一对一"><a class="header-anchor" href="#一对一">¶</a>一对一</h2> +<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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> successorId Int? @unique</span><br><span class="line"> successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])</span><br><span class="line"> predecessor User? @relation("BlogOwnerHistory")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> <p>User 展现了这样一个模型:</p> <ul> <li>User 可以有一个或零个前驱(predecessor)</li> @@ -214,9 +214,8 @@ </blockquote> <p>而在关系型数据库中,一对一的 self-relation 可以用如下 SQL 描述:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> "User" (</span><br><span class="line"> id SERIAL <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line"> "name" TEXT,</span><br><span class="line"> "successorId" <span class="type">INTEGER</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">ALTER TABLE</span> "User" <span class="keyword">ADD CONSTRAINT</span> fk_successor_user <span class="keyword">FOREIGN KEY</span> ("successorId") <span class="keyword">REFERENCES</span> "User" (id);</span><br><span class="line"></span><br><span class="line"><span class="keyword">ALTER TABLE</span> "User" <span class="keyword">ADD CONSTRAINT</span> successor_unique <span class="keyword">UNIQUE</span> ("successorId");</span><br></pre></td></tr></table></figure> - -<h2 id="一对多"><a href="#一对多" class="headerlink" title="一对多"></a>一对多</h2><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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> teacherId Int?</span><br><span class="line"> teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])</span><br><span class="line"> students User[] @relation("TeacherStudents")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - +<h2 id="一对多"><a class="header-anchor" href="#一对多">¶</a>一对多</h2> +<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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> teacherId Int?</span><br><span class="line"> teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])</span><br><span class="line"> students User[] @relation("TeacherStudents")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> <p>User 展现了这样一个模型:</p> <ul> <li>一个 User 只能有零个或一个 teacher</li> @@ -227,27 +226,26 @@ </blockquote> <p>用 SQL 描述 User model:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> "User" (</span><br><span class="line"> id SERIAL <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line"> "name" TEXT,</span><br><span class="line"> "teacherId" <span class="type">INTEGER</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">ALTER TABLE</span> "User" <span class="keyword">ADD CONSTRAINT</span> fk_teacherid_user <span class="keyword">FOREIGN KEY</span> ("teacherId") <span class="keyword">REFERENCES</span> "User" (id);</span><br></pre></td></tr></table></figure> - <p><code>teacherId</code> 没有使用 <code>UNIQUE</code> 约束,这代表着多个 students 可以有同一个 teacher</p> -<h2 id="多对多"><a href="#多对多" class="headerlink" title="多对多"></a>多对多</h2><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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> followedBy User[] @relation("UserFollows")</span><br><span class="line"> following User[] @relation("UserFollows")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - +<h2 id="多对多"><a class="header-anchor" href="#多对多">¶</a>多对多</h2> +<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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> followedBy User[] @relation("UserFollows")</span><br><span class="line"> following User[] @relation("UserFollows")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> <ul> <li>一个 User 可以被零个或多个 Users 关注</li> <li>一个 User 可以关注零个或多个 Users</li> </ul> <blockquote> -<p>对于关系型数据库,多对多的关系是隐式的,这意味着 Prisma ORM 会在底层数据库中维护一个 <a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#relation-tables">relation table</a>:<br>A relation table (also sometimes called a <em>JOIN</em>, <em>link</em> or <em>pivot</em> table) connects two or more other tables and therefore creates a <em>relation</em> between them. Creating relation tables is a common data modelling practice in SQL to represent relationships between different entities. In essence it means that “one m-n relation is modeled as two 1-n relations in the database”.</p> +<p>对于关系型数据库,多对多的关系是隐式的,这意味着 Prisma ORM 会在底层数据库中维护一个 <a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#relation-tables">relation table</a>:<br> +A relation table (also sometimes called a <em>JOIN</em>, <em>link</em> or <em>pivot</em> table) connects two or more other tables and therefore creates a <em>relation</em> between them. Creating relation tables is a common data modelling practice in SQL to represent relationships between different entities. In essence it means that “one m-n relation is modeled as two 1-n relations in the database”.</p> <p>We recommend using <a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#implicit-many-to-many-relations">implicit</a> m-n-relations, where Prisma ORM automatically generates the relation table in the underlying database. <a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#explicit-many-to-many-relations">Explicit</a> m-n-relations should be used when you need to store additional data in the relations, such as the date the relation was created.</p> </blockquote> <p>如果需要需要通过多对多的关系来保存其他字段,也可以创建<a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#explicit-many-to-many-relations">显式</a>的多对多 self 关系:</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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> followedBy Follows[] @relation("followedBy")</span><br><span class="line"> following Follows[] @relation("following")</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">model Follows {</span><br><span class="line"> followedBy User @relation("followedBy", fields: [followedById], references: [id])</span><br><span class="line"> followedById Int</span><br><span class="line"> following User @relation("following", fields: [followingId], references: [id])</span><br><span class="line"> followingId Int</span><br><span class="line"></span><br><span class="line"> @@id([followingId, followedById])</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>在关系型数据库中,可以用如下 SQL 描述:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> "User" (</span><br><span class="line"> id <span class="type">integer</span> <span class="keyword">DEFAULT</span> nextval(<span class="string">'"User_id_seq"'</span>::regclass) <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line"> name text</span><br><span class="line">);</span><br><span class="line"><span class="keyword">CREATE TABLE</span> "_UserFollows" (</span><br><span class="line"> "A" <span class="type">integer</span> <span class="keyword">NOT NULL</span> <span class="keyword">REFERENCES</span> "User"(id) <span class="keyword">ON</span> <span class="keyword">DELETE</span> CASCADE <span class="keyword">ON</span> <span class="keyword">UPDATE</span> CASCADE,</span><br><span class="line"> "B" <span class="type">integer</span> <span class="keyword">NOT NULL</span> <span class="keyword">REFERENCES</span> "User"(id) <span class="keyword">ON</span> <span class="keyword">DELETE</span> CASCADE <span class="keyword">ON</span> <span class="keyword">UPDATE</span> CASCADE</span><br><span class="line">);</span><br></pre></td></tr></table></figure> - -<h2 id="在同一模型上建立多个-self-relations"><a href="#在同一模型上建立多个-self-relations" class="headerlink" title="在同一模型上建立多个 self-relations"></a>在同一模型上建立多个 self-relations</h2><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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> teacherId Int?</span><br><span class="line"> teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])</span><br><span class="line"> students User[] @relation("TeacherStudents")</span><br><span class="line"> followedBy User[] @relation("UserFollows")</span><br><span class="line"> following User[] @relation("UserFollows")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - -<h2 id="REFS"><a href="#REFS" class="headerlink" title="REFS."></a>REFS.</h2><ul> +<h2 id="在同一模型上建立多个-self-relations"><a class="header-anchor" href="#在同一模型上建立多个-self-relations">¶</a>在同一模型上建立多个 self-relations</h2> +<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></pre></td><td class="code"><pre><span class="line">model User {</span><br><span class="line"> id Int @id @default(autoincrement())</span><br><span class="line"> name String?</span><br><span class="line"> teacherId Int?</span><br><span class="line"> teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])</span><br><span class="line"> students User[] @relation("TeacherStudents")</span><br><span class="line"> followedBy User[] @relation("UserFollows")</span><br><span class="line"> following User[] @relation("UserFollows")</span><br><span class="line">}</span><br></pre></td></tr></table></figure> +<h2 id="REFS"><a class="header-anchor" href="#REFS">¶</a>REFS.</h2> +<ul> <li><a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations#relation-fields">fully annotated</a></li> <li><a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/models#optional-and-mandatory-fields">required</a></li> <li><a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#implicit-many-to-many-relations">implicit</a></li> diff --git a/2025/02/19/Repository-pattern-in-Typescript/index.html b/2025/02/19/Repository-pattern-in-Typescript/index.html index c0753a94..99250448 100644 --- a/2025/02/19/Repository-pattern-in-Typescript/index.html +++ b/2025/02/19/Repository-pattern-in-Typescript/index.html @@ -198,30 +198,28 @@ <p>抽象化数据访问层的主要作用是将应用的业务逻辑与对数据库的数据访问等实现细节进行解耦。DB 框架的更改不应该影响到应用程序的核心服务,并且它们应该对业务逻辑代码透明。</p> <p>业务服务应该只依赖于抽象,而不是实现,数据服务同理。</p> <hr> -<h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro."></a>Intro.</h1><p>例如,此时有一个书籍存储的微服务,并且有一个“添加新书籍”的操作用例,在此用例中可以通过使用 <code>Book</code> 这个 <em>Repository</em> 来添加新书籍,而 <code>Book</code> 具体使用什么数据库,怎么存并不是业务需要关心的事情。</p> -<h2 id="具体实现方式"><a href="#具体实现方式" class="headerlink" title="具体实现方式"></a>具体实现方式</h2><p>假设有以下实体类型定义:</p> +<h1>Intro.</h1> +<p>例如,此时有一个书籍存储的微服务,并且有一个“添加新书籍”的操作用例,在此用例中可以通过使用 <code>Book</code> 这个 <em>Repository</em> 来添加新书籍,而 <code>Book</code> 具体使用什么数据库,怎么存并不是业务需要关心的事情。</p> +<h2 id="具体实现方式"><a class="header-anchor" href="#具体实现方式">¶</a>具体实现方式</h2> +<p>假设有以下实体类型定义:</p> <figure class="highlight typescript"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">Author</span> {</span><br><span class="line"> <span class="attr">firstName</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">lastName</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">Genre</span> {</span><br><span class="line"> <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">Book</span> {</span><br><span class="line"> <span class="attr">title</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">author</span>: <span class="title class_">Author</span>;</span><br><span class="line"> <span class="attr">genre</span>: <span class="title class_">Genre</span>;</span><br><span class="line"> <span class="attr">publishDate</span>: <span class="title class_">Date</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - -<h3 id="抽象化"><a href="#抽象化" class="headerlink" title="抽象化"></a>抽象化</h3><p>第一步是抽象化出一个通用的 <em>Repository</em> ,其中包含通用的操作:</p> +<h3 id="抽象化"><a class="header-anchor" href="#抽象化">¶</a>抽象化</h3> +<p>第一步是抽象化出一个通用的 <em>Repository</em> ,其中包含通用的操作:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">IGenericRepository</span><T> {</span><br><span class="line"> <span class="keyword">abstract</span> <span class="title function_">getAll</span>(): <span class="title class_">Promise</span><T[]>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">abstract</span> <span class="title function_">get</span>(<span class="attr">id</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span><T>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">abstract</span> <span class="title function_">create</span>(<span class="attr">item</span>: T): <span class="title class_">Promise</span><T>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">abstract</span> <span class="title function_">update</span>(<span class="attr">id</span>: <span class="built_in">string</span>, <span class="attr">item</span>: T);</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <ul> <li>这里具体有那些函数可以根据需要(业务)自己定义。</li> <li>类型参数 <code>T</code> 表示每个实体。</li> </ul> -<p>接着,定义一个数据服务,其中包含使用 <code>IGenericRepositoiry</code> 定义的具体实例对应的 _Repository_:</p> +<p>接着,定义一个数据服务,其中包含使用 <code>IGenericRepositoiry</code> 定义的具体实例对应的 <em>Repository</em>:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Author</span>, <span class="title class_">Book</span>, <span class="title class_">Genre</span> } <span class="keyword">from</span> <span class="string">'../entities'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">IGenericRepository</span> } <span class="keyword">from</span> <span class="string">'./generic-repository.abstract'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">IDataServices</span> {</span><br><span class="line"> <span class="keyword">abstract</span> <span class="attr">authors</span>: <span class="title class_">IGenericRepository</span><<span class="title class_">Author</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">abstract</span> <span class="attr">books</span>: <span class="title class_">IGenericRepository</span><<span class="title class_">Book</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">abstract</span> <span class="attr">genres</span>: <span class="title class_">IGenericRepository</span><<span class="title class_">Genre</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <ul> <li>这里有三个 <em>Repository</em> 分别对应不同的实体。</li> <li><code>IGenericRepository</code> 中定义的函数是每个 <em>Repository</em> 公开的通用存储函数。</li> </ul> <p>以上这些就是对业务中的实体的 <em>Repository</em> 抽象,这样就隔离开了业务和存储逻辑,例如使用 MongoDB,可以实现一个 <code>MongoGenericRepository</code>:</p> <figure class="highlight typescript"><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"><span class="keyword">import</span> { <span class="title class_">Model</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">IGenericRepository</span> } <span class="keyword">from</span> <span class="string">'../../../core'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">MongoGenericRepository</span><T> <span class="keyword">implements</span> <span class="title class_">IGenericRepository</span><T> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">_repository</span>: <span class="title class_">Model</span><T>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">_populateOnFind</span>: <span class="built_in">string</span>[];</span><br><span class="line"></span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="attr">repository</span>: <span class="title class_">Model</span><T>, <span class="attr">populateOnFind</span>: <span class="built_in">string</span>[] = []</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_repository</span> = repository;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_populateOnFind</span> = populateOnFind;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">getAll</span>(): <span class="title class_">Promise</span><T[]> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">_repository</span>.<span class="title function_">find</span>().<span class="title function_">populate</span>(<span class="variable language_">this</span>.<span class="property">_populateOnFind</span>).<span class="title function_">exec</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">get</span>(<span class="attr">id</span>: <span class="built_in">any</span>): <span class="title class_">Promise</span><T> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">_repository</span>.<span class="title function_">findById</span>(id).<span class="title function_">populate</span>(<span class="variable language_">this</span>.<span class="property">_populateOnFind</span>).<span class="title function_">exec</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">create</span>(<span class="attr">item</span>: T): <span class="title class_">Promise</span><T> {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">_repository</span>.<span class="title function_">create</span>(item);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">update</span>(<span class="params"><span class="attr">id</span>: <span class="built_in">string</span>, <span class="attr">item</span>: T</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">_repository</span>.<span class="title function_">findByIdAndUpdate</span>(id, item);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>然后实现一个 <code>MongoDataServices</code>:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Injectable</span>, <span class="title class_">OnApplicationBootstrap</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">InjectModel</span> } <span class="keyword">from</span> <span class="string">'@nestjs/mongoose'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Model</span> } <span class="keyword">from</span> <span class="string">'mongoose'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">IDataServices</span> } <span class="keyword">from</span> <span class="string">'../../../core'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">MongoGenericRepository</span> } <span class="keyword">from</span> <span class="string">'./mongo-generic-repository'</span>;</span><br><span class="line"><span class="keyword">import</span> {</span><br><span class="line"> <span class="title class_">Author</span>,</span><br><span class="line"> <span class="title class_">AuthorDocument</span>,</span><br><span class="line"> <span class="title class_">Book</span>,</span><br><span class="line"> <span class="title class_">BookDocument</span>,</span><br><span class="line"> <span class="title class_">Genre</span>,</span><br><span class="line"> <span class="title class_">GenreDocument</span>,</span><br><span class="line">} <span class="keyword">from</span> <span class="string">'./model'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">MongoDataServices</span></span><br><span class="line"> <span class="keyword">implements</span> <span class="title class_">IDataServices</span>, <span class="title class_">OnApplicationBootstrap</span></span><br><span class="line">{</span><br><span class="line"> <span class="attr">authors</span>: <span class="title class_">MongoGenericRepository</span><<span class="title class_">Author</span>>;</span><br><span class="line"> <span class="attr">books</span>: <span class="title class_">MongoGenericRepository</span><<span class="title class_">Book</span>>;</span><br><span class="line"> <span class="attr">genres</span>: <span class="title class_">MongoGenericRepository</span><<span class="title class_">Genre</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="meta">@InjectModel</span>(Author.name)</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="title class_">AuthorRepository</span>: <span class="title class_">Model</span><<span class="title class_">AuthorDocument</span>>,</span></span><br><span class="line"><span class="params"> <span class="meta">@InjectModel</span>(Book.name)</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="title class_">BookRepository</span>: <span class="title class_">Model</span><<span class="title class_">BookDocument</span>>,</span></span><br><span class="line"><span class="params"> <span class="meta">@InjectModel</span>(Genre.name)</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="title class_">GenreRepository</span>: <span class="title class_">Model</span><<span class="title class_">GenreDocument</span>>,</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="title function_">onApplicationBootstrap</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">authors</span> = <span class="keyword">new</span> <span class="title class_">MongoGenericRepository</span><<span class="title class_">Author</span>>(<span class="variable language_">this</span>.<span class="property">AuthorRepository</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">books</span> = <span class="keyword">new</span> <span class="title class_">MongoGenericRepository</span><<span class="title class_">Book</span>>(<span class="variable language_">this</span>.<span class="property">BookRepository</span>, [</span><br><span class="line"> <span class="string">'author'</span>,</span><br><span class="line"> <span class="string">'genre'</span>,</span><br><span class="line"> ]);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">genres</span> = <span class="keyword">new</span> <span class="title class_">MongoGenericRepository</span><<span class="title class_">Genre</span>>(<span class="variable language_">this</span>.<span class="property">GenreRepository</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <hr> <p>Sunday, March 30, 2025 7:58 PM:</p> <p>感觉以上全错,不应该过度抽象 Repository 的,就让它分散在各个模块中,还可以参考 <a target="_blank" rel="noopener" href="https://github.com/Papooch/nestjs-cls">https://github.com/Papooch/nestjs-cls</a> 实现不会抽象泄漏的事务性 Repository 方法。</p> diff --git a/2025/03/13/领域驱动设计中的“聚合根”/index.html b/2025/03/13/领域驱动设计中的“聚合根”/index.html index 35e8670e..4f96ab6d 100644 --- a/2025/03/13/领域驱动设计中的“聚合根”/index.html +++ b/2025/03/13/领域驱动设计中的“聚合根”/index.html @@ -207,7 +207,6 @@ </ul> <p>用 F# 来描述,以订单管理为例,大概写一下:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">OrderStatus</span> <span class="operator">=</span> </span><br><span class="line"> <span class="operator">|</span> New</span><br><span class="line"> <span class="operator">|</span> Shipped</span><br><span class="line"> <span class="operator">|</span> Delivered</span><br><span class="line"> <span class="operator">|</span> Cancelled</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">OrderItem</span> (productName<span class="operator">:</span> <span class="type">string</span>, price<span class="operator">:</span> <span class="type">float</span>, quantity<span class="operator">:</span> <span class="type">int</span>) <span class="operator">=</span> </span><br><span class="line"> <span class="keyword">do</span></span><br><span class="line"> <span class="keyword">if</span> quantity <span class="operator"><=</span> <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">failwith</span> <span class="string">"Quantity must be positive"</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.ProductName <span class="operator">=</span> productName</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Price <span class="operator">=</span> price</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Quantity <span class="operator">=</span> quantity</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.TotalPrice () <span class="operator">=</span> price <span class="operator">*</span> quantity</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Order</span> (<span class="built_in">id</span><span class="operator">:</span> <span class="type">int</span>, customerName<span class="operator">:</span> <span class="type">string</span>) <span class="operator">=</span> </span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mutable</span> status <span class="operator">=</span> OrderStatus.New</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mutable</span> orderItems <span class="operator">=</span> []</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Id <span class="operator">=</span> <span class="built_in">id</span></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.CustomerName <span class="operator">=</span> customerName</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Status <span class="operator">=</span> status</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.OrderItems <span class="operator">=</span> orderItems</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.AddItem (item<span class="operator">:</span> OrderItem, price<span class="operator">:</span> <span class="type">float</span>, quantity<span class="operator">:</span> <span class="type">int</span>) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">if</span> quantity <span class="operator"><=</span> <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">failwith</span> <span class="string">"Quantity must be positive"</span></span><br><span class="line"> </span><br><span class="line"> orderItems <span class="operator"><-</span> orderItems <span class="operator">@</span> [OrderItem(item.ProductName, price, quantity)]</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.ChangeStatus (status<span class="operator">:</span> OrderStatus) <span class="operator">=</span></span><br><span class="line"> this.Status <span class="operator"><-</span> status</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.TotalPrice () <span class="operator">=</span></span><br><span class="line"> orderItems <span class="operator">|></span> List.sumBy (<span class="keyword">fun</span> item <span class="operator">-></span> item.TotalPrice())</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.GetTotalPrice () <span class="operator">=</span></span><br><span class="line"> orderItems <span class="operator">|></span> List.sumBy (<span class="keyword">fun</span> item <span class="operator">-></span> item.TotalPrice())</span><br></pre></td></tr></table></figure> - <p>在这个例子中,<code>Order</code> 是聚合根,它通过 <code>AddItem</code> 方法来添加订单项,保证每个订单项符合业务规则。同时,聚合根 <code>Order</code> 还负责订单状态的管理,例如通过 <code>ChangeStatus</code> 方法来更新订单状态。<code>OrderItem</code> 是聚合内的一个实体,表示订单项,它通过 <code>GetTotalPrice</code> 方法来计算每个订单项的总价。外部系统只能通过 <code>Order</code> 聚合根来访问和操作订单项,而不能直接访问或修改 <code>OrderItem</code></p> </div> diff --git a/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html b/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html index 3f99a42a..d72575f9 100644 --- a/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html +++ b/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html @@ -209,7 +209,6 @@ </ol> <p>假设正在开发一个电子商务系统,业务专家使用“订单”来描述用户购买的商品集合。团队可以在代码中使用“Order”来命名相关的类和方法:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Order</span> <span class="operator">=</span> {</span><br><span class="line"> items<span class="operator">:</span> OrderItem <span class="type">list</span></span><br><span class="line"> customer<span class="operator">:</span> Customer</span><br><span class="line"> orderDate<span class="operator">:</span> DateTime</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">OrderItem</span> <span class="operator">=</span> {</span><br><span class="line"> product<span class="operator">:</span> Product</span><br><span class="line"> quantity<span class="operator">:</span> <span class="type">int</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Customer</span> <span class="operator">=</span> {</span><br><span class="line"> name<span class="operator">:</span> <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Product</span> <span class="operator">=</span> {</span><br><span class="line"> name<span class="operator">:</span> <span class="type">string</span></span><br><span class="line"> price<span class="operator">:</span> <span class="type">float</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> addItem (order<span class="operator">:</span> Order) (item<span class="operator">:</span> OrderItem) <span class="operator">=</span></span><br><span class="line"> { order <span class="keyword">with</span> items <span class="operator">=</span> order.items <span class="operator">@</span> [item] }</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> getTotalAmount (order<span class="operator">:</span> Order) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> total <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line"> <span class="keyword">for</span> item <span class="keyword">in</span> order.items <span class="keyword">do</span></span><br><span class="line"> total <span class="operator"><-</span> total <span class="operator">+</span> item.price</span><br><span class="line"> total</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> getPrice (item<span class="operator">:</span> OrderItem) <span class="operator">=</span></span><br><span class="line"> item.product.price <span class="operator">*</span> item.quantity</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> getTotalPrice (order<span class="operator">:</span> Order) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> total <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line"> <span class="keyword">for</span> item <span class="keyword">in</span> order.items <span class="keyword">do</span></span><br><span class="line"> total <span class="operator"><-</span> total <span class="operator">+</span> getPrice(item)</span><br><span class="line"> total</span><br></pre></td></tr></table></figure> - <p>在这个示例中使用了“Order”、“OrderItem”、“Customer”和“Product”等术语,这些术语都是 Ubiquitous Language 的一部分,可以提高代码与业务需求的一致性。</p> </div> diff --git a/2025/03/17/二〇二五年三月十七日/index.html b/2025/03/17/二〇二五年三月十七日/index.html index 4c617dc1..d4d68cd3 100644 --- a/2025/03/17/二〇二五年三月十七日/index.html +++ b/2025/03/17/二〇二五年三月十七日/index.html @@ -301,8 +301,10 @@ mjx-container[display="true"] + br { <p>它们是因缘聚散吗?</p> <p>那露珠消散后去了哪里?</p> <p>此有故彼有,此生故彼灭。</p> -<p>当晨光加热露珠表面至 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.489ex;" xmlns="http://www.w3.org/2000/svg" width="6.242ex" height="1.995ex" role="img" focusable="false" viewBox="0 -666 2759 882"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(778,0)"></path></g><g data-mml-node="mi" transform="translate(1278,0)"><path data-c="1D707" d="M58 -216Q44 -216 34 -208T23 -186Q23 -176 96 116T173 414Q186 442 219 442Q231 441 239 435T249 423T251 413Q251 401 220 279T187 142Q185 131 185 107V99Q185 26 252 26Q261 26 270 27T287 31T302 38T315 45T327 55T338 65T348 77T356 88T365 100L372 110L408 253Q444 395 448 404Q461 431 491 431Q504 431 512 424T523 412T525 402L449 84Q448 79 448 68Q448 43 455 35T476 26Q485 27 496 35Q517 55 537 131Q543 151 547 152Q549 153 557 153H561Q580 153 580 144Q580 138 575 117T555 63T523 13Q510 0 491 -8Q483 -10 467 -10Q446 -10 429 -4T402 11T385 29T376 44T374 51L368 45Q362 39 350 30T324 12T288 -4T246 -11Q199 -11 153 12L129 -85Q108 -167 104 -180T92 -202Q76 -216 58 -216Z"></path></g><g data-mml-node="mi" transform="translate(1881,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container> 深度时,表层水分子动能突破氢键束缚(键能约 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="9.762ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 4315 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(500,0)"></path></g><g data-mml-node="mi" transform="translate(1000,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z"></path></g><g data-mml-node="mi" transform="translate(1521,0)"><path data-c="1D43D" d="M447 625Q447 637 354 637H329Q323 642 323 645T325 664Q329 677 335 683H352Q393 681 498 681Q541 681 568 681T605 682T619 682Q633 682 633 672Q633 670 630 658Q626 642 623 640T604 637Q552 637 545 623Q541 610 483 376Q420 128 419 127Q397 64 333 21T195 -22Q137 -22 97 8T57 88Q57 130 80 152T132 174Q177 174 182 130Q182 98 164 80T123 56Q115 54 115 53T122 44Q148 15 197 15Q235 15 271 47T324 130Q328 142 387 380T447 625Z"></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2154,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z"></path></g></g><g data-mml-node="mi" transform="translate(2654,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(3532,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(4017,0)"><path data-c="1D459" d="M117 59Q117 26 142 26Q179 26 205 131Q211 151 215 152Q217 153 225 153H229Q238 153 241 153T246 151T248 144Q247 138 245 128T234 90T214 43T183 6T137 -11Q101 -11 70 11T38 85Q38 97 39 102L104 360Q167 615 167 623Q167 626 166 628T162 632T157 634T149 635T141 636T132 637T122 637Q112 637 109 637T101 638T95 641T94 647Q94 649 96 661Q101 680 107 682T179 688Q194 689 213 690T243 693T254 694Q266 694 266 686Q266 675 193 386T118 83Q118 81 118 75T117 65V59Z"></path></g></g></g></svg></mjx-container>)。<br>这种相变并非整齐划一的队列解散,而是呈现量子隧穿效应——单个水分子以 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="6.356ex" height="2.005ex" role="img" focusable="false" viewBox="0 -864 2809.2 886"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="TeXAtom" transform="translate(1033,393.1) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mn" transform="translate(778,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mi" transform="translate(2340.2,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g></g></svg></mjx-container> 量级的涨落,在液态与气态间振荡,直到完全脱离范德华力作用半径(约 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="6.235ex" height="1.557ex" role="img" focusable="false" viewBox="0 -666 2756 688"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(778,0)"></path></g><g data-mml-node="mi" transform="translate(1278,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(1878,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container>)。</p> -<p>逃逸的 H2O 分子并非直线升空,而是在空气分子碰撞下进行三维随机游走。<br>根据爱因斯坦-斯托克斯方程,其扩散系数 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="16.19ex" height="2.452ex" role="img" focusable="false" viewBox="0 -833.9 7156.1 1083.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D437" d="M287 628Q287 635 230 637Q207 637 200 638T193 647Q193 655 197 667T204 682Q206 683 403 683Q570 682 590 682T630 676Q702 659 752 597T803 431Q803 275 696 151T444 3L430 1L236 0H125H72Q48 0 41 2T33 11Q33 13 36 25Q40 41 44 43T67 46Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628ZM703 469Q703 507 692 537T666 584T629 613T590 629T555 636Q553 636 541 636T512 636T479 637H436Q392 637 386 627Q384 623 313 339T242 52Q242 48 253 48T330 47Q335 47 349 47T373 46Q499 46 581 128Q617 164 640 212T683 339T703 469Z"></path></g><g data-mml-node="mo" transform="translate(1105.8,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z"></path></g><g data-mml-node="mn" transform="translate(2161.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(778,0)"></path><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" transform="translate(1278,0)"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(1778,0)"></path></g><g data-mml-node="mi" transform="translate(4439.6,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g><g data-mml-node="msup" transform="translate(4872.6,0)"><g data-mml-node="mi"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mn" transform="translate(911,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6187.1,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z"></path></g></g><g data-mml-node="mi" transform="translate(6687.1,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g></g></svg></mjx-container>(25℃标准大气压)。这意味着单个水分子在1秒内将形成半径约 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="6.864ex" height="1.557ex" role="img" focusable="false" viewBox="0 -677 3034 688"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(778,0)"></path></g><g data-mml-node="mi" transform="translate(1278,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(2156,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container> 的概率云,与数十亿同伴共同形成水汽。</p> +<p>当晨光加热露珠表面至 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.489ex;" xmlns="http://www.w3.org/2000/svg" width="6.242ex" height="1.995ex" role="img" focusable="false" viewBox="0 -666 2759 882"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(778,0)"></path></g><g data-mml-node="mi" transform="translate(1278,0)"><path data-c="1D707" d="M58 -216Q44 -216 34 -208T23 -186Q23 -176 96 116T173 414Q186 442 219 442Q231 441 239 435T249 423T251 413Q251 401 220 279T187 142Q185 131 185 107V99Q185 26 252 26Q261 26 270 27T287 31T302 38T315 45T327 55T338 65T348 77T356 88T365 100L372 110L408 253Q444 395 448 404Q461 431 491 431Q504 431 512 424T523 412T525 402L449 84Q448 79 448 68Q448 43 455 35T476 26Q485 27 496 35Q517 55 537 131Q543 151 547 152Q549 153 557 153H561Q580 153 580 144Q580 138 575 117T555 63T523 13Q510 0 491 -8Q483 -10 467 -10Q446 -10 429 -4T402 11T385 29T376 44T374 51L368 45Q362 39 350 30T324 12T288 -4T246 -11Q199 -11 153 12L129 -85Q108 -167 104 -180T92 -202Q76 -216 58 -216Z"></path></g><g data-mml-node="mi" transform="translate(1881,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container> 深度时,表层水分子动能突破氢键束缚(键能约 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="9.762ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 4315 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(500,0)"></path></g><g data-mml-node="mi" transform="translate(1000,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z"></path></g><g data-mml-node="mi" transform="translate(1521,0)"><path data-c="1D43D" d="M447 625Q447 637 354 637H329Q323 642 323 645T325 664Q329 677 335 683H352Q393 681 498 681Q541 681 568 681T605 682T619 682Q633 682 633 672Q633 670 630 658Q626 642 623 640T604 637Q552 637 545 623Q541 610 483 376Q420 128 419 127Q397 64 333 21T195 -22Q137 -22 97 8T57 88Q57 130 80 152T132 174Q177 174 182 130Q182 98 164 80T123 56Q115 54 115 53T122 44Q148 15 197 15Q235 15 271 47T324 130Q328 142 387 380T447 625Z"></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2154,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z"></path></g></g><g data-mml-node="mi" transform="translate(2654,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(3532,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(4017,0)"><path data-c="1D459" d="M117 59Q117 26 142 26Q179 26 205 131Q211 151 215 152Q217 153 225 153H229Q238 153 241 153T246 151T248 144Q247 138 245 128T234 90T214 43T183 6T137 -11Q101 -11 70 11T38 85Q38 97 39 102L104 360Q167 615 167 623Q167 626 166 628T162 632T157 634T149 635T141 636T132 637T122 637Q112 637 109 637T101 638T95 641T94 647Q94 649 96 661Q101 680 107 682T179 688Q194 689 213 690T243 693T254 694Q266 694 266 686Q266 675 193 386T118 83Q118 81 118 75T117 65V59Z"></path></g></g></g></svg></mjx-container>)。<br> +这种相变并非整齐划一的队列解散,而是呈现量子隧穿效应——单个水分子以 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="6.356ex" height="2.005ex" role="img" focusable="false" viewBox="0 -864 2809.2 886"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="TeXAtom" transform="translate(1033,393.1) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mo"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mn" transform="translate(778,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mi" transform="translate(2340.2,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g></g></svg></mjx-container> 量级的涨落,在液态与气态间振荡,直到完全脱离范德华力作用半径(约 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="6.235ex" height="1.557ex" role="img" focusable="false" viewBox="0 -666 2756 688"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(778,0)"></path></g><g data-mml-node="mi" transform="translate(1278,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(1878,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container>)。</p> +<p>逃逸的 H2O 分子并非直线升空,而是在空气分子碰撞下进行三维随机游走。<br> +根据爱因斯坦-斯托克斯方程,其扩散系数 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="16.19ex" height="2.452ex" role="img" focusable="false" viewBox="0 -833.9 7156.1 1083.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D437" d="M287 628Q287 635 230 637Q207 637 200 638T193 647Q193 655 197 667T204 682Q206 683 403 683Q570 682 590 682T630 676Q702 659 752 597T803 431Q803 275 696 151T444 3L430 1L236 0H125H72Q48 0 41 2T33 11Q33 13 36 25Q40 41 44 43T67 46Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628ZM703 469Q703 507 692 537T666 584T629 613T590 629T555 636Q553 636 541 636T512 636T479 637H436Q392 637 386 627Q384 623 313 339T242 52Q242 48 253 48T330 47Q335 47 349 47T373 46Q499 46 581 128Q617 164 640 212T683 339T703 469Z"></path></g><g data-mml-node="mo" transform="translate(1105.8,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z"></path></g><g data-mml-node="mn" transform="translate(2161.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(778,0)"></path><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z" transform="translate(1278,0)"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(1778,0)"></path></g><g data-mml-node="mi" transform="translate(4439.6,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g><g data-mml-node="msup" transform="translate(4872.6,0)"><g data-mml-node="mi"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mn" transform="translate(911,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6187.1,0)"><g data-mml-node="mo"><path data-c="2F" d="M423 750Q432 750 438 744T444 730Q444 725 271 248T92 -240Q85 -250 75 -250Q68 -250 62 -245T56 -231Q56 -221 230 257T407 740Q411 750 423 750Z"></path></g></g><g data-mml-node="mi" transform="translate(6687.1,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g></g></svg></mjx-container>(25℃标准大气压)。这意味着单个水分子在1秒内将形成半径约 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="6.864ex" height="1.557ex" role="img" focusable="false" viewBox="0 -677 3034 688"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(778,0)"></path></g><g data-mml-node="mi" transform="translate(1278,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(2156,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container> 的概率云,与数十亿同伴共同形成水汽。</p> <p>水中捞月,伸手时,涟漪碎了三千世界。</p> <p>人类的眉睫处有十方虚空,三藏经书不过指月之指,</p> <p>看见儿时门前溪水倒流,看见婴孩啼哭时眼底星河闪烁。</p> diff --git a/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html b/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html index b15e30ac..f2210beb 100644 --- a/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html +++ b/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html @@ -194,7 +194,6 @@ <div class="post-content"> <p>使用领域事件的一种直接做法是:在 应用服务 (Application Service) 中产生事件并发布出去。例如,对于“用户昵称更新”的场景来讲,对应的应用服务 <code>UserCommandService</code> 实现如下:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">member</span> <span class="keyword">public</span> this.UpdateMyName (command<span class="operator">:</span> UpdateUsernameCommand) (user<span class="operator">:</span> User) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> user <span class="operator">=</span> userRepository.GetById user.Id</span><br><span class="line"> <span class="keyword">let</span> oldName <span class="operator">=</span> user.Username</span><br><span class="line"> <span class="keyword">let</span> newName <span class="operator">=</span> command.Username</span><br><span class="line"></span><br><span class="line"> user.UpdateUsername newName</span><br><span class="line"> <span class="operator">|></span> userRepository.Save</span><br><span class="line"> </span><br><span class="line"> UsernameChangeEvent (user.Id, newName, oldName) </span><br><span class="line"> <span class="operator">|></span> eventPublisher.Publish</span><br></pre></td></tr></table></figure> - <p>这里,在更新了用户姓名之后,即刻调用事件发布器 <code>eventPublisher.Publish</code> 将事件发送到消息队列中。虽然这种方式比较流行,但它至少存在两个问题:</p> <ol> <li>领域事件本应属于领域模型的一部分,也即应该从领域模型中产生,而这里却在应用服务中产生</li> @@ -202,24 +201,19 @@ </ol> <p>对于第1个问题,可以采用“从领域模型中返回领域事件”的方式:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">member</span> <span class="keyword">public</span> this.UpdateMyName (command<span class="operator">:</span> UpdateUsernameCommand) (user<span class="operator">:</span> User) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> user <span class="operator">=</span> userRepository.GetById user.Id</span><br><span class="line"> <span class="keyword">let</span> oldName <span class="operator">=</span> user.Username</span><br><span class="line"> <span class="keyword">let</span> newName <span class="operator">=</span> command.Username</span><br><span class="line"></span><br><span class="line"> user.UpdateUsername newName <span class="comment">// UpdateUsername 中构建 UsernameChangeEvent</span></span><br><span class="line"> <span class="operator">|></span> <span class="keyword">fun</span> user event <span class="operator">-></span> </span><br><span class="line"> userRepository.Save user</span><br><span class="line"> eventPublisher.Publish event</span><br></pre></td></tr></table></figure> - <p>这种方式保证了领域事件是从领域模型中产生,但仍然存在第二个问题。</p> <p>第二个问题中所谓的“数据一致性”,表示的是将聚合根保存到数据库和将领域事件发布到消息队列之间的一致性。由于数据库和消息队列属于异构的数据源,要保证他们之间的数据一致性需要引入分布式事务。</p> <p>但是分布式事务通常是比较重量级的,再加上当下的诸多常见消息队列均不支持分布式事务(比如Kafka),因此并不建议使用分布式事务来解决这个问题。</p> <p><a target="_blank" rel="noopener" href="https://microservices.io/patterns/data/transactional-outbox.html">Transactional Outbox</a> 便是一种方案,概括来说,这种方式将一个分布式事务的问题拆解为多个本地事务,并采用“至少一次投递(At Least Once Delivery)”原则保证消息的发布。具体来讲,发布方在与业务数据相同的数据库中为领域事件创建相应的事件发布表(Outbox table),然后在保存业务数据的同时将所产生的事件保存到事件发布表中,由于此时二者都属于同一个数据库的本地事务所管辖,因此保证了“业务操作”与“事件产生”之间的一致性。此时的代码变成了:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">member</span> <span class="keyword">public</span> this.UpdateMyName (command<span class="operator">:</span> UpdateUsernameCommand) (user<span class="operator">:</span> User) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> user <span class="operator">=</span> userRepository.GetById user.Id</span><br><span class="line"> <span class="keyword">let</span> oldName <span class="operator">=</span> user.Username</span><br><span class="line"> <span class="keyword">let</span> newName <span class="operator">=</span> command.Username</span><br><span class="line"></span><br><span class="line"> user.UpdateUsername newName <span class="comment">// UpdateUsername 中构建 UsernameChangeEvent</span></span><br><span class="line"> <span class="operator">|></span> <span class="keyword">fun</span> user event <span class="operator">-></span> </span><br><span class="line"> userRepository.Save user</span><br><span class="line"> eventStore.Save event <span class="comment">// 这儿用 eventStore 代替 eventPublisher 啦</span></span><br></pre></td></tr></table></figure> - <p>应用服务不再将事件直接发布出去,而是将事件保存到数据库中,之后,另一个模块将从数据库中读取事件并发布。</p> <p>然而,这种方式依然有个缺点:每个需要产生领域事件的场景都需要应用服务先后调用<code>repository.Save()</code>和<code>eventStore.Save()</code>,导致了代码重复。解决方法也很简单——在聚合根中临时保存领域事件,然后在资源库中同时保存聚合根和领域事件到数据库。</p> <p>在这种方式下,首先需要在聚合根的基类中完成与领域事件相关的各种设施,包括创建临时性的事件容器<code>events</code>以及通用的事件产生方法<code>RaiseEvent()</code>:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">[<AbstractClass>]</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">IAggregateRoot</span> <span class="operator">=</span> </span><br><span class="line"> <span class="operator">...</span></span><br><span class="line"> <span class="keyword">let</span> events <span class="operator">=</span> Collections.Generic.List<span class="operator"><</span>DomainEvent<span class="operator">></span> ()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">private</span> this.RaiseEvent (event<span class="operator">:</span> DomainEvent) <span class="operator">=</span></span><br><span class="line"> events.Add event</span><br><span class="line"> </span><br><span class="line"> <span class="operator">...</span></span><br></pre></td></tr></table></figure> - <p>在聚合根基类<code>AggregateRoot</code>中,<code>events</code>字段用于临时保存聚合根中所产生的所有事件,各实际的聚合根类通过调用<code>RaiseEvent()</code>向<code>events</code>中添加事件。比如,对于“用户修改昵称”而言,<code>User</code>实现如下:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">member</span> <span class="keyword">public</span> this.UpdateUsername (name<span class="operator">:</span> <span class="type">string</span>, user<span class="operator">:</span> User) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">if</span> this.Username <span class="operator">=</span> name <span class="keyword">then</span></span><br><span class="line"> ()</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="keyword">let</span> oldName <span class="operator">=</span> this.Username</span><br><span class="line"> this.Username <span class="operator"><-</span> name</span><br><span class="line"> UsernameChangeEvent (user.Id, name, oldName) </span><br><span class="line"> <span class="operator">|></span> this.RaiseEvent</span><br></pre></td></tr></table></figure> - <p>这里,聚合根 <code>User</code> 不再返回领域事件,而是将领域事件通过<code>AggregateRoot.RaiseEvent()</code>暂时性地保存到自身的<code>events</code>中。之后在保存<code>User</code>时,资源库的公共基类<code>BaseRepository</code>的<code>Save()</code>方法同时完成对聚合根和领域事件的持久化:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">member</span> <span class="keyword">public</span> this.Save<span class="operator"><</span>AR<span class="operator">:</span> <span class="keyword">when</span> AR <span class="operator">:></span> AggrateRoot<span class="operator">></span> (it<span class="operator">:</span> AR) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">match</span> it <span class="keyword">with</span></span><br><span class="line"> <span class="operator">|</span> <span class="literal">null</span> <span class="operator">-></span> <span class="built_in">failwith</span> <span class="string">"..."</span></span><br><span class="line"> <span class="operator">|</span> it <span class="keyword">when</span> it.Events <span class="operator">|></span> isEmpty <span class="operator">|></span> <span class="built_in">not</span> <span class="operator">-></span> </span><br><span class="line"> this.SaveEvents it.Events</span><br><span class="line"> this.CleanEvents ()</span><br><span class="line"> <span class="operator">|</span> _ <span class="operator">-></span> ()</span><br><span class="line"></span><br><span class="line"> db.Save it</span><br></pre></td></tr></table></figure> - <p>在<code>Save()</code>方法中,首先获取到聚合根中的所有领域事件,然后通过<code>SaveEvents()</code>方法将它们保存到发布事件表中,最后通过<code>db.Save it</code>保存聚合根。需要注意的是,在这种方式下,<code>AggregateRoot</code>中的<code>events</code>字段是不能被持久化的,因为需要保证每次从数据库中加载出聚合根时<code>events</code>都是空的,为此在<code>SaveEvents()</code>保存了领域事件后,立即调用<code>it.clearEvents()</code>将所有的领域事件清空掉,以免领域事件随着聚合根一道被持久化到数据库中。</p> <p>到目前为止,对领域事件的处理都还没有涉及到与任何消息中间件相关的内容,也即事件的产生是一个完全独立于消息队列的关注点,此时不用关心领域事件之后将以何种形式发布出去,Kafka 也好,RabbitMQ 也罢。除了关注点分离的好处外,这种解耦也使得系统在有可能切换消息中间件时更加的简单。</p> <p>对于“在应用服务中通过<code>eventPublisher.Publish()</code>直接发布事件”而言,事件的产生和发布是同时完成的;但是对于“在聚合根中临时性保存领域事件”的方式来说,它只解决了事件的产生问题,并未解决事件的发布问题,事件的发布方应该采用“发射后不管(Fire And Forget)”的原则,即发布方无需了解消费方是如何处理领域事件的,甚至都不需要知道事件被哪些消费方所消费。</p> diff --git a/2025/03/20/TDD-和-DDD-的一些小想法/index.html b/2025/03/20/TDD-和-DDD-的一些小想法/index.html index 8c1575ec..e7adc5c0 100644 --- a/2025/03/20/TDD-和-DDD-的一些小想法/index.html +++ b/2025/03/20/TDD-和-DDD-的一些小想法/index.html @@ -193,25 +193,31 @@ </div> <div class="post-content"> <p>Test-Driven Development (TDD) 和 Domain-Driven Design (DDD) 是两种不同的软件开发方法论,各自有其独特的优缺点和应用场景。</p> -<h3 id="Test-Driven-Development-TDD"><a href="#Test-Driven-Development-TDD" class="headerlink" title="Test-Driven Development (TDD)"></a>Test-Driven Development (TDD)</h3><h4 id="优点:"><a href="#优点:" class="headerlink" title="优点:"></a>优点:</h4><ol> +<h3 id="Test-Driven-Development-TDD"><a class="header-anchor" href="#Test-Driven-Development-TDD">¶</a>Test-Driven Development (TDD)</h3> +<h4 id="优点:"><a class="header-anchor" href="#优点:">¶</a>优点:</h4> +<ol> <li>提高代码质量:通过编写测试来驱动开发,可以确保代码的正确性和可靠性。</li> <li>减少缺陷:早期发现和修复缺陷,减少后期的调试和维护成本。</li> <li>促进可维护性:代码更加模块化和可测试,便于后续的维护和扩展。</li> <li>文档化:测试代码本身就是一种文档,描述了系统的行为和期望。</li> <li>提高开发速度:虽然初期可能会慢一些,但长期来看,由于减少了重构和调试的时间,开发速度会提高。</li> </ol> -<h4 id="缺点:"><a href="#缺点:" class="headerlink" title="缺点:"></a>缺点:</h4><ol> +<h4 id="缺点:"><a class="header-anchor" href="#缺点:">¶</a>缺点:</h4> +<ol> <li>初期投入大:需要在开发前编写测试,初期投入的时间和精力较大。</li> <li>学习曲线陡峭:对新手开发者来说,学习和掌握TDD需要一定的时间。</li> <li>过度依赖测试:可能导致过度依赖单元测试,忽略了系统的整体性能和用户体验。</li> </ol> -<h3 id="Domain-Driven-Design-DDD"><a href="#Domain-Driven-Design-DDD" class="headerlink" title="Domain-Driven Design (DDD)"></a>Domain-Driven Design (DDD)</h3><h4 id="优点:-1"><a href="#优点:-1" class="headerlink" title="优点:"></a>优点:</h4><ol> +<h3 id="Domain-Driven-Design-DDD"><a class="header-anchor" href="#Domain-Driven-Design-DDD">¶</a>Domain-Driven Design (DDD)</h3> +<h4 id="优点:-v2"><a class="header-anchor" href="#优点:-v2">¶</a>优点:</h4> +<ol> <li>聚焦领域:通过深入理解业务领域,确保软件系统与业务需求紧密结合。</li> <li>模型驱动:建立一个清晰的领域模型,帮助开发者和业务专家共同理解系统。</li> <li>可维护性:通过明确的领域模型和边界,系统结构更加清晰,便于维护和扩展。</li> <li>沟通桥梁:提供了一种通用的语言(Ubiquitous Language),促进开发团队和业务团队之间的沟通。</li> </ol> -<h4 id="缺点:-1"><a href="#缺点:-1" class="headerlink" title="缺点:"></a>缺点:</h4><ol> +<h4 id="缺点:-v2"><a class="header-anchor" href="#缺点:-v2">¶</a>缺点:</h4> +<ol> <li>复杂性高:DDD方法论较为复杂,需要深入理解领域模型和设计模式。</li> <li>初期投入大:需要大量的时间和精力进行领域分析和建模。</li> <li>适用范围有限:对于简单的系统或小型项目,DDD可能显得过于复杂和冗余。</li> diff --git a/2025/03/20/肺功能检查数值/index.html b/2025/03/20/肺功能检查数值/index.html index eea6b710..5733b0ac 100644 --- a/2025/03/20/肺功能检查数值/index.html +++ b/2025/03/20/肺功能检查数值/index.html @@ -193,41 +193,51 @@ </div> <div class="post-content"> <p>肺功能测试是评估患者呼吸系统健康的重要工具。各个指标的及其异常数值可能指示的潜在生理疾病:</p> -<h3 id="1-FEV1(第一秒用力呼气量)"><a href="#1-FEV1(第一秒用力呼气量)" class="headerlink" title="1. FEV1(第一秒用力呼气量)"></a>1. FEV1(第一秒用力呼气量)</h3><ul> +<h3 id="1-FEV1(第一秒用力呼气量)"><a class="header-anchor" href="#1-FEV1(第一秒用力呼气量)">¶</a>1. FEV1(第一秒用力呼气量)</h3> +<ul> <li>评估:FEV1用于评估气道的通畅程度。它反映了在用力呼气的第一秒内,患者能够排出的气体量。</li> <li>异常:FEV1降低通常提示气道阻塞,常见于慢性阻塞性肺病(COPD)、哮喘、支气管炎等疾病。</li> </ul> -<h3 id="2-FVC(用力肺活量)"><a href="#2-FVC(用力肺活量)" class="headerlink" title="2. FVC(用力肺活量)"></a>2. FVC(用力肺活量)</h3><ul> +<h3 id="2-FVC(用力肺活量)"><a class="header-anchor" href="#2-FVC(用力肺活量)">¶</a>2. FVC(用力肺活量)</h3> +<ul> <li>评估:FVC测量的是患者在一次用力呼气中能够排出的最大气体量,反映了肺的容量和扩张能力。</li> <li>异常:FVC降低可能指示限制性肺病,如肺纤维化、胸廓畸形或神经肌肉疾病等。</li> </ul> -<h3 id="3-FEV1-FVC比值"><a href="#3-FEV1-FVC比值" class="headerlink" title="3. FEV1/FVC比值"></a>3. FEV1/FVC比值</h3><ul> -<li>评估:FEV1/FVC比值用于区分阻塞性和限制性肺病。正常情况下,该比值应大于70%。</li> -<li>异常:<ul> +<h3 id="3-FEV1-FVC比值"><a class="header-anchor" href="#3-FEV1-FVC比值">¶</a>3. FEV1/FVC比值</h3> +<ul> +<li>评估:FEV1/FVC比值用于区分阻塞性和限制性肺病。正常情况下,该比值应大于70%。</li> +<li>异常: +<ul> <li>低于70%:提示气道阻塞,常见于COPD和哮喘。</li> <li>正常或高于70%但FVC降低:可能提示限制性肺病。</li> </ul> </li> </ul> -<h3 id="4-PEF(峰值呼气流量)"><a href="#4-PEF(峰值呼气流量)" class="headerlink" title="4. PEF(峰值呼气流量)"></a>4. PEF(峰值呼气流量)</h3><ul> +<h3 id="4-PEF(峰值呼气流量)"><a class="header-anchor" href="#4-PEF(峰值呼气流量)">¶</a>4. PEF(峰值呼气流量)</h3> +<ul> <li>评估:PEF测量患者在用力呼气时达到的最大流速,常用于监测哮喘患者的病情变化。</li> <li>异常:PEF降低可能提示气道狭窄或阻塞,常见于哮喘急性发作或COPD加重。</li> </ul> -<h3 id="5-MVV(最大通气量)"><a href="#5-MVV(最大通气量)" class="headerlink" title="5. MVV(最大通气量)"></a>5. MVV(最大通气量)</h3><ul> +<h3 id="5-MVV(最大通气量)"><a class="header-anchor" href="#5-MVV(最大通气量)">¶</a>5. MVV(最大通气量)</h3> +<ul> <li>评估:MVV测量在一定时间内(通常是12秒)能够进行的最大通气量,反映了肺部的通气能力和呼吸肌的力量。</li> <li>异常:MVV降低可能与呼吸肌无力、气道阻塞或肺部疾病(如COPD)相关。</li> </ul> -<h3 id="6-TLC(总肺容量)"><a href="#6-TLC(总肺容量)" class="headerlink" title="6. TLC(总肺容量)"></a>6. TLC(总肺容量)</h3><ul> +<h3 id="6-TLC(总肺容量)"><a class="header-anchor" href="#6-TLC(总肺容量)">¶</a>6. TLC(总肺容量)</h3> +<ul> <li>评估:TLC测量肺部在最大吸气后所能容纳的气体总量,反映了肺的整体容量。</li> -<li>异常:<ul> +<li>异常: +<ul> <li>增加:可能与阻塞性肺病(如COPD)相关,因肺部过度膨胀。</li> <li>降低:可能与限制性肺病(如肺纤维化、胸廓畸形)相关。</li> </ul> </li> </ul> -<h3 id="7-RV(残气量)"><a href="#7-RV(残气量)" class="headerlink" title="7. RV(残气量)"></a>7. RV(残气量)</h3><ul> +<h3 id="7-RV(残气量)"><a class="header-anchor" href="#7-RV(残气量)">¶</a>7. RV(残气量)</h3> +<ul> <li>评估:RV测量在最大呼气后,肺内仍然残留的气体量。</li> -<li>异常:<ul> +<li>异常: +<ul> <li>增加:常见于阻塞性肺病,因气道阻塞导致气体无法完全排出。</li> <li>降低:可能与限制性肺病相关。</li> </ul> diff --git a/2025/03/25/Scala-3-Capture-Checking/index.html b/2025/03/25/Scala-3-Capture-Checking/index.html index b7c12801..b3dcf199 100644 --- a/2025/03/25/Scala-3-Capture-Checking/index.html +++ b/2025/03/25/Scala-3-Capture-Checking/index.html @@ -213,11 +213,8 @@ <li>不安全的资源管理: 例如,传统的 <code>try-with-resources</code> 模式旨在确保资源在使用后被正确关闭。Capture Checking 通过跟踪与资源相关的 Capabilities,可以防止在资源关闭或失效后继续使用它的情况。文档中提到的 <code>usingLogFile</code> 示例就展示了这一点,其中一个闭包尝试写入一个已经关闭的文件,而Capture Checking可以捕获这种不安全的操作:</li> </ul> <figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">usingLogFile</span></span>[<span class="type">T</span>](op: <span class="type">FileOutputStream</span> => <span class="type">T</span>): <span class="type">T</span> =</span><br><span class="line"> <span class="keyword">val</span> logFile = <span class="type">FileOutputStream</span>(<span class="string">"log"</span>)</span><br><span class="line"> <span class="keyword">val</span> result = op(logFile)</span><br><span class="line"> logFile.close()</span><br><span class="line"> result</span><br></pre></td></tr></table></figure> - <figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">usingLogFile</span></span>[<span class="type">T</span>](op: <span class="type">FileOutputStream</span>^ => <span class="type">T</span>): <span class="type">T</span> =</span><br><span class="line"> <span class="comment">// same body as before</span></span><br></pre></td></tr></table></figure> - <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></pre></td><td class="code"><pre><span class="line">| val later = usingLogFile { f => () => f.write(0) }</span><br><span class="line">| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span><br><span class="line">|The expression's type () => Unit is not allowed to capture the root capability `cap`.</span><br><span class="line">|This usually means that a capability persists longer than its allowed lifetime.</span><br></pre></td></tr></table></figure> - <ul> <li>Effect Polymorphism: Capture Checking 提供了一种更灵活和精确地推理和控制副作用的机制。它可以被视为一种 Effect system,允许类型系统跟踪和控制代码可能产生的副作用。</li> <li>“函数的颜色”问题: 在异步编程中,区分同步和异步操作一直是一个挑战。Capture Checking 有可能帮助区分和管理同步与异步计算,这通过跟踪与异步操作相关的 Capabilities 来实现。</li> @@ -231,15 +228,13 @@ <p>开发者还可以指定函数可以捕获的特定 Capability,语法为 <code>A ->{c, d} B</code>,表示该函数可以捕获 Capability <code>c</code> 和 <code>d</code>。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。</p> <p>捕获注解 <code>^</code> 的优先级高于 <code>-></code> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,<code>A ^ C -> B</code> 表示一个从捕获的 <code>A</code> 到 <code>B</code> 的纯函数。</p> <figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">f</span></span>(x: ->{c} <span class="type">Int</span>): <span class="type">Int</span></span><br></pre></td></tr></table></figure> - <p>Capture Checking 也适用于上下文函数。不纯的上下文函数使用 <code>?=></code>,行为类似于 <code>=></code>,可以捕获任意Capability。纯的上下文函数使用 <code>?-></code>,行为类似于 <code>-></code>,不能捕获任何Capability。这表明,Capture Checking扩展到了上下文函数,允许控制在它们的隐式参数作用域内捕获的 Capability。</p> <p>值得注意的是,方法本身并不是值,因此它们不直接捕获Capability。相反,它们对 Capability 的引用会被计入封闭对象的捕获集中。这种区分很重要,因为方法的捕获行为与它们所属对象的状态和Capability相关联,这反映了 Scala 的面向对象特性。</p> <p>与函数类型类似,Capture Checking的概念也延伸到了命名参数类型。<code>=> Int</code> 允许任意Capability引用,类似于不纯函数类型。<code>-> Int</code> 禁止任何Capability引用,类似于纯函数类型。而 <code>->{c} Int</code> 则只允许引用Capability <code>c</code>。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。</p> <p>子捕获(<code>C₁ <: C₂</code>)定义了捕获集之间的关系。捕获集 <code>C₁</code> 是 <code>C₂</code> 的子类型,如果 <code>C₂</code> 包含了 <code>C₁</code> 中的每一个元素,并且满足以下条件之一:<code>c ∈ C₂</code>(直接包含);<code>c</code> 是一个类参数,且 <code>C₂</code> 包含 <code>Cls.this</code>(Capability 来源于封闭的类实例);<code>c</code> 的类型具有捕获集 <code>C</code>,且 <code>C <: C₂</code>(基于 Capability 类型的递归子捕获)。子捕获定义了 Capability 依赖的层级结构,这对于类型系统判断一个需要特定 Capability 集合的值是否可以在提供不同 Capability 集合的上下文中使用至关重要。</p> <p>对于捕获类型的子类型,存在以下规则:纯类型是捕获类型的子类型(<code>T <: C T</code>);较小的捕获集会产生子类型(如果 <code>C₁ <: C₂</code> 且 <code>T₁ <: T₂</code>,则 <code>C₁ T₁ <: C₂ T₂</code>)。这意味着一个依赖较少 Capability 的值通常更通用,可以在更广泛的场景中使用。根 Capability <code>{cap}</code> 覆盖了所有其他捕获集,因此任何特定的捕获集都是 <code>{cap}</code> 的子类型。这允许具有特定捕获要求的类型在允许任何 Capability 的上下文中使用。</p> -<p>Capability widening(也称为 _avoidance_)是一种简化局部变量类型的机制。局部变量的类型会被 widening 到不提及该变量本身的最小超类型,这个过程通常会涉及到变量的捕获集。这种加宽有助于改善类型推断和代码清晰度,避免局部变量的类型变得过于复杂。</p> +<p>Capability widening(也称为 <em>avoidance</em>)是一种简化局部变量类型的机制。局部变量的类型会被 widening 到不提及该变量本身的最小超类型,这个过程通常会涉及到变量的捕获集。这种加宽有助于改善类型推断和代码清晰度,避免局部变量的类型变得过于复杂。</p> <figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">fs: <span class="type">FileSystem</span>^</span><br><span class="line">ct: <span class="type">CanThrow</span>[<span class="type">Exception</span>]^</span><br><span class="line">l : <span class="type">Logger</span>^{fs}</span><br><span class="line"></span><br><span class="line">{l} <: {fs} <: {cap}</span><br><span class="line">{fs} <: {fs, ct} <: {cap}</span><br><span class="line">{ct} <: {fs, ct} <: {cap}</span><br></pre></td></tr></table></figure> - <p>继承自 <code>caps.Capability</code> 的类具有隐式的 <code>{cap}</code> 捕获集。这表明这些类的实例本质上代表了一种 Capability。Capability 类提供了一种将 Capability 显式定义和管理为类型系统中的一等公民的方式。开发者可以创建具有特定语义和使用模式的自定义 Capability。</p> <p>在使用 Capability 类的场景中,通常会结合 using clauses 和隐式参数来减少在代码中显式传递 Capability 的需要。这两种方法提供了一种自动将必要的 Capability 提供给函数和方法的方式,从而减少了样板代码并提高了代码的可读性。</p> <p>闭包会捕获在其主体中引用的来自其周围环境的 Capability。这导致闭包的函数类型中包含捕获集。例如,如果一个闭包引用了一个局部变量 <code>fs</code>,而 <code>fs</code> 是一个 Capability,那么该闭包的类型可能就是 <code>String ->{fs} Unit</code>。这意味着闭包继承了其封闭代码的 Capability 要求,确保它们只能在这些 Capability 可用的上下文中被使用。</p> @@ -249,10 +244,7 @@ <p>类的捕获引用包括从类外部使用的局部 Capability 以及具有捕获类型的构造函数参数(参数Capability)。局部Capability会被内部类继承。</p> <p>类实例的 <code>this</code> 的捕获集是根据捕获的引用、父类以及类内部的使用约束来推断的 1。这种自动推断机制在很多情况下减少了手动指定类捕获集的需要。</p> <figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> caps.<span class="type">Capability</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FileSystem</span> <span class="keyword">extends</span> <span class="title">Capability</span></span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Logger</span>(<span class="params">using <span class="type">FileSystem</span></span>)</span>:</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">log</span></span>(s: <span class="type">String</span>): <span class="type">Unit</span> = ???</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">test</span></span>(<span class="keyword">using</span> fs: <span class="type">FileSystem</span>) =</span><br><span class="line"> <span class="keyword">val</span> l: <span class="type">Logger</span>^{fs} = <span class="type">Logger</span>()</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure> - <figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Logger</span>(<span class="params">using <span class="type">FileSystem</span>^{cap}</span>)</span>:</span><br><span class="line"> ^^^^^^^^^^^^^^</span><br><span class="line"> redundant capture: <span class="type">FileSystem</span> already accounts <span class="keyword">for</span> cap</span><br></pre></td></tr></table></figure> - - <p>捕获隧道 (Capture Tunnelling) 是指当一个类型变量被一个捕获类型实例化时,捕获信息不会立即传播到外层的泛型类型。相反,捕获会“穿过隧道”,并在类型变量被访问或其成员被使用时重新出现。这种机制有助于以更简洁和可管理的方式处理泛型代码中的捕获集,避免类型签名过于复杂。</p> <p>逃逸检查施加了一些限制。作为类型变量实例的捕获类型不能携带通用Capability <code>cap</code> 。可变变量也不能拥有通用捕获集 。逃逸检查阻止了在参数化类型的参数中返回或分配带有局部 Capability 的闭包,因为这可能导致 Capability 逃逸其预期的作用域。单调性规则指出,在一个带有字段 <code>f</code> 的类中,<code>{this}</code> 覆盖了 <code>{this.f}</code> 以及 <code>this.f</code> 对纯参数的应用。这意味着如果类实例本身被视为一种 Capability,那么其字段所持有的任何 Capability 也会被隐式地覆盖。逃逸检查对于维护 Capability 跟踪的完整性至关重要,它可以防止 Capability 在其预期生命周期或作用域之外被使用,尤其是在泛型和可变状态的上下文中。</p> <p>受检异常可以通过导入 <code>language.experimental.saferExceptions</code> 来启用。方法上的 <code>throws</code> 子句会扩展为一个隐式的 <code>CanThrow</code> Capability参数,表明该方法可能抛出指定类型的异常。<code>throw</code> 表达式需要 <code>CanThrow</code> Capability,而 <code>try</code> 表达式会创建这种Capability。在 <code>language.experimental.captureChecking</code> 下,由于逃逸的 Capability 而导致未处理异常的代码会被拒绝。为了实现这种集成,<code>CanThrow</code> 需要继承 <code>Capability</code>,并且需要将逃逸检查扩展到 <code>try</code> 表达式,以防止捕获 <code>cap</code>。Capture Checking 与受检异常的集成确保了异常的可能性也被作为一种 Capability 需求来跟踪,从而加强了语言的整体资源管理和错误处理 Capability。</p> @@ -268,7 +260,8 @@ <th></th> </tr> </thead> -<tbody><tr> +<tbody> +<tr> <td>代码片段</td> <td>演示概念</td> <td>捕获行为解释</td> @@ -338,7 +331,8 @@ <td>使用Capability多态的类型定义</td> <td>类型 <code>Source</code> 被参数化为可以持有Capability集 <code>X^</code>。</td> </tr> -</tbody></table> +</tbody> +</table> <p>当 <code>cap</code> 出现在函数的结果类型中时,通常表示一个由存在性量词绑定的未知类型(例如,<code>() -> Iterator^</code> 意味着 <code>() -> Exists x. Iterator^x</code>)。这表明返回的迭代器可能捕获了某种 Capability,但具体的哪种 Capability 在静态类型检查时是未知的。在内部,这种存在性 Capability 使用带有 sealed trait <code>Exists</code> 的依赖函数类型来表示 。结果类型中协变的 <code>cap</code> 会被替换为一个新的 existential variable。当应用一个具有 existential result 类型 <code>Exists ex.T</code> 的函数时,结果是 <code>T</code>,其中 <code>ex</code> 被 <code>cap</code> 替换。Existential Capability 允许类型系统表达在编译时具体捕获的 Capability 未知的情况,从而提供了灵活性,同时仍然保持了一定程度的跟踪。</p> <p>Reach Capability 用于表达一个变量引用了通过另一个Capability“Reach”的任何操作。例如,如果 <code>ops</code> 是一个表示一组操作的Capability,那么 <code>ops*</code> 就表示出现在 <code>ops</code> 类型中且通过 <code>ops</code> 访问的任何协变 Capability。Reach Capability 提供了一种间接推理和跟踪 Capability 的方式,这对于建模具有相互连接资源的复杂系统非常有用。</p> <p>Capability 多态允许使用带有上界 <code>CapSet</code> 的类型变量来参数化操作的捕获集。这使得定义诸如 <code>Source[X^]</code> 这样的类型成为可能,其中 <code>X^</code> 表示监听器可以持有的一组 Capability。Capability 多态增强了代码的表达性和可重用性,因为它允许函数和数据结构在它们可能依赖的 Capability 集上进行参数化。</p> diff --git a/2025/03/30/F-重载解析问题/index.html b/2025/03/30/F-重载解析问题/index.html index 1b842af0..196229da 100644 --- a/2025/03/30/F-重载解析问题/index.html +++ b/2025/03/30/F-重载解析问题/index.html @@ -194,7 +194,6 @@ <div class="post-content"> <p>当在 F# 中使用一些 C# 类库的时候,可能会遇到类似如下的问题:</p> <figure class="highlight fsharp"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">open</span> System</span><br><span class="line"><span class="keyword">open</span> System.Numerics</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> someFunction () <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> v<span class="operator">:</span> Vector<span class="operator"><</span><span class="type">byte</span><span class="operator">></span> <span class="operator">=</span> Vector(<span class="number">0</span>uy)</span><br><span class="line"> <span class="keyword">let</span> array<span class="operator">:</span> <span class="type">byte</span>[] <span class="operator">=</span> [<span class="operator">||</span>]</span><br><span class="line"> <span class="keyword">let</span> span<span class="operator">:</span> Span<span class="operator"><</span><span class="type">byte</span><span class="operator">></span> <span class="operator">=</span> Span(array)</span><br><span class="line"> v.CopyTo(span)</span><br><span class="line">Compiler error</span><br><span class="line"> v.CopyTo(span)</span><br><span class="line"> <span class="operator">----^^^^^^^^^^^^^^</span></span><br><span class="line"></span><br><span class="line"><span class="operator">/</span>home<span class="operator">/</span>muqiu<span class="operator">/</span>stdin(<span class="number">99</span>,<span class="number">5</span>)<span class="operator">:</span> error FS0041<span class="operator">:</span> A unique overload <span class="keyword">for</span> method <span class="symbol">'CopyTo</span>' could <span class="built_in">not</span> be determined based on <span class="keyword">type</span> information prior <span class="keyword">to</span> this program point. A <span class="keyword">type</span> annotation may be needed.</span><br><span class="line"></span><br><span class="line">Known <span class="keyword">type</span> <span class="title class_">of</span> argument<span class="operator">:</span> Span<span class="operator"><</span><span class="type">byte</span><span class="operator">></span></span><br><span class="line"></span><br><span class="line">Candidates<span class="operator">:</span></span><br><span class="line"> <span class="operator">-</span> Vector.CopyTo(destination<span class="operator">:</span> Span<span class="operator"><</span><span class="type">byte</span><span class="operator">></span>) <span class="operator">:</span> <span class="type">unit</span></span><br><span class="line"> <span class="operator">-</span> Vector.CopyTo(destination<span class="operator">:</span> Span<span class="operator"><</span><span class="type">byte</span><span class="operator">></span>) <span class="operator">:</span> <span class="type">unit</span></span><br></pre></td></tr></table></figure> - <p>编译器认为存在两个相同的 <code>CopyTo</code> 重载无法区分。但查阅文档发现,<code>Vector<T></code> 的 <code>CopyTo</code> 方法实际上只有一个匹配的重载(接受 <code>Span<T></code>),这似乎矛盾。</p> <p>这是因为 F# 编译器处理泛型方法重载的方式:</p> <ul> @@ -203,10 +202,10 @@ </ul> <p>仅我所知的一种解决方案是通过添加扩展方法显式指引编译器:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Vector</span><span class="operator"><</span><span class="symbol">'T</span><span class="operator">></span> <span class="keyword">with</span></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">inline</span> this.CopyToByteSpan (span <span class="operator">:</span> Span<span class="operator"><</span><span class="type">byte</span><span class="operator">></span>) <span class="operator">=</span> </span><br><span class="line"> this.CopyTo(span) <span class="comment">// 显式绑定具体重载</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用时使用新方法</span></span><br><span class="line">v.CopyToByteSpan(span)</span><br></pre></td></tr></table></figure> - <p>这种方法通过创建具体的类型路径,帮助编译器绕过复杂的重载解析逻辑。</p> <p>学艺不精,不知道这是不是语言设计的差异,可能 F# 倾向于要求更明确的类型信息以避免意外行为?或许当泛型类型继承多个接口时,有没有可能出现在具体类型中不易察觉的隐式重载冲突?</p> -<h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs."></a>Refs.</h2><ul> +<h2 id="Refs"><a class="header-anchor" href="#Refs">¶</a>Refs.</h2> +<ul> <li><a target="_blank" rel="noopener" href="https://github.com/fsharp/fslang-suggestions/issues/905">“Most concrete” tiebreaker for generic overloads #905</a></li> <li><a target="_blank" rel="noopener" href="https://github.com/dotnet/fsharp/issues/1647">Fail to resolve to non generic overload. #1647</a></li> </ul> diff --git a/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html b/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html index 21e0a840..e778cdd4 100644 --- a/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html +++ b/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html @@ -192,51 +192,50 @@ </div> </div> <div class="post-content"> - <h2 id="一、传统手艺:Partial-Application"><a href="#一、传统手艺:Partial-Application" class="headerlink" title="一、传统手艺:Partial Application"></a>一、传统手艺:Partial Application</h2><p>在函数式编程中,Partial Application 是传递依赖的常用方式。例如:</p> + <h2 id="一、传统手艺:Partial-Application"><a class="header-anchor" href="#一、传统手艺:Partial-Application">¶</a>一、传统手艺:Partial Application</h2> +<p>在函数式编程中,Partial Application 是传递依赖的常用方式。例如:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> foo bar baz request <span class="operator">=</span> <span class="operator">...</span></span><br><span class="line"><span class="keyword">let</span> wired <span class="operator">=</span> foo dependency1 dependency2</span><br><span class="line"><span class="keyword">let</span> response <span class="operator">=</span> wired request</span><br></pre></td></tr></table></figure> - -<p>优点: </p> +<p>优点:</p> <ul> -<li>无需框架或反射,直接通过函数参数传递依赖。 </li> +<li>无需框架或反射,直接通过函数参数传递依赖。</li> <li>符合函数式编程的纯函数理念。</li> </ul> -<p>缺点: </p> +<p>缺点:</p> <ul> -<li>参数爆炸:当功能扩展时,参数数量激增(如日志、数据库、加密等)。 </li> -<li>维护困难:新增依赖需修改所有调用点的参数传递。 </li> +<li>参数爆炸:当功能扩展时,参数数量激增(如日志、数据库、加密等)。</li> +<li>维护困难:新增依赖需修改所有调用点的参数传递。</li> <li>隐式依赖:难以从函数签名直接区分核心参数与辅助依赖。</li> </ul> <hr> -<h2 id="二、结构化方法:单一环境参数(env)"><a href="#二、结构化方法:单一环境参数(env)" class="headerlink" title="二、结构化方法:单一环境参数(env)"></a>二、结构化方法:单一环境参数(<code>env</code>)</h2><p>为解决参数爆炸问题,可将依赖封装为单一环境对象<code>env</code>,并通过接口约束访问权限:</p> +<h2 id="二、结构化方法:单一环境参数(env)"><a class="header-anchor" href="#二、结构化方法:单一环境参数(env)">¶</a>二、结构化方法:单一环境参数(<code>env</code>)</h2> +<p>为解决参数爆炸问题,可将依赖封装为单一环境对象<code>env</code>,并通过接口约束访问权限:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">[<Interface>]</span> <span class="keyword">type</span> <span class="title class_">ILog</span> <span class="operator">=</span> <span class="keyword">abstract</span> Logger<span class="operator">:</span> ILogger</span><br><span class="line"><span class="meta">[<Interface>]</span> <span class="keyword">type</span> <span class="title class_">IDb</span> <span class="operator">=</span> <span class="keyword">abstract</span> Database<span class="operator">:</span> IDatabase</span><br><span class="line"></span><br><span class="line"><span class="keyword">module</span> Log <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> info (env<span class="operator">:</span> #ILog) <span class="operator">=</span> env.Logger.Info(<span class="string">"Message"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">module</span> Db <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> fetchUser (env<span class="operator">:</span> #IDb) <span class="operator">=</span> env.Database.Query(<span class="operator">...</span>)</span><br></pre></td></tr></table></figure> - -<p>优点: </p> +<p>优点:</p> <ul> -<li>显式依赖声明:函数签名仅需<code>env</code>参数,编译器验证接口实现。 </li> -<li>模块化隔离:各模块仅声明所需接口(如<code>ILog</code>、<code>IDb</code>),避免全局依赖。 </li> +<li>显式依赖声明:函数签名仅需<code>env</code>参数,编译器验证接口实现。</li> +<li>模块化隔离:各模块仅声明所需接口(如<code>ILog</code>、<code>IDb</code>),避免全局依赖。</li> <li>易于测试:通过模拟<code>env</code>实现单元测试,无需依赖具体实现。</li> </ul> -<p>应用场景: </p> +<p>应用场景:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> changePass env req <span class="operator">=</span> <span class="keyword">task</span> {</span><br><span class="line"> <span class="keyword">let!</span> user <span class="operator">=</span> Db.fetchUser env req.UserId</span><br><span class="line"> Log.info env <span class="string">"Processing user: %i"</span> user.Id</span><br><span class="line"> <span class="operator">...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <hr> -<h2 id="三、Reader-Monad"><a href="#三、Reader-Monad" class="headerlink" title="三、Reader Monad"></a>三、Reader Monad</h2><p>为消除显式的<code>env</code>传递,可引入 Reader Monad,将环境隐式注入计算流程:</p> +<h2 id="三、Reader-Monad"><a class="header-anchor" href="#三、Reader-Monad">¶</a>三、Reader Monad</h2> +<p>为消除显式的<code>env</code>传递,可引入 Reader Monad,将环境隐式注入计算流程:</p> <figure class="highlight fsharp"><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="meta">[<Struct>]</span> <span class="keyword">type</span> <span class="title class_">Effect</span><span class="operator"><</span><span class="symbol">'env</span>, <span class="symbol">'out</span><span class="operator">></span> <span class="operator">=</span> Effect <span class="keyword">of</span> (<span class="symbol">'env</span> <span class="operator">-></span> <span class="symbol">'out</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">module</span> Effect <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> run env (Effect fn) <span class="operator">=</span> fn env</span><br><span class="line"> <span class="keyword">let</span> bind f effect <span class="operator">=</span> Effect (<span class="keyword">fun</span> env <span class="operator">-></span> run env (f (run env effect)))</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">EffectBuilder</span>() <span class="operator">=</span></span><br><span class="line"> <span class="keyword">member</span> __.Bind(e, f) <span class="operator">=</span> Effect.bind f e</span><br><span class="line"> <span class="keyword">member</span> __.Return(x) <span class="operator">=</span> Effect (<span class="keyword">fun</span> _ <span class="operator">-></span> x)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> effect <span class="operator">=</span> EffectBuilder()</span><br></pre></td></tr></table></figure> - <p>然后:</p> <figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> changePass req <span class="operator">=</span> <span class="keyword">effect</span> {</span><br><span class="line"> <span class="keyword">let!</span> user <span class="operator">=</span> Db.fetchUser req.UserId</span><br><span class="line"> <span class="keyword">let!</span> salt <span class="operator">=</span> Random.bytes <span class="number">32</span></span><br><span class="line"> <span class="keyword">do!</span> Log.info <span class="string">"Password updated for user %i"</span> user.Id</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">Ok</span>()</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - -<p>优点: </p> +<p>优点:</p> <ul> -<li>隐式依赖管理:通过<code>effect</code>计算表达式自动传递<code>env</code>,减少样板代码。 </li> -<li>组合性:支持与其他计算表达式(如<code>async</code>/<code>task</code>)结合,处理异步操作。</li> +<li>隐式依赖管理:通过<code>effect</code>计算表达式自动传递<code>env</code>,减少样板代码。</li> +<li>组合性:支持与其他计算表达式(如<code>async</code>/<code>task</code>)结合,处理异步操作。</li> </ul> -<p>缺点: </p> +<p>缺点:</p> <ul> -<li>性能开销:频繁的闭包创建和间接调用可能导致性能下降。 </li> +<li>性能开销:频繁的闭包创建和间接调用可能导致性能下降。</li> <li>生态兼容性:需自定义计算表达式,与现有异步框架集成复杂。</li> </ul> -<h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs."></a>Refs.</h2><ul> +<h2 id="Refs"><a class="header-anchor" href="#Refs">¶</a>Refs.</h2> +<ul> <li>Spring的构造器注入</li> <li>Blazor的DI实现</li> </ul> diff --git a/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html b/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html index d8936873..819f6d02 100644 --- a/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html +++ b/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html @@ -198,10 +198,12 @@ <p>现在,需要获取前 10 个用户以及他们各自的所有帖子。</p> <p>一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:</p> <ol> -<li>第一次查询 (The “1”): 获取前 10 个用户。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">User</span> LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure></li> -<li>接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 用户 1</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="comment">-- 用户 2</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">2</span>;</span><br><span class="line"><span class="comment">-- 用户 3</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line"><span class="comment">-- ... 直到 用户 10</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">10</span>;</span><br></pre></td></tr></table></figure></li> +<li>第一次查询 (The “1”): 获取前 10 个用户。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">User</span> LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure> +</li> +<li>接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 用户 1</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="comment">-- 用户 2</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">2</span>;</span><br><span class="line"><span class="comment">-- 用户 3</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line"><span class="comment">-- ... 直到 用户 10</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">10</span>;</span><br></pre></td></tr></table></figure> +</li> </ol> -<p>在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。</p> +<p>在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。</p> <p>N+1 问题通常源于 ORM 处理关联数据的方式,特别是与“懒加载”(Lazy Loading)相关的策略。懒加载是指只有在显式访问关联属性时,ORM 才会去数据库加载这些数据。虽然这在某些情况下可以避免加载不需要的数据,但如果在循环中访问关联属性,就很容易触发 N+1 问题。</p> <p>然而,问题的根源在于没有有效地预先加载(或批量加载)所需的关联数据。即使不使用严格意义上的懒加载,如果 ORM 在处理关联查询时不够智能,采用了逐个获取关联对象的策略,同样会产生 N+1 查询。</p> <p>在 Prisma 出现之前或在其他 ORM 中,解决 N+1 问题常见的方法包括:</p> @@ -212,14 +214,16 @@ <p>Prisma ORM 在设计上就考虑了 N+1 问题,并提供了一种既方便开发者又高效的解决方案。当使用 Prisma Client 查询数据并需要包含关联模型时,Prisma 会自动优化查询,避免产生 N+1 查询,主要通过关系查询(Relation Queries)中的 <code>include</code> 选项或嵌套读取(nested reads)来实现这一点:</p> <p>假设想获取所有用户及其发布的帖子,使用 Prisma Client,可以这样写:</p> <figure class="highlight typescript"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">PrismaClient</span> } <span class="keyword">from</span> <span class="string">'@prisma/client'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> prisma = <span class="keyword">new</span> <span class="title class_">PrismaClient</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">getUsersWithPosts</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> usersWithPosts = <span class="keyword">await</span> prisma.<span class="property">user</span>.<span class="title function_">findMany</span>({</span><br><span class="line"> <span class="attr">include</span>: {</span><br><span class="line"> <span class="attr">posts</span>: <span class="literal">true</span>, <span class="comment">// 指示 Prisma 加载关联的 posts</span></span><br><span class="line"> },</span><br><span class="line"> })</span><br><span class="line"> <span class="comment">// usersWithPosts 包含了用户列表,每个用户对象中都有一个 posts 数组</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(usersWithPosts)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="title function_">getUsersWithPosts</span>()</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function">(<span class="params">e</span>) =></span> {</span><br><span class="line"> <span class="keyword">throw</span> e</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">finally</span>(<span class="title function_">async</span> () => {</span><br><span class="line"> <span class="keyword">await</span> prisma.$disconnect()</span><br><span class="line"> })</span><br></pre></td></tr></table></figure> - <p>当执行上述查询时,Prisma 不会 生成 N+1 个 SQL 查询。而是首先会分析请求,并将其转化为数量非常有限的高效 SQL 查询。对于上面这个一对多关系的 <code>include</code> 查询,Prisma 通常会执行以下两步(类似于批量加载策略):</p> <ol> -<li>查询父模型: 获取所有 <code>User</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> "public"."User"."id", "public"."User"."name", <span class="comment">/* ... other user fields */</span> <span class="keyword">FROM</span> "public"."User" <span class="keyword">WHERE</span> <span class="number">1</span><span class="operator">=</span><span class="number">1</span></span><br></pre></td></tr></table></figure></li> -<li>查询关联的子模型: 使用上一步获取到的所有用户 <code>id</code>,通过 <code>WHERE IN (...)</code> 子句一次性查询所有相关的 <code>Post</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> "public"."Post"."id", "public"."Post"."title", "public"."Post"."authorId", <span class="comment">/* ... other post fields */</span> <span class="keyword">FROM</span> "public"."Post" <span class="keyword">WHERE</span> "public"."Post"."authorId" <span class="keyword">IN</span> ($<span class="number">1</span>, $<span class="number">2</span>, $<span class="number">3</span>, ...) <span class="comment">/* 这里的 $1, $2, ... 是第一步查到的用户 ID 列表 */</span></span><br></pre></td></tr></table></figure></li> +<li>查询父模型: 获取所有 <code>User</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> "public"."User"."id", "public"."User"."name", <span class="comment">/* ... other user fields */</span> <span class="keyword">FROM</span> "public"."User" <span class="keyword">WHERE</span> <span class="number">1</span><span class="operator">=</span><span class="number">1</span></span><br></pre></td></tr></table></figure> +</li> +<li>查询关联的子模型: 使用上一步获取到的所有用户 <code>id</code>,通过 <code>WHERE IN (...)</code> 子句一次性查询所有相关的 <code>Post</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> "public"."Post"."id", "public"."Post"."title", "public"."Post"."authorId", <span class="comment">/* ... other post fields */</span> <span class="keyword">FROM</span> "public"."Post" <span class="keyword">WHERE</span> "public"."Post"."authorId" <span class="keyword">IN</span> ($<span class="number">1</span>, $<span class="number">2</span>, $<span class="number">3</span>, ...) <span class="comment">/* 这里的 $1, $2, ... 是第一步查到的用户 ID 列表 */</span></span><br></pre></td></tr></table></figure> +</li> </ol> <p>Prisma Client 在内存中将这两次查询的结果高效地组合起来,最终返回嵌套的、符合 TypeScript 类型的数据。</p> -<h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs."></a>Refs.</h2><ul> +<h2 id="Refs"><a class="header-anchor" href="#Refs">¶</a>Refs.</h2> +<ul> <li><a target="_blank" rel="noopener" href="https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping">Stack Overflow: What is the N+1 selects problem in ORM?</a></li> <li><a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-client/queries/query-optimization-performance#solving-the-n1-problem">Prisma Docs: Solving the N+1 problem</a></li> </ul> diff --git a/2025/04/13/uuidv7-rdbms/index.html b/2025/04/13/uuidv7-rdbms/index.html index 6872f514..48d66555 100644 --- a/2025/04/13/uuidv7-rdbms/index.html +++ b/2025/04/13/uuidv7-rdbms/index.html @@ -193,23 +193,31 @@ </div> <div class="post-content"> <p>UUID v7 之所以能显著提升关系型数据库(尤其是采用聚集索引的 MySQL InnoDB)在插入和查询速度,主要归功于它在 ID 中引入了“可排序的时间戳前缀”,从而大幅减少了 B-Tree 索引页分裂(page split)和数据碎片化。这里分几点来说:</p> -<p>一、 聚集索引(Clustered Index)的插入机制 </p> +<p>一、 聚集索引(Clustered Index)的插入机制</p> <p>在 InnoDB 中,聚集索引的叶子节点同时存储了行数据,且按照索引键(主键)顺序物理排序。</p> -<p>当新记录的主键完全随机(如 UUID v4)时,每次插入都会随机落在 B-Tree 的不同叶子页,导致频繁的页分裂和指针重排,而页分裂和随机 I/O 会带来大量的磁盘写放大和缓存抖动(cache churn),削弱吞吐并拉高延迟。</p> -<p>二、UUID v7 的时间排序特性 </p> -<p>UUID v7 在高位(前 48 位)嵌入了以毫秒级精度的 Unix 时间戳,剩下的位用于随机数或序列号,这样生成的 ID 保持全局唯一性的同时,随着时间自然递增(即“近似单调递增”),新插入的记录几乎总是追加到 B-Tree 的最右端叶子节点。 </p> -<p>参见 dbaplus.cn 的分析: </p> +<p>当新记录的主键完全随机(如 UUID v4)时,每次插入都会随机落在 B-Tree 的不同叶子页,导致频繁的页分裂和指针重排,而页分裂和随机 I/O 会带来大量的磁盘写放大和缓存抖动(cache churn),削弱吞吐并拉高延迟。</p> +<p>二、UUID v7 的时间排序特性</p> +<p>UUID v7 在高位(前 48 位)嵌入了以毫秒级精度的 Unix 时间戳,剩下的位用于随机数或序列号,这样生成的 ID 保持全局唯一性的同时,随着时间自然递增(即“近似单调递增”),新插入的记录几乎总是追加到 B-Tree 的最右端叶子节点。</p> +<p>参见 <a target="_blank" rel="noopener" href="http://dbaplus.cn">dbaplus.cn</a> 的分析:</p> <blockquote> <p>“UUID v7 的创新之处在于其时间排序特性,它在前 48 位中嵌入了以毫秒为单位的 Unix 时间戳……可能在插入和查询操作上提供更好的性能”【1】。</p> </blockquote> -<p>三、降低页分裂与碎片化 </p> +<p>三、降低页分裂与碎片化</p> <p>顺序或近似顺序的主键能使叶子节点连续增长,极少触发页分裂,并且更少的页分裂意味着更低的写放大(write amplification)和更稳定的插入延迟,同时,减少了空洞和链表重排,提高了磁盘和内存缓存的命中率。</p> -<p>四、提升查询局部性与缓存命中 </p> -<p>因为数据物理上是按时间顺序紧凑写入,时间范围查询(如 “最近 1 小时的日志”)可以快速定位连续的叶子页,I/O 更聚集,内存缓冲池(buffer pool)或操作系统页缓存能更有效地缓存最近热数据,进一步加速查询。</p> +<p>四、提升查询局部性与缓存命中</p> +<p>因为数据物理上是按时间顺序紧凑写入,时间范围查询(如 “最近 1 小时的日志”)可以快速定位连续的叶子页,I/O 更聚集,内存缓冲池(buffer pool)或操作系统页缓存能更有效地缓存最近热数据,进一步加速查询。</p> <hr> -<p>Rimon Tawadrous 在其 GitHub repo 中的测试,对比 100 万条逐条插入实验,UUID v7 相较 UUID v4 在单线程插入上速度快约 3.24%,多线程下更可观【1】。 </p> +<p>Rimon Tawadrous 在其 GitHub repo 中的测试,对比 100 万条逐条插入实验,UUID v7 相较 UUID v4 在单线程插入上速度快约 3.24%,多线程下更可观【1】。</p> <hr> -<p>参考链接<br>[1] “为什么 UUID 7 比 UUID 4 更适合作为 RDBMS 的聚集索引?” dbaplus.cn<br> <a target="_blank" rel="noopener" href="https://dbaplus.cn/news-160-6313-1.html">https://dbaplus.cn/news-160-6313-1.html</a><br>[2] “PostgreSQL and UUID as primary key” maciejwalkowiak<br> <a target="_blank" rel="noopener" href="https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/">https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/</a><br>[3] “Optimised UUIDs in mysql” stitcher<br> <a target="_blank" rel="noopener" href="https://stitcher.io/blog/optimised-uuids-in-mysql">https://stitcher.io/blog/optimised-uuids-in-mysql</a><br>[3] “Storing UUID Values in MySQL” percona<br> <a target="_blank" rel="noopener" href="https://www.percona.com/blog/store-uuid-optimized-way/">https://www.percona.com/blog/store-uuid-optimized-way/</a></p> +<p>参考链接<br> +[1] “为什么 UUID 7 比 UUID 4 更适合作为 RDBMS 的聚集索引?” <a target="_blank" rel="noopener" href="http://dbaplus.cn">dbaplus.cn</a><br> +<a target="_blank" rel="noopener" href="https://dbaplus.cn/news-160-6313-1.html">https://dbaplus.cn/news-160-6313-1.html</a><br> +[2] “PostgreSQL and UUID as primary key” maciejwalkowiak<br> +<a target="_blank" rel="noopener" href="https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/">https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/</a><br> +[3] “Optimised UUIDs in mysql” stitcher<br> +<a target="_blank" rel="noopener" href="https://stitcher.io/blog/optimised-uuids-in-mysql">https://stitcher.io/blog/optimised-uuids-in-mysql</a><br> +[3] “Storing UUID Values in MySQL” percona<br> +<a target="_blank" rel="noopener" href="https://www.percona.com/blog/store-uuid-optimized-way/">https://www.percona.com/blog/store-uuid-optimized-way/</a></p> </div> diff --git a/2025/04/20/linux-amd-screen-boom/index.html b/2025/04/20/linux-amd-screen-boom/index.html index 78a8c76f..41c36deb 100644 --- a/2025/04/20/linux-amd-screen-boom/index.html +++ b/2025/04/20/linux-amd-screen-boom/index.html @@ -195,7 +195,6 @@ <p>不知道我们所说的闪烁是不是一个东西,在我的笔记本上,闪烁是指偶发的屏幕中出现部分彩色雪花。</p> <p>我使用 Pop!_OS,此发行版使用 EFI 启动,故而可以在 <code>/boot/efi/loader/loader.conf</code> 中加上:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">options quiet splash amdgpu.dcdebugmask=0x10 amdgpu.sg_display=0</span><br></pre></td></tr></table></figure> - <p>对于其他使用 EFI 或 GRUB 启动的发行版也可以添加类似的参数尝试尝试。</p> </div> diff --git a/2025/05/07/nestjs-bullmq-mail-business/index.html b/2025/05/07/nestjs-bullmq-mail-business/index.html index 6b36afbd..5eb3bcbd 100644 --- a/2025/05/07/nestjs-bullmq-mail-business/index.html +++ b/2025/05/07/nestjs-bullmq-mail-business/index.html @@ -192,48 +192,53 @@ </div> </div> <div class="post-content"> - <p>在 BullMQ(以及它在 NestJS 里包装的 <code>@Processor</code>/<code>WorkerHost</code>)里,整个生命周期大致是这样的:</p> + <p>在 BullMQ(以及它在 NestJS 里包装的 <code>@Processor</code>/<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 就被标记为 completed,然后才会去触发所有注册了 <code>@OnWorkerEvent('completed')</code> 的回调。</li> +<li>队列(在 NestJS 里由 <code>@Processor</code> 装饰的类)会被一个底层的 <code>Worker</code> 订阅。</li> +<li>有新任务(job)进来时,Worker 会调用写在该类里的 <code>async process(job: Job)</code> 方法。</li> +<li>如果 <code>process()</code> 正常返回(即没有抛异常),Job 就被标记为 completed,然后才会去触发所有注册了 <code>@OnWorkerEvent('completed')</code> 的回调。</li> </ol> <p>也就是说:</p> <ul> -<li><code>process</code>:是真正“干活”的地方,收到 job 之后立刻被调用,任何主业务逻辑(发邮件/写数据库/第三方请求等)都应该放这里。 </li> +<li><code>process</code>:是真正“干活”的地方,收到 job 之后立刻被调用,任何主业务逻辑(发邮件/写数据库/第三方请求等)都应该放这里。</li> <li><code>onCompleted</code>:只是一个事件监听器,在 job 已经成功完成之后 才会被触发,不会影响 job 的重试逻辑(也就是说,在这里抛错,job 已经算完成了,也不会重试)。</li> </ul> <p>而我在此处的业务目的是 “用队列来做可靠的、可重试的邮件发送”,那么一定要把发送邮件的逻辑写到 <code>process()</code> 里,这样在 <code>commandBus.execute(new SendMailCommand(...))</code> 抛错时,BullMQ 会根据创建 JOB 时的重试策略(retry、backoff 等)自动重新入队。而把它放到 <code>onCompleted()</code>,只相当于 job 成功完成后的“事后通知”,一旦失败不会再重试,也无法利用 BullMQ 的锁、超时、重试机制。</p> -<p>举个最简化的调整示例,删掉 <code>onCompleted</code>,把真正的发信放到 <code>process</code>: </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 || "gcpm-mailer")</span><br><span class="line">export class BullMQMailerProcesser extends WorkerHost {</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"> ) {</span><br><span class="line"> super();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // ① 当有新 job 拉取到时,这个方法会被调用</span><br><span class="line"> public async process(job: Job): Promise<void> {</span><br><span class="line"> const mailAggregate = new Mail(job.data.mail);</span><br><span class="line"> try {</span><br><span class="line"> await this.commandBus.execute(new SendMailCommand(mailAggregate));</span><br><span class="line"> } catch (err) {</span><br><span class="line"> this.logger.error(`邮件发送失败,jobId=${job.id}`, err);</span><br><span class="line"> // 抛出错误,触发重试或失败</span><br><span class="line"> throw err;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // ② onCompleted 仅在 process() 正常返回后触发,</span><br><span class="line"> // 不建议在这里执行核心业务(也无法触发重试)。</span><br><span class="line"> // @OnWorkerEvent("completed")</span><br><span class="line"> // async onCompleted(job: Job) { … }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>参考 BullMQ 官方文档:</p> <ul> -<li>“Workers → Sandboxed processors”:Worker 拉到 job 就调用注册的处理函数,然后根据返回/抛错把 job 标记成 completed 或 failed。 </li> +<li>“Workers → Sandboxed processors”:Worker 拉到 job 就调用注册的处理函数,然后根据返回/抛错把 job 标记成 completed 或 failed。</li> <li>“Events → OnJobCompleted”:completed 事件只是一个监听钩子,不会参与重试。</li> </ul> <hr> <p>而 重试次数本身并没有一个硬性上限,完全由添加 Job 时通过 <code>attempts</code> 这个选项来控制:</p> <ul> -<li>默认情况下,如果不传 <code>attempts</code>(或不在 <code>defaultJobOptions</code> 里配置),Job 不会自动重试(相当于 <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>(或不在 <code>defaultJobOptions</code> 里配置),Job 不会自动重试(相当于 <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"> { <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(<span class="comment">/*…*/</span> ) },</span><br><span class="line"> {</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>: { <span class="comment">// 重试时的延迟策略(可选)</span></span><br><span class="line"> <span class="attr">type</span>: <span class="string">'exponential'</span>,</span><br><span class="line"> <span class="attr">delay</span>: <span class="number">1000</span>,</span><br><span class="line"> },</span><br><span class="line"> },</span><br><span class="line">);</span><br></pre></td></tr></table></figure> - <hr> <p>还有一个需要注意的地方,在我的业务中,邮件发送的是一种时间区间报告,这个报告包含了过去二十四小时的一些系统中的事件,但如果重试有延迟策略或重试本身就有计算成本的话,这封邮件就不是 “过去二十四小时” 的了,因为重试带来了一个真空期。</p> <p>换言之,这个问题本质上是——重试导致「发送时刻」与「原始 24 小时窗口」错开,从而让邮件里报出来的数据不再精确。常见的解决思路就是:把「窗口定义」或者「报表内容」在调度时就固化下来,真正的队列任务只负责发送,而不再实时去重新计算时间区间。</p> <p>我想到了两种解决方案:</p> -<p>一、任务参数里带上「时间区间」<br> 在 enqueue 的时候,就算出 windowStart/windowEnd,然后把它放到 <code>job.data</code> 里。无论后面 <code>process</code> 什么时候真正跑,都是基于同一个时间区间去查询:</p> +<p>一、任务参数里带上「时间区间」<br> +在 enqueue 的时候,就算出 windowStart/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"> {</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>({</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"> }),</span><br><span class="line"> },</span><br><span class="line"> {</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>: { <span class="attr">type</span>: <span class="string">'exponential'</span>, <span class="attr">delay</span>: <span class="number">1000</span> },</span><br><span class="line"> },</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>) {</span><br><span class="line"> <span class="keyword">const</span> { windowStart, windowEnd } = 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">}</span><br></pre></td></tr></table></figure> -<p> ➜ 这样无是马上执行还是几次重试后才执行,数据规则都不会变。</p> -<p>二、预先生成「静态报表内容」,挂到队列里<br> 如果计算成本很高,或者怕重复查询数据开销大,也可以在调度时就把最终的 HTML/Text/附件 都先打好,然后作为 <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"> {</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>({ <span class="comment">/*…*/</span>, windowStart, <span class="attr">windowEnd</span>: now }),</span><br><span class="line"> reportHtml, <span class="comment">// <- 预渲染好的文本/HTML</span></span><br><span class="line"> <span class="attr">attachments</span>: […], <span class="comment">// <- 如果有附件也一并塞</span></span><br><span class="line"> },</span><br><span class="line"> { <span class="attr">attempts</span>: <span class="number">3</span>, <span class="attr">backoff</span>: { <span class="attr">type</span>: <span class="string">'fixed'</span>, <span class="attr">delay</span>: <span class="number">5_000</span> } },</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>) {</span><br><span class="line"> <span class="keyword">try</span> {</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>({</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"> });</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="keyword">throw</span> e; <span class="comment">// 触发重试</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br> ➜ 重试带来的任何延迟,都不影响邮件正文,始终是一份「事先约定好、并且静态化」的报告。</p> -<p>这两种模式都能保证最终发送时的数据窗口或内容,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算/少算”问题。</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 & 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> +<p>➜ 这样无是马上执行还是几次重试后才执行,数据规则都不会变。</p> +<p>二、预先生成「静态报表内容」,挂到队列里<br> +如果计算成本很高,或者怕重复查询数据开销大,也可以在调度时就把最终的 HTML/Text/附件 都先打好,然后作为 <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><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"> {</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>({ <span class="comment">/*…*/</span>, windowStart, <span class="attr">windowEnd</span>: now }),</span><br><span class="line"> reportHtml, <span class="comment">// <- 预渲染好的文本/HTML</span></span><br><span class="line"> <span class="attr">attachments</span>: […], <span class="comment">// <- 如果有附件也一并塞</span></span><br><span class="line"> },</span><br><span class="line"> { <span class="attr">attempts</span>: <span class="number">3</span>, <span class="attr">backoff</span>: { <span class="attr">type</span>: <span class="string">'fixed'</span>, <span class="attr">delay</span>: <span class="number">5_000</span> } },</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>) {</span><br><span class="line"> <span class="keyword">try</span> {</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>({</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"> });</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="keyword">throw</span> e; <span class="comment">// 触发重试</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> +<p>➜ 重试带来的任何延迟,都不影响邮件正文,始终是一份「事先约定好、并且静态化」的报告。</p> +<p>这两种模式都能保证最终发送时的数据窗口或内容,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算/少算”问题。</p> +<h2 id="参考文档:"><a class="header-anchor" href="#参考文档:">¶</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 & 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> diff --git a/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html b/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html index 47a9feef..42f2895c 100644 --- a/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html +++ b/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html @@ -193,36 +193,41 @@ </div> <div class="post-content"> <p>最近碰到一块业务:在系统中可以存在多个用户同时对某个项目信息进行编辑,这种多人协作的场景挺有意思的,不过在我们的业务中,并不需要实时协作,只需要保证不会出错就行,话虽如此,但也可以探索一下实时协作的实现方案,防止老年痴呆。</p> -<p>先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线/并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。 </p> +<p>先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线/并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。</p> <p>核心思想是:所有并发操作都是幂等(idempotent)、可交换(commutative)的。</p> -<p>常见类型有:<br>一、G-Counter(只能增计数器)<br>二、PN-Counter(可增可减计数器)<br>三、LWW-Register(最后写入胜出)<br>四、结合 JSON 的树型 CRDT(如 <a target="_blank" rel="noopener" href="https://github.com/automerge/automerge">Automerge</a> / <a target="_blank" rel="noopener" href="https://yjs.dev/">Yjs</a>) </p> -<p>更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”[^1]。</p> +<p>常见类型有:<br> +一、G-Counter(只能增计数器)<br> +二、PN-Counter(可增可减计数器)<br> +三、LWW-Register(最后写入胜出)<br> +四、结合 JSON 的树型 CRDT(如 <a target="_blank" rel="noopener" href="https://github.com/automerge/automerge">Automerge</a> / <a target="_blank" rel="noopener" href="https://yjs.dev/">Yjs</a>)</p> +<p>更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。</p> <blockquote> <p>有一个挺有趣的 Rust 项目 <a target="_blank" rel="noopener" href="https://github.com/loro-dev/loro">Loro: Make your JSON data collaborative and version-controlled with CRDTs</a></p> </blockquote> -<p>假设我的项目信息编辑页面允许多人实时/离线修改某个研究项目的“名称”、“描述”字段,前端用 SvelteKit + GraphQL 获取和提交变更:</p> +<p>假设我的项目信息编辑页面允许多人实时/离线修改某个研究项目的“名称”、“描述”字段,前端用 SvelteKit + GraphQL 获取和提交变更:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">┌── 用户 A 离线修改了 “description” 的若干段文本 </span><br><span class="line">└── 用户 B 同时在线修改了同一字段的其他段落 </span><br></pre></td></tr></table></figure> - <p>如果后端使用 CRDT(比如把 <code>description</code> 用 JSON-CRDT 存储),两次修改只要在任意顺序合并都能得到完整的内容:</p> -<p>首先,A 客户端本地 apply 操作并缓存,恢复网络后推给服务器;<br>然后,服务器用 CRDT merge(A.delta, B.delta),得到一致文档<br>最后,服务器广播新文档到所有客户端,A/B 均得到相同结果 </p> +<p>首先,A 客户端本地 apply 操作并缓存,恢复网络后推给服务器;<br> +然后,服务器用 CRDT merge(A.delta, B.delta),得到一致文档<br> +最后,服务器广播新文档到所有客户端,A/B 均得到相同结果</p> <hr> -<p>好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁/超时自动释放。 </p> +<p>好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁/超时自动释放。</p> <p>假如有一个这样的锁表:</p> -<pre><code class="sql">CREATE TABLE project_lock ( +<pre><code class="language-sql">CREATE TABLE project_lock ( project_id UUID PRIMARY KEY, locked_by UUID NOT NULL, expires_at TIMESTAMPTZ NOT NULL ); </code></pre> <p>可以在事务内申请它:</p> -<pre><code class="ts">const now = new Date(); +<pre><code class="language-ts">const now = new Date(); const expires = new Date(now.getTime() + 5*60*1000); // 5 分钟后过期 await prisma.$transaction(async tx => { const existing = await tx.project_lock.findUnique({ where:{ project_id } }); if (existing && existing.expires_at > now) { - throw new Error('项目正被人编辑'); - } + throw new Error('项目正被人编辑'); + } await tx.project_lock.upsert({ where: { project_id }, @@ -232,14 +237,16 @@ await prisma.$transaction(async tx => { }); </code></pre> <p>释放锁就直接从锁表里删掉对应的数据即可:</p> -<pre><code class="ts">await prisma.project_lock.delete({ where:{ project_id } }); +<pre><code class="language-ts">await prisma.project_lock.delete({ where:{ project_id } }); </code></pre> <p>前端的话,大概就是:</p> -<p>在进入编辑前请求一下 <code>/api/project/:id/lock</code> 之类的 API,失败则提示“被占用”;<br>在 <code>onbeforeunload</code> 时执行 <code>/unlock</code>;<br>超时后后端自动允许新锁。 </p> +<p>在进入编辑前请求一下 <code>/api/project/:id/lock</code> 之类的 API,失败则提示“被占用”;<br> +在 <code>onbeforeunload</code> 时执行 <code>/unlock</code>;<br> +超时后后端自动允许新锁。</p> <hr> -<p>第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的版本号或时间戳;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。 </p> +<p>第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的版本号或时间戳;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。</p> <p>具体实现中,可以尝试在 <code>project</code> 表加上 <code>version INT NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ</code> ,然后更新项目时:</p> -<pre><code class="ts">async updateProject(parent, { id, version, input }, ctx) { +<pre><code class="language-ts">async updateProject(parent, { id, version, input }, ctx) { const result = await prisma.$executeRaw` UPDATE project SET name = ${input.name}, @@ -247,38 +254,51 @@ await prisma.$transaction(async tx => { version = version + 1, updated_at = now() WHERE id = ${id} AND version = ${version} - `; + `; if (result === 0) { - throw new ConflictException('版本冲突,请刷新后重试'); + throw new ConflictException('版本冲突,请刷新后重试'); } return prisma.project.findUnique({ where:{ id } }); } </code></pre> -<p>前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并/重新加载?” 之类的玩意儿。</p> +<p>前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并/重新加载?” 之类的玩意儿。</p> <hr> -<p>第三个方案是:操作转化(Operational Transformation,OT) </p> -<p>也就是记录用户每次的“操作”(insert/delete at position),服务器根据历史操作序列对并发操作做转化(transform),确保先到达的操作调整后再应用后到达的。 </p> -<p>有一些实现案例:<br>一、ShareDB(Node.js)<br>二、Google Docs 中的同步算法 </p> -<p>具体实现的话,可能要现在前端逐字符/块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。</p> +<p>第三个方案是:操作转化(Operational Transformation,OT)</p> +<p>也就是记录用户每次的“操作”(insert/delete at position),服务器根据历史操作序列对并发操作做转化(transform),确保先到达的操作调整后再应用后到达的。</p> +<p>有一些实现案例:<br> +一、ShareDB(Node.js)<br> +二、Google Docs 中的同步算法</p> +<p>具体实现的话,可能要现在前端逐字符/块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。</p> <hr> <p>最后可能还可以用事件溯源(Event Sourcing)+ 场景命令模式来实现:</p> -<p>不直接存状态,而是存所有“命令 / 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。 </p> -<p>例如:<br>在每次更新时推送 <code>ProjectUpdated { projectId, fieldsChanged, userId, timestamp }</code> ,<br>然后写入事件存储(如 Kafka / EventStoreDB),<br>读端 Consumer 按顺序重建最新状态或按领域聚合 ,<br>最后在并发时如果两个事件都修改了同一字段,可在写端做校验/补偿,或在读端做最后写入胜出等策略 。</p> +<p>不直接存状态,而是存所有“命令 / 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。</p> +<p>例如:<br> +在每次更新时推送 <code>ProjectUpdated { projectId, fieldsChanged, userId, timestamp }</code> ,<br> +然后写入事件存储(如 Kafka / EventStoreDB),<br> +读端 Consumer 按顺序重建最新状态或按领域聚合 ,<br> +最后在并发时如果两个事件都修改了同一字段,可在写端做校验/补偿,或在读端做最后写入胜出等策略 。</p> <hr> <p>总结来说,</p> <ul> -<li>CRDT 最擅长 去中心化、离线编辑、自动合并; </li> -<li>若不引入 CRDT,可根据业务侧重点选用: <ol> -<li>悲观锁 → 强制串行编辑,简单粗暴; </li> -<li>乐观并发 → 适合大多数业务场景,成本低; </li> -<li>OT → 适合富文本或实时协同场景,复杂度中等; </li> +<li>CRDT 最擅长 去中心化、离线编辑、自动合并;</li> +<li>若不引入 CRDT,可根据业务侧重点选用: +<ol> +<li>悲观锁 → 强制串行编辑,简单粗暴;</li> +<li>乐观并发 → 适合大多数业务场景,成本低;</li> +<li>OT → 适合富文本或实时协同场景,复杂度中等;</li> <li>事件溯源 → 适合需要全历史审计、可回放的场景。</li> </ol> </li> </ul> <hr> -<p>[^1]: Decipad 博客 “Collaborative and Offline Editing Using CRDTs”<br> <a target="_blank" rel="noopener" href="https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts">https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts</a></p> -<p>[^2]: Hacker News 讨论(CRDT 相关线程)<br> <a target="_blank" rel="noopener" href="https://news.ycombinator.com/item?id=38289327">https://news.ycombinator.com/item?id=38289327</a></p> +<hr class="footnotes-sep"> +<section class="footnotes"> +<ol class="footnotes-list"> +<li id="fn1" class="footnote-item"><p>Decipad 博客 “Collaborative and Offline Editing Using CRDTs”<br> +<a target="_blank" rel="noopener" href="https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts">https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p> +</li> +</ol> +</section> </div> diff --git a/2025/05/27/vertical-slicing-practice/index.html b/2025/05/27/vertical-slicing-practice/index.html index 98dee1bb..206a3e98 100644 --- a/2025/05/27/vertical-slicing-practice/index.html +++ b/2025/05/27/vertical-slicing-practice/index.html @@ -210,7 +210,7 @@ <p>对于登录功能,需要考虑:</p> <ul> <li>UI 层:登录表单(输入邮箱、密码的地方)、提交按钮、错误提示信息。</li> -<li>API/服务层:接收登录请求、验证用户凭证的接口。</li> +<li>API/服务层:接收登录请求、验证用户凭证的接口。</li> <li>业务逻辑层:校验输入格式、查询用户信息、验证密码、生成会话(Session)或令牌(Token)。</li> <li>数据访问层:从数据库中读取用户信息。</li> </ul> diff --git a/2025/06/30/ibs/index.html b/2025/06/30/ibs/index.html index fadd1e31..8818748d 100644 --- a/2025/06/30/ibs/index.html +++ b/2025/06/30/ibs/index.html @@ -193,29 +193,38 @@ </div> <div class="post-content"> <p>肠易激综合征 (Irritable Bowel Syndrome, IBS) 是一种常见的功能性胃肠病。其核心特征是在没有发现任何器质性病变(如炎症、溃疡或肿瘤)的情况下,患者长期经历腹痛、腹胀,并伴有排便习惯的改变(腹泻、便秘或两者交替)。</p> -<h3 id="一、-肠易激综合征的病理生理学"><a href="#一、-肠易激综合征的病理生理学" class="headerlink" title="一、 肠易激综合征的病理生理学"></a>一、 肠易激综合征的病理生理学</h3><p>IBS 的病理生理机制非常复杂,并非由单一原因导致,而是多种因素相互作用的结果。目前的医学研究认为,其主要与以下几个核心机制有关:</p> -<h4 id="1-脑-肠轴功能紊乱-Disorders-of-the-Brain-Gut-Axis"><a href="#1-脑-肠轴功能紊乱-Disorders-of-the-Brain-Gut-Axis" class="headerlink" title="1. 脑-肠轴功能紊乱 (Disorders of the Brain-Gut Axis)"></a>1. 脑-肠轴功能紊乱 (Disorders of the Brain-Gut Axis)</h4><p>这是 IBS 最核心的病理机制。大脑和肠道通过神经、内分泌和免疫系统形成一个双向沟通的“脑-肠轴”。在 IBS 患者中,这个轴的功能出现紊乱。</p> +<h3 id="一、-肠易激综合征的病理生理学"><a class="header-anchor" href="#一、-肠易激综合征的病理生理学">¶</a>一、 肠易激综合征的病理生理学</h3> +<p>IBS 的病理生理机制非常复杂,并非由单一原因导致,而是多种因素相互作用的结果。目前的医学研究认为,其主要与以下几个核心机制有关:</p> +<h4 id="1-脑-肠轴功能紊乱-Disorders-of-the-Brain-Gut-Axis"><a class="header-anchor" href="#1-脑-肠轴功能紊乱-Disorders-of-the-Brain-Gut-Axis">¶</a>1. 脑-肠轴功能紊乱 (Disorders of the Brain-Gut Axis)</h4> +<p>这是 IBS 最核心的病理机制。大脑和肠道通过神经、内分泌和免疫系统形成一个双向沟通的“脑-肠轴”。在 IBS 患者中,这个轴的功能出现紊乱。</p> <ul> <li><strong>自上而下:</strong> 心理压力、焦虑、抑郁等负面情绪可以直接通过大脑影响肠道的运动、感觉和分泌功能,从而诱发或加重症状。</li> <li><strong>自下而上:</strong> 肠道内的信号(如食物、气体、肠道菌群代谢产物)被过度放大后传给大脑,导致大脑对这些正常刺激产生过度反应,表现为疼痛和不适。</li> </ul> -<h4 id="2-内脏高敏感性-Visceral-Hypersensitivity"><a href="#2-内脏高敏感性-Visceral-Hypersensitivity" class="headerlink" title="2. 内脏高敏感性 (Visceral Hypersensitivity)"></a>2. 内脏高敏感性 (Visceral Hypersensitivity)</h4><p>这是指 IBS 患者的肠道对各种刺激(如食物、气体、肠壁的牵拉)异常敏感。正常人可以耐受的肠道内压力或活动,在 IBS 患者身上却会引发强烈的腹痛、急便感或不适。这种“放大”的感觉是 IBS 腹痛的主要原因。</p> -<h4 id="3-胃肠动力异常-Altered-Gut-Motility"><a href="#3-胃肠动力异常-Altered-Gut-Motility" class="headerlink" title="3. 胃肠动力异常 (Altered Gut Motility)"></a>3. 胃肠动力异常 (Altered Gut Motility)</h4><p>肠道肌肉的收缩和舒张节律出现问题,导致食物和粪便在肠道内的转运速度异常。</p> +<h4 id="2-内脏高敏感性-Visceral-Hypersensitivity"><a class="header-anchor" href="#2-内脏高敏感性-Visceral-Hypersensitivity">¶</a>2. 内脏高敏感性 (Visceral Hypersensitivity)</h4> +<p>这是指 IBS 患者的肠道对各种刺激(如食物、气体、肠壁的牵拉)异常敏感。正常人可以耐受的肠道内压力或活动,在 IBS 患者身上却会引发强烈的腹痛、急便感或不适。这种“放大”的感觉是 IBS 腹痛的主要原因。</p> +<h4 id="3-胃肠动力异常-Altered-Gut-Motility"><a class="header-anchor" href="#3-胃肠动力异常-Altered-Gut-Motility">¶</a>3. 胃肠动力异常 (Altered Gut Motility)</h4> +<p>肠道肌肉的收缩和舒张节律出现问题,导致食物和粪便在肠道内的转运速度异常。</p> <ul> <li><strong>转运过快:</strong> 肠道内容物过快通过结肠,水分来不及被充分吸收,导致腹泻(IBS-D)。</li> <li><strong>转运过慢:</strong> 肠道蠕动乏力,粪便在结肠内停留时间过长,水分被过度吸收,导致便秘(IBS-C)。</li> <li><strong>节律紊乱:</strong> 有时快有时慢,导致腹泻与便秘交替出现(IBS-M)。</li> </ul> -<h4 id="4-肠道微生态失调-Alteration-in-Gut-Microbiota"><a href="#4-肠道微生态失调-Alteration-in-Gut-Microbiota" class="headerlink" title="4. 肠道微生态失调 (Alteration in Gut Microbiota)"></a>4. 肠道微生态失调 (Alteration in Gut Microbiota)</h4><p>IBS 患者的肠道菌群组成和功能与健康人存在差异。有害菌可能增多,有益菌(如双歧杆菌、乳酸杆菌)可能减少。这些失调的菌群会影响肠道屏障功能、产生过多气体、并激活免疫系统,导致腹胀和腹泻。</p> -<h4 id="5-肠道低度炎症与免疫激活-Low-Grade-Inflammation-and-Immune-Activation"><a href="#5-肠道低度炎症与免疫激活-Low-Grade-Inflammation-and-Immune-Activation" class="headerlink" title="5. 肠道低度炎症与免疫激活 (Low-Grade Inflammation and Immune Activation)"></a>5. 肠道低度炎症与免疫激活 (Low-Grade Inflammation and Immune Activation)</h4><p>部分 IBS 患者的肠道黏膜中可以观察到轻微的炎症反应,例如肥大细胞等免疫细胞数量增多并被激活。这些细胞会释放组胺、5-羟色胺等物质,直接刺激肠道神经,引起疼痛和动力异常。这种情况尤其在感染性肠炎后发生的 IBS(PI-IBS)中更为常见。</p> -<h4 id="6-遗传与饮食因素"><a href="#6-遗传与饮食因素" class="headerlink" title="6. 遗传与饮食因素"></a>6. 遗传与饮食因素</h4><ul> +<h4 id="4-肠道微生态失调-Alteration-in-Gut-Microbiota"><a class="header-anchor" href="#4-肠道微生态失调-Alteration-in-Gut-Microbiota">¶</a>4. 肠道微生态失调 (Alteration in Gut Microbiota)</h4> +<p>IBS 患者的肠道菌群组成和功能与健康人存在差异。有害菌可能增多,有益菌(如双歧杆菌、乳酸杆菌)可能减少。这些失调的菌群会影响肠道屏障功能、产生过多气体、并激活免疫系统,导致腹胀和腹泻。</p> +<h4 id="5-肠道低度炎症与免疫激活-Low-Grade-Inflammation-and-Immune-Activation"><a class="header-anchor" href="#5-肠道低度炎症与免疫激活-Low-Grade-Inflammation-and-Immune-Activation">¶</a>5. 肠道低度炎症与免疫激活 (Low-Grade Inflammation and Immune Activation)</h4> +<p>部分 IBS 患者的肠道黏膜中可以观察到轻微的炎症反应,例如肥大细胞等免疫细胞数量增多并被激活。这些细胞会释放组胺、5-羟色胺等物质,直接刺激肠道神经,引起疼痛和动力异常。这种情况尤其在感染性肠炎后发生的 IBS(PI-IBS)中更为常见。</p> +<h4 id="6-遗传与饮食因素"><a class="header-anchor" href="#6-遗传与饮食因素">¶</a>6. 遗传与饮食因素</h4> +<ul> <li><strong>遗传:</strong> IBS具有一定的家族聚集性,提示可能存在遗传易感性。</li> <li><strong>饮食:</strong> 某些食物,特别是含有高<strong>FODMAPs</strong>(可发酵的寡糖、双糖、单糖和多元醇)的食物,在小肠内难以被吸收,进入大肠后被细菌快速发酵,产生大量气体并增加肠道渗透压,从而诱发腹胀、腹痛和腹泻。</li> </ul> -<h3 id="二、-肠易激综合征的药物治疗"><a href="#二、-肠易激综合征的药物治疗" class="headerlink" title="二、 肠易激综合征的药物治疗"></a>二、 肠易激综合征的药物治疗</h3><blockquote> +<h3 id="二、-肠易激综合征的药物治疗"><a class="header-anchor" href="#二、-肠易激综合征的药物治疗">¶</a>二、 肠易激综合征的药物治疗</h3> +<blockquote> <p>IBS的治疗需要个体化,所有药物都应在医生指导下使用。医生应该根据具体的 IBS 亚型(腹泻型、便秘型、混合型)和主要症状来选择最合适的药物。</p> </blockquote> -<h4 id="针对主要症状的药物"><a href="#针对主要症状的药物" class="headerlink" title="针对主要症状的药物"></a>针对主要症状的药物</h4><p><strong>1. 针对腹痛和腹胀(解痉药)</strong></p> +<h4 id="针对主要症状的药物"><a class="header-anchor" href="#针对主要症状的药物">¶</a>针对主要症状的药物</h4> +<p><strong>1. 针对腹痛和腹胀(解痉药)</strong></p> <p>这类药物通过放松肠道平滑肌,缓解肠道痉挛来减轻疼痛。</p> <ul> <li><strong>匹维溴铵 (Pinaverium Bromide):</strong> 作用于肠道平滑肌的钙离子通道,选择性高,副作用较小。</li> @@ -235,7 +244,8 @@ <ul> <li><strong>容积性泻药(如欧车前、聚卡波非钙):</strong> 通过吸收水分增加粪便体积,刺激肠道蠕动。使用时需饮用足量水,否则可能加重便秘。</li> <li><strong>渗透性泻药(如聚乙二醇 PEG、乳果糖):</strong> 通过在肠道内形成高渗环境,将水分保留在肠腔内,软化粪便,促进排便。相对安全,适合长期使用。</li> -<li><strong>促分泌药:</strong><ul> +<li><strong>促分泌药:</strong> +<ul> <li><strong>鲁比前列酮 (Lubiprostone):</strong> 激活肠道氯离子通道,增加肠液分泌,软化粪便。</li> <li><strong>利那洛肽 (Linaclotide)、普卡那肽 (Plecanatide):</strong> 激活鸟苷酸环化酶-C,促进肠液分泌,并能一定程度上缓解腹痛。</li> </ul> diff --git a/2025/07/08/dadgad/index.html b/2025/07/08/dadgad/index.html index a1c061b4..044a3a33 100644 --- a/2025/07/08/dadgad/index.html +++ b/2025/07/08/dadgad/index.html @@ -194,9 +194,10 @@ <div class="post-content"> <p>模态定弦的空弦音不直接构成传统意义上的大调或小调和弦,而是形成一种听感上更为空灵、悬浮的“挂留和弦”(Suspended Chord)效果。</p> <p><strong>DADGAD (发音为 “dad-gad”):</strong> 这是凯尔特音乐(Celtic Music)中最具代表性的定弦。它的空弦音给人一种既非大调也非小调的模糊感,充满了神秘和悠远的色彩,非常适合营造氛围音乐和复杂的指弹旋律。</p> -<p>挂留和弦(Suspended Chord),通常简写为 <strong>“sus”<strong>,是一种在传统和弦结构上进行了巧妙“修改”的和弦。它的核心特点是:</strong>用一个非三度的音,去“替代”或“悬挂”了原本决定和弦大/小调色彩的三度音</strong>,从而创造出一种独特的声音效果。</p> +<p>挂留和弦(Suspended Chord),通常简写为 <strong>“sus”</strong>,是一种在传统和弦结构上进行了巧妙“修改”的和弦。它的核心特点是:<strong>用一个非三度的音,去“替代”或“悬挂”了原本决定和弦大/小调色彩的三度音</strong>,从而创造出一种独特的声音效果。</p> <p>想象一个标准的大三和弦或小三和弦,它由<strong>根音、三度音和五度音</strong>构成。其中,<strong>三度音</strong>是决定这个和弦听起来是明亮、开心的“大调”,还是忧郁、伤感的“小调”的关键。</p> -<p>而挂留和弦,就是把这个关键的“三度音”暂时拿掉,换上别的音。最常见的替代者有两个:<br>一是 <strong>纯四度音 (Perfect 4th)</strong> ,二是 <strong>大二度音 (Major 2nd)</strong> 。</p> +<p>而挂留和弦,就是把这个关键的“三度音”暂时拿掉,换上别的音。最常见的替代者有两个:<br> +一是 <strong>纯四度音 (Perfect 4th)</strong> ,二是 <strong>大二度音 (Major 2nd)</strong> 。</p> <p><strong>sus4 和弦 (挂四和弦)</strong> 由 <strong>根音 (1) + 纯四度音 (4) + 纯五度音 (5)</strong> 构成,即用<strong>四度音</strong>替代了原本的三度音。听感上,这是最常见的挂留和弦。它创造出一种<strong>强烈而悬浮的张力感</strong>,声音既不属于大调也不属于小调,听起来非常“开阔”。例如,一个 <strong>Csus4</strong> 和弦,就是由 C、F、G 三个音构成,取代了标准C大调和弦(C-E-G)中的 E 音。</p> <p><strong>sus2 和弦 (挂二和弦)</strong> 由 <strong>根音 (1) + 大二度音 (2) + 纯五度音 (5)</strong> 构成,特点是用二度音替代了原本的三度音。听感上,sus2和弦的声音比sus4更柔和、更明亮一些。它同样具有开放、模糊的色彩,但张力感稍弱,听起来更像是一种平静、梦幻的色彩和弦。例如,一个 <strong>Csus2</strong> 和弦,就是由 C、D、G 三个音构成,取代了标准C大调和弦(C-E-G)中的 E 音。</p> <p>而 DADGAD 定弦方式的空弦音,其核心就是一个挂留和弦,具体来说是一个 Dsus4 和弦。当你弹响 DADGAD 的所有空弦时,你实际上听到的就是 <strong>D、G、A</strong> 这三个音在不同八度上的叠加和共鸣。这三个音不多不少,正好构成了 <strong>Dsus4</strong> 和弦。</p> diff --git a/2025/07/11/fuck-hcho/index.html b/2025/07/11/fuck-hcho/index.html index a3d153a1..6692c0f5 100644 --- a/2025/07/11/fuck-hcho/index.html +++ b/2025/07/11/fuck-hcho/index.html @@ -192,11 +192,12 @@ </div> </div> <div class="post-content"> - <p>前几天和群友聊到甲醛相关的东东,有点好奇甲醛进入人体之后的一系列反应,于逝,<del>我去吸了两口</del> ,先来看看一些无聊的总结:</p> + <p>前几天和群友聊到甲醛相关的东东,有点好奇甲醛进入人体之后的一系列反应,于逝,<s>我去吸了两口</s> ,先来看看一些无聊的总结:</p> <p>吸入甲醛后,人体之所以会产生眼部灼痛、喉咙刺痒、呼吸困难等一系列强烈的不适感,是因为甲醛作为一种高活性、强刺激性的化学物质,对我们的身体构成了从宏观到微观的多层次、多途径的直接攻击。其影响方式主要可以归结为两大方面:<strong>直接的刺激与腐蚀作用</strong>和<strong>深层的细胞毒性与遗传毒性</strong>。</p> <p>当甲醛气体通过呼吸进入人体,首当其冲的是我们的眼睛和呼吸道黏膜。这些部位的细胞直接暴露在甲醛的攻击下,引发一系列即时且强烈的不适症状:</p> <ul> -<li><strong>对黏膜的强烈刺激:</strong> 甲醛具有极强的亲水性和反应活性,能迅速与眼睛结膜、鼻腔、咽喉和气管等部位黏膜表面的蛋白质和水分发生反应。这种反应会直接损伤黏膜组织,导致神经末梢受到强烈刺激。<ul> +<li><strong>对黏膜的强烈刺激:</strong> 甲醛具有极强的亲水性和反应活性,能迅速与眼睛结膜、鼻腔、咽喉和气管等部位黏膜表面的蛋白质和水分发生反应。这种反应会直接损伤黏膜组织,导致神经末梢受到强烈刺激。 +<ul> <li><strong>眼部症状:</strong> 眼睛会感到灼烧、刺痛、发痒和不自主流泪。</li> <li><strong>呼吸道症状:</strong> 喉咙会感到干燥、刺痛、咳嗽,高浓度下更会引发胸闷、气喘,甚至喉头水肿和肺水肿,导致呼吸极度困难。</li> </ul> @@ -207,66 +208,82 @@ <ul> <li><strong>蛋白质变性:</strong> 甲醛能够与构成生命基础的蛋白质中的氨基(-NH2)发生反应,形成“亚甲基桥”,破坏蛋白质原有的三维结构,使其失去正常的生理功能。细胞的功能依赖于各种蛋白质(酶、结构蛋白等)的正常工作,蛋白质变性意味着细胞代谢紊乱、功能障碍,甚至死亡。这在本质上是一种“腐蚀”作用,也是福尔马林(甲醛水溶液)能用作防腐剂的根本原因。</li> <li><strong>诱导细胞凋亡与氧化应激:</strong> 研究表明,甲醛会损伤细胞内的能量工厂——线粒体,诱导细胞产生大量的活性氧(ROS)。过量的活性氧会破坏细胞膜、蛋白质和DNA,引发“氧化应激”反应,并最终启动细胞的程序性死亡(凋亡)机制。</li> -<li><strong>遗传毒性与致癌性:</strong> 甲醛最具危害性的影响在于其遗传毒性。它可以穿过细胞核,与遗传物质DNA和组蛋白(包裹DNA的蛋白质)形成“加合物”,这会干扰DNA的正常复制和修复过程,导致基因突变。<ul> +<li><strong>遗传毒性与致癌性:</strong> 甲醛最具危害性的影响在于其遗传毒性。它可以穿过细胞核,与遗传物质DNA和组蛋白(包裹DNA的蛋白质)形成“加合物”,这会干扰DNA的正常复制和修复过程,导致基因突变。 +<ul> <li><strong>致癌风险:</strong> 当这种基因损伤发生在关键的癌基因或抑癌基因上,并且人体的修复机制无法有效清除时,就可能导致细胞的异常增殖,最终引发癌症。世界卫生组织(WHO)下属的国际癌症研究机构(IARC)已将甲醛列为<strong>一类致癌物</strong>,有充分证据表明其与鼻咽癌、白血病等恶性肿瘤的发生密切相关。</li> </ul> </li> <li><strong>神经与免疫毒性:</strong> 甲醛还能对中枢神经系统产生毒性作用,导致头晕、头痛、记忆力减退、失眠等症状。同时,它也可能干扰免疫系统的正常功能,使人更容易过敏(如引发过敏性皮炎、哮喘)或抵抗力下降。</li> </ul> <hr> -<h2 id="甲醛与粘膜表面的蛋白质和水分发生反应?"><a href="#甲醛与粘膜表面的蛋白质和水分发生反应?" class="headerlink" title="甲醛与粘膜表面的蛋白质和水分发生反应?"></a>甲醛与粘膜表面的蛋白质和水分发生反应?</h2><p>当甲醛(HCHO)这个小而活泼的分子,遇到我们湿润的眼睛、鼻腔和喉咙黏膜时,它首先会迅速溶于黏膜表面的水分,形成甲二醇 (CH₂(OH)₂) 。这个形态的甲醛会将目标转向 <strong>蛋白质</strong>。</p> +<h2 id="甲醛与粘膜表面的蛋白质和水分发生反应?"><a class="header-anchor" href="#甲醛与粘膜表面的蛋白质和水分发生反应?">¶</a>甲醛与粘膜表面的蛋白质和水分发生反应?</h2> +<p>当甲醛(HCHO)这个小而活泼的分子,遇到我们湿润的眼睛、鼻腔和喉咙黏膜时,它首先会迅速溶于黏膜表面的水分,形成甲二醇 (CH₂(OH)₂) 。这个形态的甲醛会将目标转向 <strong>蛋白质</strong>。</p> <p>这个反应并非简单的接触,而是一种被称为“交联反应”(Cross-linking)的化学过程。具体来说:甲醛分子会精准地找到蛋白质分子链上的氨基(-NH₂),这是许多氨基酸的共同特征。接着与蛋白质的氨基发生反应,形成一个不稳定的中间体。这个中间体可以进一步与邻近的另一个蛋白质的氨基反应,从而在两个原本独立的蛋白质(或同一个蛋白质的不同部分)之间,强行架起一座“亚甲基桥”(-CH₂-)。</p> <p>蛋白质就像一团经过精确折叠、有着特定三维结构的精密机器,它的功能完全依赖于这个构型。而甲醛的“交联”反应,就像是往这台精密机器的各个活动关节之间,甚至和旁边的机器之间倒 502,给它们粘死了。</p> -<h3 id="为什么甲醛分子能精准地找到蛋白质分子链上的氨基?"><a href="#为什么甲醛分子能精准地找到蛋白质分子链上的氨基?" class="headerlink" title="为什么甲醛分子能精准地找到蛋白质分子链上的氨基?"></a>为什么甲醛分子能精准地找到蛋白质分子链上的氨基?</h3><p>“精准”这两个字,听起来像是一个有思想、有目标的动作,但实际上是一场由分子结构和电荷分布主导的化学反应。</p> -<p>甲醛分子(HCHO)的核心是一个<strong>羰基(C=O)</strong>。在这个结构中,氧原子(O)是一个电负性非常强的元素,它对电子有着极强的吸引力。它会大力地将碳氧双键中的共用电子云拉向自己。这种电子云的偏移,导致了与它相连的碳原子(C)周围的电子变得非常稀薄,从而带上了部分正电荷(δ+)。</p> +<h3 id="为什么甲醛分子能精准地找到蛋白质分子链上的氨基?"><a class="header-anchor" href="#为什么甲醛分子能精准地找到蛋白质分子链上的氨基?">¶</a>为什么甲醛分子能精准地找到蛋白质分子链上的氨基?</h3> +<p>“精准”这两个字,听起来像是一个有思想、有目标的动作,但实际上是一场由分子结构和电荷分布主导的化学反应。</p> +<p>甲醛分子(HCHO)的核心是一个<strong>羰基(C=O)</strong>。在这个结构中,氧原子(O)是一个电负性非常强的元素,它对电子有着极强的吸引力。它会大力地将碳氧双键中的共用电子云拉向自己。这种电子云的偏移,导致了与它相连的碳原子(C)周围的电子变得非常稀薄,从而带上了部分正电荷(δ+)。</p> <p>在化学上,这样一个“缺少电子”、对电子充满渴望的原子或基团,被称为“亲电体”(Electrophile)。</p> <p>氨基(-NH₂)在蛋白质中广泛存在,尤其是在赖氨酸、精氨酸等氨基酸的侧链上。氨基的核心是氮原子(N)。根据它的电子排布,氮原子的最外层有一对没有参与形成化学键的电子,这叫做 “孤对电子”(Lone Pair Electrons)。 这对孤对电子,使得氮原子成了一个电子云密度很高的区域,带上了<strong>部分负电荷(δ-)</strong>。</p> <p>这样一个拥有“富余”电子、并愿意用它去攻击其他正电中心的原子或基团,在化学上被称为 “亲核体”(Nucleophile)。</p> <p>当甲醛分子靠近蛋白质时,甲醛分子上带部分正电荷的碳原子(亲电体),与蛋白质氨基上带部分负电荷的氮原子(亲核体),会因基本的静电引力而相互靠近。一旦距离足够近,氨基上慷慨的“孤对电子”会毫不犹豫地“攻击”甲醛上那个渴望电子的碳原子,并与它形成一个新的化学键。</p> <p>这个过程,就是化学中的“亲核加成反应”。</p> -<h3 id="这个反应为何会损伤黏膜组织?"><a href="#这个反应为何会损伤黏膜组织?" class="headerlink" title="这个反应为何会损伤黏膜组织?"></a>这个反应为何会损伤黏膜组织?</h3><p>蛋白质被甲醛“交联” 这个过程在化学上称为 “蛋白质变性”,它直接导致了组织损伤。</p> +<h3 id="这个反应为何会损伤黏膜组织?"><a class="header-anchor" href="#这个反应为何会损伤黏膜组织?">¶</a>这个反应为何会损伤黏膜组织?</h3> +<p>蛋白质被甲醛“交联” 这个过程在化学上称为 “蛋白质变性”,它直接导致了组织损伤。</p> <p>首先黏膜细胞需要各种酶(本身也是蛋白质)来维持新陈代谢。被交联后,这些酶的结构被破坏,失去了活性,细胞的正常生理活动瞬间停滞。而细胞的骨架、细胞膜上的离子通道等结构蛋白,同样会被交联破坏。这导致细胞失去原有的形态,细胞膜的通透性被改变,甚至直接破裂。当大量的细胞功能瘫痪、结构崩解后,它们会迅速走向死亡。成片的细胞死亡,就意味着我们宏观可见的<strong>组织损伤</strong>。这本质上是一种<strong>微观的化学性灼伤</strong>。</p> <p>这也是为什么福尔马林(甲醛水溶液)能用来制作标本——它通过“交联”反应,将生物组织的所有蛋白质“固化”,使其永不腐败,但这对于活体组织而言,就是彻头彻尾的毁灭。</p> -<h3 id="为何神经末梢会受到强烈刺激?"><a href="#为何神经末梢会受到强烈刺激?" class="headerlink" title="为何神经末梢会受到强烈刺激?"></a>为何神经末梢会受到强烈刺激?</h3><p>我们的黏膜组织下,密布着丰富的神经末梢。</p> +<h3 id="为何神经末梢会受到强烈刺激?"><a class="header-anchor" href="#为何神经末梢会受到强烈刺激?">¶</a>为何神经末梢会受到强烈刺激?</h3> +<p>我们的黏膜组织下,密布着丰富的神经末梢。</p> <p>完好的黏膜上皮细胞是神经末梢的保护层。当这层保护被甲醛破坏,下方的神经末梢就直接暴露了出来。此时,高活性的甲醛分子可以直接攻击这些裸露的神经末梢,就像用化学品直接触碰神经一样,瞬间产生剧烈的刺激信号。</p> <p>被损伤和死亡的细胞,在“弥留之际”会释放出大量的化学信号物质,如前列腺素、缓激肽、组胺等。这些物质被称为“炎症介质”。而这些炎症介质本身,就是神经末梢上痛觉感受器的强效激活剂。它们会放大并持续地刺激神经,导致灼痛、瘙痒和刺痛感。</p> -<h3 id="什么是活性氧?"><a href="#什么是活性氧?" class="headerlink" title="什么是活性氧?"></a>什么是活性氧?</h3><p><strong>活性氧(Reactive Oxygen Species, ROS)</strong> 是一类含氧的、化学性质极其活泼的分子或自由基的总称。常见的活性氧有 超氧阴离子(O₂⁻•)、羟自由基(•OH)、过氧化氢(H₂O₂)等。它们的特点之一是其都带有“未配对的电子”,这使得它们在化学上极不稳定,迫切地想要从其他分子那里“抢夺”电子来让自己稳定下来。</p> -<p> 在正常情况下,我们身体会产生少量活性氧。它们是重要的信号分子,参与调节细胞生长、分化,并且是免疫细胞(如巨噬细胞)用来消灭入侵病原体(细菌、病毒)的“化学武器”。</p> -<p> 一旦活性氧的产生<strong>远远超过</strong>了细胞自身的清除能力(细胞内有超氧化物歧化酶SOD等“警察”来控制它们),就会形成“<strong>氧化应激</strong>”(Oxidative Stress)状态。这些失控的“小流氓”就会在细胞内肆意破坏,攻击一切它们遇到的分子。</p> -<h3 id="甲醛如何诱导线粒体产生大量活性氧?"><a href="#甲醛如何诱导线粒体产生大量活性氧?" class="headerlink" title="甲醛如何诱导线粒体产生大量活性氧?"></a>甲醛如何诱导线粒体产生大量活性氧?</h3><p>线粒体是通过一个叫做“电子传递链”的复杂过程来产生能量(ATP)的。就像一条生产线,电子(e⁻)在这条生产线上被一步步传递,最终与我们吸入的氧气(O₂)结合生成水(H₂O),同时释放出大量能量。</p> +<h3 id="什么是活性氧?"><a class="header-anchor" href="#什么是活性氧?">¶</a>什么是活性氧?</h3> +<p><strong>活性氧(Reactive Oxygen Species, ROS)</strong> 是一类含氧的、化学性质极其活泼的分子或自由基的总称。常见的活性氧有 超氧阴离子(O₂⁻•)、羟自由基(•OH)、过氧化氢(H₂O₂)等。它们的特点之一是其都带有“未配对的电子”,这使得它们在化学上极不稳定,迫切地想要从其他分子那里“抢夺”电子来让自己稳定下来。</p> +<p>在正常情况下,我们身体会产生少量活性氧。它们是重要的信号分子,参与调节细胞生长、分化,并且是免疫细胞(如巨噬细胞)用来消灭入侵病原体(细菌、病毒)的“化学武器”。</p> +<p>一旦活性氧的产生<strong>远远超过</strong>了细胞自身的清除能力(细胞内有超氧化物歧化酶SOD等“警察”来控制它们),就会形成“<strong>氧化应激</strong>”(Oxidative Stress)状态。这些失控的“小流氓”就会在细胞内肆意破坏,攻击一切它们遇到的分子。</p> +<h3 id="甲醛如何诱导线粒体产生大量活性氧?"><a class="header-anchor" href="#甲醛如何诱导线粒体产生大量活性氧?">¶</a>甲醛如何诱导线粒体产生大量活性氧?</h3> +<p>线粒体是通过一个叫做“电子传递链”的复杂过程来产生能量(ATP)的。就像一条生产线,电子(e⁻)在这条生产线上被一步步传递,最终与我们吸入的氧气(O₂)结合生成水(H₂O),同时释放出大量能量。</p> <p>而甲醛及其代谢产物(如甲酸)会直接损伤电子传递链上的复合体蛋白(特别是复合体I和III)。这就像是破坏了生产线上的关键机器。</p> <p>当生产线上的机器受损或堵塞时,本该被有序传递的电子就会从传递链上“泄漏”出来。</p> <p>这些泄漏出来的高能电子,会直接传递给旁边无辜的氧气分子(O₂)。正常的氧气分子得到一个额外的电子后,就成为了极不稳定的<strong>超氧阴离子(O₂⁻•)</strong> (活性氧)。</p> -<h3 id="为什么活性氧能破坏细胞膜、蛋白质和DNA?"><a href="#为什么活性氧能破坏细胞膜、蛋白质和DNA?" class="headerlink" title="为什么活性氧能破坏细胞膜、蛋白质和DNA?"></a>为什么活性氧能破坏细胞膜、蛋白质和DNA?</h3><p>失控的活性氧会疯狂地从周围的生物大分子上抢夺电子,这个过程会导致这些大分子的化学结构被破坏,失去原有功能。</p> +<h3 id="为什么活性氧能破坏细胞膜、蛋白质和DNA?"><a class="header-anchor" href="#为什么活性氧能破坏细胞膜、蛋白质和DNA?">¶</a>为什么活性氧能破坏细胞膜、蛋白质和DNA?</h3> +<p>失控的活性氧会疯狂地从周围的生物大分子上抢夺电子,这个过程会导致这些大分子的化学结构被破坏,失去原有功能。</p> <p>细胞膜主要由<strong>磷脂双分子层</strong>构成,其中含有大量的<strong>不饱和脂肪酸</strong>。活性氧(特别是羟自由基•OH)会攻击不饱和脂肪酸中的双键,抢走一个电子,引发一个叫做“脂质过氧化”的链式反应。脂质过氧化会破坏细胞膜的流动性和完整性,使其变得僵硬、脆裂,最终导致细胞膜破裂,细胞内容物泄漏。</p> -<p> 而活性氧会攻击蛋白质侧链上的氨基酸(特别是半胱氨酸、甲硫氨酸等),使其氧化、交联(和甲醛的作用类似但原理不同),导致蛋白质变性,失去作为酶、结构蛋白或信号分子的功能。</p> -<p> 活性氧还会攻击DNA碱基(特别是鸟嘌呤G)和脱氧核糖骨架,导致<strong>碱基修饰、DNA单链或双链断裂</strong>。这会直接导致基因突变,干扰DNA的复制和转录,是诱发癌症的重要原因。</p> -<h3 id="什么是细胞的程序性死亡?"><a href="#什么是细胞的程序性死亡?" class="headerlink" title="什么是细胞的程序性死亡?"></a>什么是细胞的程序性死亡?</h3><p><strong>细胞程序性死亡(Programmed Cell Death)</strong>,最主要的形式是<strong>细胞凋亡(Apoptosis)</strong>。这并非细胞因损伤而发生的被动解体(那是“坏死”),而是细胞在接收到特定信号后,主动启动的一套基因编码的“自杀程序”。目的是清除体内不再需要、衰老或受到严重损伤(如无法修复的DNA损伤)的细胞,从而维持组织器官的稳定和健康。这是一个对机体有利的主动行为。</p> +<p>而活性氧会攻击蛋白质侧链上的氨基酸(特别是半胱氨酸、甲硫氨酸等),使其氧化、交联(和甲醛的作用类似但原理不同),导致蛋白质变性,失去作为酶、结构蛋白或信号分子的功能。</p> +<p>活性氧还会攻击DNA碱基(特别是鸟嘌呤G)和脱氧核糖骨架,导致<strong>碱基修饰、DNA单链或双链断裂</strong>。这会直接导致基因突变,干扰DNA的复制和转录,是诱发癌症的重要原因。</p> +<h3 id="什么是细胞的程序性死亡?"><a class="header-anchor" href="#什么是细胞的程序性死亡?">¶</a>什么是细胞的程序性死亡?</h3> +<p><strong>细胞程序性死亡(Programmed Cell Death)</strong>,最主要的形式是<strong>细胞凋亡(Apoptosis)</strong>。这并非细胞因损伤而发生的被动解体(那是“坏死”),而是细胞在接收到特定信号后,主动启动的一套基因编码的“自杀程序”。目的是清除体内不再需要、衰老或受到严重损伤(如无法修复的DNA损伤)的细胞,从而维持组织器官的稳定和健康。这是一个对机体有利的主动行为。</p> <p>在这个过程中每,细胞会主动收缩、染色质固缩、细胞核碎裂,最终形成多个被完整细胞膜包裹的“凋亡小体”,等待被免疫细胞吞噬清理。整个过程非常“干净”,不会引发周围的炎症。</p> -<p>在甲醛诱导的氧化应激中,由活性氧造成的大量、无法修复的DNA损伤和蛋白质损伤,本身就是最强烈的“死亡信号”。这个信号会激活细胞内一类叫做Bax和Bak的“促凋亡蛋白”。被激活的Bax/Bak蛋白会聚集到线粒体外膜上,像一个“打孔器”一样,在线粒体外膜上形成孔道。线粒体外膜被打孔后,原本储存在线粒体内部的一种关键蛋白——<strong>细胞色素C(Cytochrome C)</strong>,就会被释放到细胞质中。释放到细胞质中的细胞色素C,会激活一系列被称为 <strong>Caspase</strong> 的蛋白酶。它们一旦被激活,就会形成一个级联反应,像多米诺骨牌一样,一个激活下一个。被激活的Caspase会系统性地降解细胞内的关键蛋白质和DNA,最终导致细胞有序地解体,完成凋亡。</p> +<p>在甲醛诱导的氧化应激中,由活性氧造成的大量、无法修复的DNA损伤和蛋白质损伤,本身就是最强烈的“死亡信号”。这个信号会激活细胞内一类叫做Bax和Bak的“促凋亡蛋白”。被激活的Bax/Bak蛋白会聚集到线粒体外膜上,像一个“打孔器”一样,在线粒体外膜上形成孔道。线粒体外膜被打孔后,原本储存在线粒体内部的一种关键蛋白——<strong>细胞色素C(Cytochrome C)</strong>,就会被释放到细胞质中。释放到细胞质中的细胞色素C,会激活一系列被称为 <strong>Caspase</strong> 的蛋白酶。它们一旦被激活,就会形成一个级联反应,像多米诺骨牌一样,一个激活下一个。被激活的Caspase会系统性地降解细胞内的关键蛋白质和DNA,最终导致细胞有序地解体,完成凋亡。</p> <p><strong>“激活”在这里指的是一个剧烈的“构象变化”(Conformational Change)</strong>,即蛋白质从一种三维折叠形态,转变为另一种形态。</p> -<h4 id="什么是Bax-Bak蛋白?"><a href="#什么是Bax-Bak蛋白?" class="headerlink" title="什么是Bax/Bak蛋白?"></a>什么是Bax/Bak蛋白?</h4><p><strong>Bax和Bak</strong>是细胞内一个庞大家族——<strong>Bcl-2蛋白家族</strong>中的两个关键成员。Bax和Bak是在这里具有代表性的促凋亡派(Pro-apoptotic),Bcl-2、Bcl-xL 是 <strong>抗凋亡派(Anti-apoptotic):</strong> </p> +<h4 id="什么是Bax-Bak蛋白?"><a class="header-anchor" href="#什么是Bax-Bak蛋白?">¶</a>什么是Bax/Bak蛋白?</h4> +<p><strong>Bax和Bak</strong>是细胞内一个庞大家族——<strong>Bcl-2蛋白家族</strong>中的两个关键成员。Bax和Bak是在这里具有代表性的促凋亡派(Pro-apoptotic),Bcl-2、Bcl-xL 是 <strong>抗凋亡派(Anti-apoptotic):</strong></p> <p>在正常健康的细胞中,它们处于一种<strong>无害的、折叠起来的休眠状态</strong>。<strong>Bax</strong>大部分时间游荡在细胞质中。<strong>Bak</strong> 则停靠在线粒体的外膜上,但同样处于被抑制的休眠状态。</p> <p>当细胞遭受了大量、无法修复的DNA或蛋白质损伤时,这个信号会被细胞内的 <strong>p53蛋白</strong> 感知到。被激活的p53蛋白,会生产大量的一类被称为 “仅含BH3结构域蛋白”(BH3-only proteins)的小蛋白,例如 PUMA 和 Noxa。</p> -<h4 id="Bax-Bak-蛋白如何打孔?"><a href="#Bax-Bak-蛋白如何打孔?" class="headerlink" title="Bax/Bak 蛋白如何打孔?"></a>Bax/Bak 蛋白如何打孔?</h4><p>当Bax被激活后,它会暴露内部的<strong>疏水结构域</strong>。这个结构域极度“讨厌”水(细胞质的主要成分是水),而非常“喜欢”油性的环境。线粒体外膜正是一个由磷脂双分子层构成的“油性”环境。 因此,被激活的Bax会像一滴油会自动融入另一滴油一样,<strong>自发地从细胞质中转移并插入到线粒体外膜上</strong>。而已在膜上的Bak,被激活后则会更深、更稳定地嵌入膜中。</p> -<p>这是一个寡聚化(Oligomerization)过程。多个被激活的Bax/Bak分子在膜上相遇,会像乐高积木一样,通过彼此暴露出来的结构域互相识别并拼接在一起,形成大小不一的聚合体(Oligomer),有的像线形,有的像弧形,有的像不完整的环。这些Bax/Bak聚合体并<strong>不是</strong>贯穿膜形成一个隧道。相反,它们像楔子一样,大量地、浅浅地插入到线粒体外膜的<strong>外层</strong>。当越来越多的蛋白质“楔子”挤进这一层薄薄的膜时,会产生巨大的<strong>物理张力和弯曲应力</strong>。</p> -<p> 最终,线粒体外膜因为无法承受这种内部应力,就像一个被过度拉伸的气球一样被撕裂,形成的“孔道”或“裂口”,其边缘是由蛋白质和脂质分子共同构成的,被称为 “蛋白-脂质孔道”(Proteolipidic Pore)。</p> -<h4 id="为什么“严重损伤”会激活p53蛋白?"><a href="#为什么“严重损伤”会激活p53蛋白?" class="headerlink" title="为什么“严重损伤”会激活p53蛋白?"></a>为什么“严重损伤”会激活p53蛋白?</h4><p>在正常、安逸的细胞中,p53蛋白一被生产出来,就会被一个叫做 <strong>MDM2</strong> 的蛋白“盯上”。MDM2会给p53贴上一个叫做 “泛素”(Ubiquitin)的标签,被贴上标签的p53会被送到细胞的“垃圾处理厂”——<strong>蛋白酶体</strong>中迅速降解。因此,正常细胞里的p53蛋白水平极低。</p> +<h4 id="Bax-Bak-蛋白如何打孔?"><a class="header-anchor" href="#Bax-Bak-蛋白如何打孔?">¶</a>Bax/Bak 蛋白如何打孔?</h4> +<p>当Bax被激活后,它会暴露内部的<strong>疏水结构域</strong>。这个结构域极度“讨厌”水(细胞质的主要成分是水),而非常“喜欢”油性的环境。线粒体外膜正是一个由磷脂双分子层构成的“油性”环境。 因此,被激活的Bax会像一滴油会自动融入另一滴油一样,<strong>自发地从细胞质中转移并插入到线粒体外膜上</strong>。而已在膜上的Bak,被激活后则会更深、更稳定地嵌入膜中。</p> +<p>这是一个寡聚化(Oligomerization)过程。多个被激活的Bax/Bak分子在膜上相遇,会像乐高积木一样,通过彼此暴露出来的结构域互相识别并拼接在一起,形成大小不一的聚合体(Oligomer),有的像线形,有的像弧形,有的像不完整的环。这些Bax/Bak聚合体并<strong>不是</strong>贯穿膜形成一个隧道。相反,它们像楔子一样,大量地、浅浅地插入到线粒体外膜的<strong>外层</strong>。当越来越多的蛋白质“楔子”挤进这一层薄薄的膜时,会产生巨大的<strong>物理张力和弯曲应力</strong>。</p> +<p>最终,线粒体外膜因为无法承受这种内部应力,就像一个被过度拉伸的气球一样被撕裂,形成的“孔道”或“裂口”,其边缘是由蛋白质和脂质分子共同构成的,被称为 “蛋白-脂质孔道”(Proteolipidic Pore)。</p> +<h4 id="为什么“严重损伤”会激活p53蛋白?"><a class="header-anchor" href="#为什么“严重损伤”会激活p53蛋白?">¶</a>为什么“严重损伤”会激活p53蛋白?</h4> +<p>在正常、安逸的细胞中,p53蛋白一被生产出来,就会被一个叫做 <strong>MDM2</strong> 的蛋白“盯上”。MDM2会给p53贴上一个叫做 “泛素”(Ubiquitin)的标签,被贴上标签的p53会被送到细胞的“垃圾处理厂”——<strong>蛋白酶体</strong>中迅速降解。因此,正常细胞里的p53蛋白水平极低。</p> <p>当 “严重损伤”<strong>,尤其是</strong>DNA双链断裂(这被视为细胞最危急的损伤之一)发生时,DNA断裂处会立刻吸引并激活一批蛋白,其中最关键的是一类激酶(Kinase),比如<strong>ATM</strong>和<strong>ATR</strong>。激酶是一种能给其他蛋白质“安装”上磷酸基团(-PO₃²⁻)的工具酶。</p> -<p>被激活的ATM/ATR激酶,会立刻找到 MDM2,并给它装上好几个磷酸基团。被磷酸化后的MDM2结构发生改变,无法再识别和结合p53。 与此同时,ATM/ATR 等激酶也会给 p53 蛋白本身装上磷酸基团。这个过程不仅让p53无法被MDM2识别,更重要的是,它改变了p53的构象。</p> -<p>对于p53来说,最基础的激活,就是摆脱 MDM2 的控制,不再被降解。这使得 p53 蛋白可以在细胞核内大量、稳定地积累起来,被磷酸化等一系列化学修饰后,p53蛋白的三维结构会发生精妙的变化。这种变化会暴露p53内部一个关键的功能区——<strong>“DNA结合域”</strong>。同时,它会促使四个p53蛋白分子结合在一起,形成一个<strong>四聚体(Tetramer)</strong>。 </p> -<h5 id="为什么-MDM2-会盯上-p53?"><a href="#为什么-MDM2-会盯上-p53?" class="headerlink" title="为什么 MDM2 会盯上 p53?"></a>为什么 MDM2 会盯上 p53?</h5><p>在MDM2蛋白的N端(头部),有一个精心构造的、深邃的<strong>疏水性口袋(hydrophobic pocket)</strong>,就像手套一样。而在p53蛋白的N端(反式激活域),有一段α-螺旋区域。这段螺旋上有三个关键的氨基酸残基——<strong>苯丙氨酸(Phe19)、色氨酸(Trp23)和亮氨酸(Leu26)</strong>。这三个氨基酸像三根关键的“手指”,完美地伸入并契合MDM2的那个手套中。</p> -<h5 id="p53被激活后如何“下令”生产PUMA等蛋白?"><a href="#p53被激活后如何“下令”生产PUMA等蛋白?" class="headerlink" title="p53被激活后如何“下令”生产PUMA等蛋白?"></a>p53被激活后如何“下令”生产PUMA等蛋白?</h5><p> PUMA这个蛋白,它的“设计图纸”编码在一段叫做 _PUMA_(也称 _BBC3_)的基因里。在这段基因的上游,有一段特殊的DNA序列,叫做 “启动子”(Promoter)。而PUMA基因的启动子上,有 p53 蛋白能够识别并结合的特定“停泊位点”,称为 “p53响应元件”(p53 Response Element)。</p> +<p>被激活的ATM/ATR激酶,会立刻找到 MDM2,并给它装上好几个磷酸基团。被磷酸化后的MDM2结构发生改变,无法再识别和结合p53。 与此同时,ATM/ATR 等激酶也会给 p53 蛋白本身装上磷酸基团。这个过程不仅让p53无法被MDM2识别,更重要的是,它改变了p53的构象。</p> +<p>对于p53来说,最基础的激活,就是摆脱 MDM2 的控制,不再被降解。这使得 p53 蛋白可以在细胞核内大量、稳定地积累起来,被磷酸化等一系列化学修饰后,p53蛋白的三维结构会发生精妙的变化。这种变化会暴露p53内部一个关键的功能区——<strong>“DNA结合域”</strong>。同时,它会促使四个p53蛋白分子结合在一起,形成一个<strong>四聚体(Tetramer)</strong>。</p> +<h5 id="为什么-MDM2-会盯上-p53?"><a class="header-anchor" href="#为什么-MDM2-会盯上-p53?">¶</a>为什么 MDM2 会盯上 p53?</h5> +<p>在MDM2蛋白的N端(头部),有一个精心构造的、深邃的<strong>疏水性口袋(hydrophobic pocket)</strong>,就像手套一样。而在p53蛋白的N端(反式激活域),有一段α-螺旋区域。这段螺旋上有三个关键的氨基酸残基——<strong>苯丙氨酸(Phe19)、色氨酸(Trp23)和亮氨酸(Leu26)</strong>。这三个氨基酸像三根关键的“手指”,完美地伸入并契合MDM2的那个手套中。</p> +<h5 id="p53被激活后如何“下令”生产PUMA等蛋白?"><a class="header-anchor" href="#p53被激活后如何“下令”生产PUMA等蛋白?">¶</a>p53被激活后如何“下令”生产PUMA等蛋白?</h5> +<p>PUMA这个蛋白,它的“设计图纸”编码在一段叫做 <em>PUMA</em>(也称 <em>BBC3</em>)的基因里。在这段基因的上游,有一段特殊的DNA序列,叫做 “启动子”(Promoter)。而PUMA基因的启动子上,有 p53 蛋白能够识别并结合的特定“停泊位点”,称为 “p53响应元件”(p53 Response Element)。</p> <p>被激活的p53四聚体,凭借其暴露出来的DNA结合域,会精准地找到并牢牢地结合在PUMA基因的这个“停泊位点”上。一旦结合,p53 自身就会像一块磁铁,吸引来细胞核内负责“抄写”基因的庞大机器——<strong>RNA聚合酶II</strong>以及其他大量的辅助蛋白。这个庞大的蛋白质复合体,就是负责执行命令的“抄写员”。</p> <p>“抄写员”以DNA为模板,“抄写”出一段信使RNA(mRNA)。这段mRNA会离开细胞核进入细胞质,并被核糖体(蛋白质的“生产工厂”)读取,最终翻译、生产出我们需要的<strong>PUMA蛋白</strong>。</p> <p>这就是基因转录。</p> -<h4 id="为什么-DNA-的断裂会吸引激酶?"><a href="#为什么-DNA-的断裂会吸引激酶?" class="headerlink" title="为什么 DNA 的断裂会吸引激酶?"></a>为什么 DNA 的断裂会吸引激酶?</h4><p>在正常细胞中,ATM是以无活性的二聚体(两个ATM蛋白抱在一起)形式存在的。这个“拥抱”的姿态,恰好就掩盖了彼此的“催化核心区”。 激活,就是让这俩分开。</p> +<h4 id="为什么-DNA-的断裂会吸引激酶?"><a class="header-anchor" href="#为什么-DNA-的断裂会吸引激酶?">¶</a>为什么 DNA 的断裂会吸引激酶?</h4> +<p>在正常细胞中,ATM是以无活性的二聚体(两个ATM蛋白抱在一起)形式存在的。这个“拥抱”的姿态,恰好就掩盖了彼此的“催化核心区”。 激活,就是让这俩分开。</p> <p>当DNA发生双链断裂时,会暴露出两个<strong>DNA断端</strong>。这在正常的、连续的DNA双螺旋结构中是绝对不会出现的。这个裸露的、不连续的断端,就是一个<strong>强烈的物理异常信号</strong>。</p> <p>细胞内有一组蛋白质,叫做<strong>MRN复合体</strong>(由Mre11、Rad50、Nbs1三个蛋白组成)。</p> <p><strong>Mre11和Rad50</strong> 能特异性地识别并抓住DNA的断端,像桥梁一样将两个断端物理性地拉近,防止它们飘远。MRN复合体一旦抓住了DNA断端,它自身的构象就会发生改变。这种改变使得它能够吸引在细胞核内游走的、处于休眠二聚体状态的ATM。MRN复合体(特别是其中的Nbs1蛋白)上会暴露出一个能与ATM二聚体结合的“接口”。这种分子间的特异性亲和力,使得ATM被“吸引”并<strong>物理性地富集</strong>到DNA断裂的地方。</p> <p>当大量的ATM二聚体被招募到MRN复合体标记的DNA断端附近时,DNA断端的物理存在,加上MRN复合体的“撬动”作用,会诱导ATM二聚体的构象发生变化,促使这俩解离,变成两个独立的单体。</p> -<p> 解离后的ATM单体,其被隐藏的“催化核心区”立刻暴露了出来。一个ATM单体会立刻给另一个(或自己)的特定位点(如丝氨酸1981位点)“安装”上一个磷酸基团。这是个 “自我磷酸化” 的过程,会使其构象进一步稳定在完全激活的状态。</p> -<h1 id="先到这儿吧,我要吃炒粉了"><a href="#先到这儿吧,我要吃炒粉了" class="headerlink" title="先到这儿吧,我要吃炒粉了"></a>先到这儿吧,我要吃炒粉了</h1> +<p>解离后的ATM单体,其被隐藏的“催化核心区”立刻暴露了出来。一个ATM单体会立刻给另一个(或自己)的特定位点(如丝氨酸1981位点)“安装”上一个磷酸基团。这是个 “自我磷酸化” 的过程,会使其构象进一步稳定在完全激活的状态。</p> +<h1>先到这儿吧,我要吃炒粉了</h1> + </div> <script> diff --git a/2025/07/11/rep-date/index.html b/2025/07/11/rep-date/index.html index f2b10afd..86e4b75f 100644 --- a/2025/07/11/rep-date/index.html +++ b/2025/07/11/rep-date/index.html @@ -201,27 +201,31 @@ <p><strong>除权 (Ex-rights):</strong> 如果公司不是派现金,而是送股票(比如“10送3股”),那么总股本会增加,每股代表的实际价值就被稀释了。<strong>“除权”就是调整股价,以反映这种股本增加带来的稀释效应。</strong></p> <p>现金分红主要是除息。</p> <p>在2025年7月14日这一天,该股票的开盘参考价会人为地向下调整。</p> -<p>如果在除息日前一天(假设是T日)持有这只股票,那么资产 = 股票市值 + 即将到手的现金分红。 到了除息日(T+1日,即7月14日),市场会重新定价。新的股价 = 原股价 - 每股分红。</p> +<p>如果在除息日前一天(假设是T日)持有这只股票,那么资产 = 股票市值 + 即将到手的现金分红。 到了除息日(T+1日,即7月14日),市场会重新定价。新的股价 = 原股价 - 每股分红。</p> <p><strong>所以,除权除息日,本质上是股价因分红派息而进行调整的日子。</strong> 股价会下跌,但这并不意味着亏钱,因为股价减少的部分,恰好等于即将收到的现金红利。总资产在理论上保持不变。</p> <p>不过 “除权除息日” 并不是钱到账的日子。这一天发生的是<strong>股价的调整</strong>,而不是现金的发放。</p> <p>完整的分红流程如下:</p> <ol> -<li>股权登记日 (Record Date) <ul> +<li>股权登记日 (Record Date) +<ul> <li>按照A股市场的惯例(T+1制度),<strong>除权除息日通常是股权登记日的下一个交易日。</strong></li> <li>因此,如果除权除息日是2025年7月14日(周一),那么<strong>股权登记日很可能就是前一个交易日,即2025年7月11日(周五)。</strong> (注:这需要以公司正式公告为准,但逻辑如此)。</li> <li><strong>所以必须在股权登记日当天收盘时持有该股票,才能拿到分红。</strong></li> </ul> </li> -<li>除权除息日 (Ex-Date) — 股价调整的日子<ul> +<li>除权除息日 (Ex-Date) — 股价调整的日子 +<ul> <li><strong>股价调整:</strong> 股票价格会下跌,反映红利已被剔除。</li> -<li><strong>权利分割:</strong><ul> +<li><strong>权利分割:</strong> +<ul> <li>如果在这一天<strong>买入</strong>这只股票,无法享受这次分红。因为买的是已经“打折”后的股票。</li> <li>如果在这一天<strong>卖出</strong>在登记日持有的股票,依然可以拿到分红!因为在登记日是股东。</li> </ul> </li> </ul> </li> -<li>派息日 (Payment Date) <ul> +<li>派息日 (Payment Date) +<ul> <li>现金红利会在这一天自动打入你的证券资金账户。</li> <li>派息日通常在除权除息日之后几天,有时甚至是同一天,但这需要看上市公司的具体安排和公告。</li> </ul> diff --git a/2025/07/29/nestjs-nested-object-dto/index.html b/2025/07/29/nestjs-nested-object-dto/index.html index 3e7695e5..b67e6ef3 100644 --- a/2025/07/29/nestjs-nested-object-dto/index.html +++ b/2025/07/29/nestjs-nested-object-dto/index.html @@ -193,40 +193,50 @@ </div> <div class="post-content"> <p>在开发 NestJS 应用程序时,一个常见的场景是根据复杂的条件筛选资源。当使用嵌套查询参数来表达这些条件时,例如通过 URL <code>http://localhost:3000/products/filter?filters[price][min]=100</code>,可能会意外地遇到 <code>property filters[price][min] should not exist</code> 错误。本文将深入剖析此问题背后的原因,并提供针对 Express 和 Fastify 两种底层框架的解决方案。</p> -<h4 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h4><p>假设我们正在构建一个电子商务平台的后端,需要实现一个商品筛选接口 <code>/products/filter</code>。该接口应允许客户端根据不同的属性(如价格 <code>price</code>、库存 <code>stock</code>)进行筛选,并支持对数值型属性(如价格)指定一个范围。</p> +<h4 id="问题背景"><a class="header-anchor" href="#问题背景">¶</a>问题背景</h4> +<p>假设我们正在构建一个电子商务平台的后端,需要实现一个商品筛选接口 <code>/products/filter</code>。该接口应允许客户端根据不同的属性(如价格 <code>price</code>、库存 <code>stock</code>)进行筛选,并支持对数值型属性(如价格)指定一个范围。</p> <p>为了实现这一功能,我们定义了以下 NestJS 数据传输对象 (DTO) 结构:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">ApiPropertyOptional</span> } <span class="keyword">from</span> <span class="string">"@nestjs/swagger"</span>;</span><br><span class="line"><span class="keyword">import</span> {</span><br><span class="line"> <span class="title class_">IsNumber</span>,</span><br><span class="line"> <span class="title class_">IsOptional</span>,</span><br><span class="line"> <span class="title class_">Min</span>,</span><br><span class="line"> <span class="title class_">ValidateNested</span>,</span><br><span class="line">} <span class="keyword">from</span> <span class="string">"class-validator"</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Type</span> } <span class="keyword">from</span> <span class="string">"class-transformer"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">RangeFilterDto</span> {</span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({ <span class="attr">description</span>: <span class="string">"最小值 (大于等于)"</span> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">Number</span>)</span><br><span class="line"> <span class="meta">@IsNumber</span>()</span><br><span class="line"> <span class="meta">@Min</span>(<span class="number">0</span>)</span><br><span class="line"> <span class="attr">min</span>?: <span class="built_in">number</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({ <span class="attr">description</span>: <span class="string">"最大值 (小于等于)"</span> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">Number</span>)</span><br><span class="line"> <span class="meta">@IsNumber</span>()</span><br><span class="line"> <span class="meta">@Min</span>(<span class="number">0</span>)</span><br><span class="line"> <span class="attr">max</span>?: <span class="built_in">number</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ProductAttributeFilterDto</span> {</span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({</span><br><span class="line"> <span class="attr">description</span>: <span class="string">"价格范围"</span>,</span><br><span class="line"> <span class="attr">type</span>: <span class="title class_">RangeFilterDto</span>,</span><br><span class="line"> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@ValidateNested</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">RangeFilterDto</span>)</span><br><span class="line"> <span class="attr">price</span>?: <span class="title class_">RangeFilterDto</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductFilterDto</span> {</span><br><span class="line"> <span class="meta">@ApiPropertyOptional</span>({ <span class="attr">description</span>: <span class="string">"商品属性筛选条件"</span> })</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@ValidateNested</span>()</span><br><span class="line"> <span class="meta">@Type</span>(<span class="function">() =></span> <span class="title class_">ProductAttributeFilterDto</span>)</span><br><span class="line"> <span class="attr">filters</span>?: <span class="title class_">ProductAttributeFilterDto</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>对应的控制器代码如下:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Controller</span>, <span class="title class_">Get</span>, <span class="title class_">Query</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ApiOperation</span>, <span class="title class_">ApiResponse</span>, <span class="title class_">ApiTags</span> } <span class="keyword">from</span> <span class="string">'@nestjs/swagger'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ProductFilterDto</span> } <span class="keyword">from</span> <span class="string">'./dtos/product-filter.dto'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@ApiTags</span>(<span class="string">'products'</span>)</span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'products'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductsController</span> {</span><br><span class="line"> <span class="meta">@Get</span>(<span class="string">"filter"</span>)</span><br><span class="line"> <span class="meta">@ApiOperation</span>({ <span class="attr">summary</span>: <span class="string">"根据条件筛选商品"</span> })</span><br><span class="line"> <span class="meta">@ApiResponse</span>({ <span class="attr">status</span>: <span class="number">200</span>, <span class="attr">description</span>: <span class="string">"筛选商品成功"</span> })</span><br><span class="line"> <span class="meta">@ApiResponse</span>({ <span class="attr">status</span>: <span class="number">400</span>, <span class="attr">description</span>: <span class="string">"请求参数错误"</span> })</span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">getFilteredProducts</span>(</span><br><span class="line"> <span class="meta">@Query</span>() <span class="attr">query</span>: <span class="title class_">ProductFilterDto</span>,</span><br><span class="line"> ): <span class="title class_">Promise</span><<span class="built_in">any</span>> {</span><br><span class="line"> <span class="comment">// 在实际应用中,这里会调用服务处理筛选逻辑</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(query);</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">message</span>: <span class="string">"Products filtered successfully."</span>,</span><br><span class="line"> <span class="attr">data</span>: query,</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>当客户端通过以下 URL 发送请求时:</p> <p><code>http://localhost:3000/products/filter?filters[price][min]=100</code></p> <p>后端返回了 HTTP 400 错误,并附带以下日志:</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></pre></td><td class="code"><pre><span class="line">[Nest] 12345 - 07/29/2025, 11:00:00 AM ERROR [HttpExceptionFilter] [GET /products/filter?filters%5Bprice%5D%5Bmin%5D=100] HTTP 400 Error: property filters[price][min] should not exist</span><br><span class="line">BadRequestException: Bad Request Exception</span><br><span class="line"> at ValidationPipe.exceptionFactory (/path/to/project/node_modules/@nestjs/common/pipes/validation.pipe.js:107:20)</span><br><span class="line"> at ValidationPipe.transform (/path/to/project/node_modules/@nestjs/common/pipes/validation.pipe.js:74:30)</span><br><span class="line"> ... (其他堆栈信息)</span><br></pre></td></tr></table></figure> - -<h4 id="错误分析"><a href="#错误分析" class="headerlink" title="错误分析"></a>错误分析</h4><p>此错误的核心在于 NestJS 的 <code>@Query()</code> 装饰器与 <code>ValidationPipe</code> 在处理 URL 查询字符串时的默认行为,未能正确地将客户端提供的嵌套方括号语法 (<code>filters[price][min]</code>) 解析为 DTO 所期望的 JavaScript 嵌套对象结构。</p> +<h4 id="错误分析"><a class="header-anchor" href="#错误分析">¶</a>错误分析</h4> +<p>此错误的核心在于 NestJS 的 <code>@Query()</code> 装饰器与 <code>ValidationPipe</code> 在处理 URL 查询字符串时的默认行为,未能正确地将客户端提供的嵌套方括号语法 (<code>filters[price][min]</code>) 解析为 DTO 所期望的 JavaScript 嵌套对象结构。</p> <ol> -<li><p><strong>DTO 的预期结构:</strong><br><code>ProductFilterDto</code> 定义了 <code>filters</code> 属性,其类型为 <code>ProductAttributeFilterDto</code>。<code>ProductAttributeFilterDto</code> 又包含了 <code>price</code> 属性,类型为 <code>RangeFilterDto</code>,最终含有 <code>min</code> 和 <code>max</code> 属性。这意味着 DTO 期望接收的数据结构应为:<code>{ filters: { price: { min: 100 } } }</code>。</p> +<li> +<p><strong>DTO 的预期结构:</strong><br> +<code>ProductFilterDto</code> 定义了 <code>filters</code> 属性,其类型为 <code>ProductAttributeFilterDto</code>。<code>ProductAttributeFilterDto</code> 又包含了 <code>price</code> 属性,类型为 <code>RangeFilterDto</code>,最终含有 <code>min</code> 和 <code>max</code> 属性。这意味着 DTO 期望接收的数据结构应为:<code>{ filters: { price: { min: 100 } } }</code>。</p> </li> -<li><p><strong>URL 查询参数的扁平化解析:</strong><br>HTTP 协议的查询字符串本质上是扁平的键值对集合。虽然 <code>key[nestedKey]=value</code> 这种方括号语法被许多 Web 框架(如 PHP、Ruby on Rails)和库(如 <code>qs</code>)广泛用于表示嵌套数据,但这并非 HTTP 协议的标准。<br>NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的请求。在默认配置下,Express 不会自动地、递归地将 <code>filters[price][min]</code> 这样的字符串键名解析成一个具有正确嵌套层级的 JavaScript 对象。相反,它会将其视为一个完整的、扁平的字符串键名 <code>filters[price][min]</code>,并将其值 <code>100</code> 与之对应。</p> +<li> +<p><strong>URL 查询参数的扁平化解析:</strong><br> +HTTP 协议的查询字符串本质上是扁平的键值对集合。虽然 <code>key[nestedKey]=value</code> 这种方括号语法被许多 Web 框架(如 PHP、Ruby on Rails)和库(如 <code>qs</code>)广泛用于表示嵌套数据,但这并非 HTTP 协议的标准。<br> +NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的请求。在默认配置下,Express 不会自动地、递归地将 <code>filters[price][min]</code> 这样的字符串键名解析成一个具有正确嵌套层级的 JavaScript 对象。相反,它会将其视为一个完整的、扁平的字符串键名 <code>filters[price][min]</code>,并将其值 <code>100</code> 与之对应。</p> </li> -<li><p><strong><code>ValidationPipe</code> 的验证失败:</strong><br>当 <code>ValidationPipe</code> 接收到被扁平化解析后的查询参数时,它会在 <code>ProductFilterDto</code> 中寻找一个名为 <code>filters[price][min]</code> 的顶层属性。由于 DTO 中并未直接定义这样一个扁平的属性,并且 <code>ValidationPipe</code> 通常会启用 <code>forbidNonWhitelisted: true</code> 或 <code>whitelist: true</code> 选项来拒绝 DTO 中未明确声明的属性,因此验证过程失败,并抛出 <code>property filters[price][min] should not exist</code> 的错误。这表明 <code>ValidationPipe</code> 将 <code>filters[price][min]</code> 视为一个非法的、未在白名单中的属性,而不是预期的嵌套对象 <code>filters</code> 下的深层子属性。</p> +<li> +<p><strong><code>ValidationPipe</code> 的验证失败:</strong><br> +当 <code>ValidationPipe</code> 接收到被扁平化解析后的查询参数时,它会在 <code>ProductFilterDto</code> 中寻找一个名为 <code>filters[price][min]</code> 的顶层属性。由于 DTO 中并未直接定义这样一个扁平的属性,并且 <code>ValidationPipe</code> 通常会启用 <code>forbidNonWhitelisted: true</code> 或 <code>whitelist: true</code> 选项来拒绝 DTO 中未明确声明的属性,因此验证过程失败,并抛出 <code>property filters[price][min] should not exist</code> 的错误。这表明 <code>ValidationPipe</code> 将 <code>filters[price][min]</code> 视为一个非法的、未在白名单中的属性,而不是预期的嵌套对象 <code>filters</code> 下的深层子属性。</p> </li> </ol> <p>简而言之,问题不在于 DTO 定义或 <code>@Query()</code> 装饰器本身,而在于底层 HTTP 框架对查询字符串的默认解析行为与 NestJS <code>ValidationPipe</code> 对复杂嵌套 DTO 结构的需求不匹配。</p> -<h4 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h4><p>要解决此问题,关键在于配置 NestJS 应用的底层 HTTP 适配器,使其能够正确地解析包含方括号语法的嵌套查询参数,将其转换为嵌套的 JavaScript 对象。</p> -<h5 id="针对-Express-适配器"><a href="#针对-Express-适配器" class="headerlink" title="针对 Express 适配器"></a>针对 Express 适配器</h5><p>如果您的 NestJS 应用使用 Express 作为底层 HTTP 框架(这是新项目的默认配置),您需要在应用的入口文件(通常是 <code>main.ts</code>)中,通过 <code>app.set('query parser', 'extended');</code> 显式启用 Express 的扩展查询字符串解析功能。此配置会指示 Express 使用 <code>qs</code> 库(Express 内置依赖)来处理查询字符串,该库能够正确处理嵌套结构。</p> +<h4 id="解决方案"><a class="header-anchor" href="#解决方案">¶</a>解决方案</h4> +<p>要解决此问题,关键在于配置 NestJS 应用的底层 HTTP 适配器,使其能够正确地解析包含方括号语法的嵌套查询参数,将其转换为嵌套的 JavaScript 对象。</p> +<h5 id="针对-Express-适配器"><a class="header-anchor" href="#针对-Express-适配器">¶</a>针对 Express 适配器</h5> +<p>如果您的 NestJS 应用使用 Express 作为底层 HTTP 框架(这是新项目的默认配置),您需要在应用的入口文件(通常是 <code>main.ts</code>)中,通过 <code>app.set('query parser', 'extended');</code> 显式启用 Express 的扩展查询字符串解析功能。此配置会指示 Express 使用 <code>qs</code> 库(Express 内置依赖)来处理查询字符串,该库能够正确处理嵌套结构。</p> <p>示例 (<code>main.ts</code>):</p> <figure class="highlight typescript"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">NestFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ValidationPipe</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">NestExpressApplication</span> } <span class="keyword">from</span> <span class="string">'@nestjs/platform-express'</span>; <span class="comment">// 导入此类型</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">bootstrap</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> app = <span class="keyword">await</span> <span class="title class_">NestFactory</span>.<span class="property">create</span><<span class="title class_">NestExpressApplication</span>>(<span class="title class_">AppModule</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 启用 Express 的扩展查询参数解析</span></span><br><span class="line"> <span class="comment">// 'simple' (默认) | 'extended'</span></span><br><span class="line"> app.<span class="title function_">set</span>(<span class="string">'query parser'</span>, <span class="string">'extended'</span>);</span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">useGlobalPipes</span>(<span class="keyword">new</span> <span class="title class_">ValidationPipe</span>({</span><br><span class="line"> <span class="attr">whitelist</span>: <span class="literal">true</span>, <span class="comment">// 自动剥离 DTO 中未定义的属性</span></span><br><span class="line"> <span class="attr">forbidNonWhitelisted</span>: <span class="literal">true</span>, <span class="comment">// 如果传入 DTO 中未定义的属性,则抛出错误</span></span><br><span class="line"> <span class="attr">transform</span>: <span class="literal">true</span>, <span class="comment">// 自动将负载转换为 DTO 类的实例</span></span><br><span class="line"> <span class="attr">transformOptions</span>: {</span><br><span class="line"> <span class="attr">enableImplicitConversion</span>: <span class="literal">true</span>, <span class="comment">// 允许隐式类型转换,配合 @Type()</span></span><br><span class="line"> },</span><br><span class="line"> }));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ... 其他配置</span></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(<span class="number">3000</span>);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></table></figure> - -<h5 id="针对-Fastify-适配器"><a href="#针对-Fastify-适配器" class="headerlink" title="针对 Fastify 适配器"></a>针对 Fastify 适配器</h5><p>如果您的 NestJS 应用使用 Fastify 作为底层 HTTP 框架,您需要在创建 Fastify 适配器实例时,通过 <code>querystringParser</code> 选项提供一个自定义的查询字符串解析函数。通常,我们会利用 <code>qs</code> 这样的成熟库来完成递归解析。</p> -<p>首先,确保已安装 <code>qs</code> 库及其类型定义:<br><code>npm install qs</code><br><code>npm install -D @types/qs</code></p> +<h5 id="针对-Fastify-适配器"><a class="header-anchor" href="#针对-Fastify-适配器">¶</a>针对 Fastify 适配器</h5> +<p>如果您的 NestJS 应用使用 Fastify 作为底层 HTTP 框架,您需要在创建 Fastify 适配器实例时,通过 <code>querystringParser</code> 选项提供一个自定义的查询字符串解析函数。通常,我们会利用 <code>qs</code> 这样的成熟库来完成递归解析。</p> +<p>首先,确保已安装 <code>qs</code> 库及其类型定义:<br> +<code>npm install qs</code><br> +<code>npm install -D @types/qs</code></p> <p>示例 (<code>main.ts</code>):</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">NestFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ValidationPipe</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> {</span><br><span class="line"> <span class="title class_">FastifyAdapter</span>,</span><br><span class="line"> <span class="title class_">NestFastifyApplication</span>,</span><br><span class="line">} <span class="keyword">from</span> <span class="string">'@nestjs/platform-fastify'</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> qs <span class="keyword">from</span> <span class="string">'qs'</span>; <span class="comment">// 引入 qs 库</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">bootstrap</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> app = <span class="keyword">await</span> <span class="title class_">NestFactory</span>.<span class="property">create</span><<span class="title class_">NestFastifyApplication</span>>(</span><br><span class="line"> <span class="title class_">AppModule</span>,</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">FastifyAdapter</span>({</span><br><span class="line"> <span class="comment">// 配置 Fastify 的查询字符串解析器</span></span><br><span class="line"> <span class="attr">querystringParser</span>: <span class="function">(<span class="params">str</span>) =></span> qs.<span class="title function_">parse</span>(str),</span><br><span class="line"> }),</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">useGlobalPipes</span>(<span class="keyword">new</span> <span class="title class_">ValidationPipe</span>({</span><br><span class="line"> <span class="attr">whitelist</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">forbidNonWhitelisted</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">transform</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">transformOptions</span>: {</span><br><span class="line"> <span class="attr">enableImplicitConversion</span>: <span class="literal">true</span>,</span><br><span class="line"> },</span><br><span class="line"> }));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ... 其他配置</span></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(<span class="number">3000</span>);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></table></figure> - -<p>通过上述配置,无论是使用 Express 还是 Fastify,NestJS 应用都将能够正确地将 <code>?filters[price][min]=100</code> 解析为 <code>{ filters: { price: { min: '100' } } }</code>。随后,在 <code>ValidationPipe</code> 的 <code>transform</code> 阶段,<code>@Type(() => Number)</code> 装饰器会确保 <code>min</code> 的值从字符串 <code>'100'</code> 转换为数字 <code>100</code>,从而顺利通过验证并注入到控制器方法中。</p> -<h3 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h3><ul> +<p>通过上述配置,无论是使用 Express 还是 Fastify,NestJS 应用都将能够正确地将 <code>?filters[price][min]=100</code> 解析为 <code>{ filters: { price: { min: '100' } } }</code>。随后,在 <code>ValidationPipe</code> 的 <code>transform</code> 阶段,<code>@Type(() => Number)</code> 装饰器会确保 <code>min</code> 的值从字符串 <code>'100'</code> 转换为数字 <code>100</code>,从而顺利通过验证并注入到控制器方法中。</p> +<h3 id="引用"><a class="header-anchor" href="#引用">¶</a>引用</h3> +<ul> <li><a target="_blank" rel="noopener" href="https://stackoverflow.com/questions/29960764/what-does-extended-mean-in-express-4-0">What does ‘extended’ mean in express 4.0? - Stack Overflow</a></li> <li><a target="_blank" rel="noopener" href="https://expressjs.com/en/5x/api.html#app.set">Express 5.0 - API Reference (<code>app.set</code>)</a></li> </ul> diff --git a/2025/07/31/svelte-lazyquery/index.html b/2025/07/31/svelte-lazyquery/index.html index a90029c0..436b509b 100644 --- a/2025/07/31/svelte-lazyquery/index.html +++ b/2025/07/31/svelte-lazyquery/index.html @@ -195,41 +195,47 @@ <p>在前端,一个常见的性能瓶颈是初始加载时请求了用户当前 viewpoint 之外的非必要数据。惰性加载(Lazy Loading)是一种关键策略,它将数据获取推迟到组件进入 viewpoint 时才执行。本文将阐述如何结合使用 Svelte 5、<code>@tanstack/svelte-query</code> 和 <code>runed.dev</code> 的 <code>useIntersectionObserver</code>,构建一个通用的、可复用的惰性加载组件。</p> <p><strong>核心依赖与环境</strong></p> <ul> -<li><p><strong>Svelte 5:</strong> 本实现依赖于 Svelte 5 的符文(Runes)特性,它提供了更精细、更直观的状态管理能力。</p> +<li> +<p><strong>Svelte 5:</strong> 本实现依赖于 Svelte 5 的符文(Runes)特性,它提供了更精细、更直观的状态管理能力。</p> </li> -<li><p><strong>@tanstack/svelte-query:</strong> TanStack Query 的 Svelte 适配版,用户获取数据。</p> +<li> +<p><strong>@tanstack/svelte-query:</strong> TanStack Query 的 Svelte 适配版,用户获取数据。</p> </li> -<li><p><strong>runed.dev:</strong> 一个提供多种 Svelte 5 实用工具的库,本文主要使用其 <code>useIntersectionObserver</code>。</p> +<li> +<p><strong>runed.dev:</strong> 一个提供多种 Svelte 5 实用工具的库,本文主要使用其 <code>useIntersectionObserver</code>。</p> </li> </ul> -<h4 id="设计哲学:分离关注点"><a href="#设计哲学:分离关注点" class="headerlink" title="设计哲学:分离关注点"></a><strong>设计哲学:分离关注点</strong></h4><p>该方案的核心思想是将“何时加载”与“如何加载”这两个关注点进行解耦。</p> +<h4 id="设计哲学:分离关注点"><a class="header-anchor" href="#设计哲学:分离关注点">¶</a><strong>设计哲学:分离关注点</strong></h4> +<p>该方案的核心思想是将“何时加载”与“如何加载”这两个关注点进行解耦。</p> <ol> -<li><p><strong>何时加载 (When to Load):</strong> 组件的可见性决定了数据加载的时机。我们利用 <code>Intersection Observer API</code> 来精确、高效地监听一个元素是否进入 viewpoint。<code>runed.dev</code> 库为此提供了名为 <code>useIntersectionObserver</code> 的便捷封装。</p> +<li> +<p><strong>何时加载 (When to Load):</strong> 组件的可见性决定了数据加载的时机。我们利用 <code>Intersection Observer API</code> 来精确、高效地监听一个元素是否进入 viewpoint。<code>runed.dev</code> 库为此提供了名为 <code>useIntersectionObserver</code> 的便捷封装。</p> </li> -<li><p><strong>如何加载 (How to Load):</strong> 数据获取、缓存、同步和状态管理的复杂性由 <code>@tanstack/svelte-query</code> (TanStack Query) 处理。它提供了一套强大的工具集来管理异步数据。</p> +<li> +<p><strong>如何加载 (How to Load):</strong> 数据获取、缓存、同步和状态管理的复杂性由 <code>@tanstack/svelte-query</code> (TanStack Query) 处理。它提供了一套强大的工具集来管理异步数据。</p> </li> </ol> <p>通过将这两者结合,可以创建一个名为 <code>LazyQuery</code> 的抽象组件。该组件内部处理可见性检测,并根据检测结果动态控制 TanStack Query 的执行,而将具体的查询逻辑(<code>queryFn</code>)和键(<code>queryKey</code>)完全交由使用者定义。</p> -<h4 id="LazyQuery-组件的实现"><a href="#LazyQuery-组件的实现" class="headerlink" title="LazyQuery 组件的实现"></a><code>LazyQuery</code> 组件的实现</h4><blockquote> +<h4 id="LazyQuery-组件的实现"><a class="header-anchor" href="#LazyQuery-组件的实现">¶</a><code>LazyQuery</code> 组件的实现</h4> +<blockquote> <p>目标:封装惰性加载逻辑,并向外暴露一个标准的 TanStack Query 接口。</p> </blockquote> <p>一、对组件的接口类型做如下定义:</p> <figure class="highlight typescript"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { <span class="title class_">CreateQueryOptions</span>, <span class="title class_">QueryKey</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { <span class="title class_">Snippet</span> } <span class="keyword">from</span> <span class="string">'svelte'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Props</span><</span><br><span class="line"> <span class="title class_">TQueryFnData</span> = <span class="built_in">unknown</span>,</span><br><span class="line"> <span class="title class_">TError</span> = <span class="title class_">Error</span>,</span><br><span class="line"> <span class="title class_">TData</span> = <span class="title class_">TQueryFnData</span>,</span><br><span class="line"> <span class="title class_">TQueryKey</span> <span class="keyword">extends</span> <span class="title class_">QueryKey</span> = <span class="title class_">QueryKey</span></span><br><span class="line">> = {</span><br><span class="line"> <span class="comment">/// 一个标准的 TanStack Query 配置对象</span></span><br><span class="line"> <span class="attr">queryOptions</span>: <span class="title class_">CreateQueryOptions</span><<span class="title class_">TQueryFnData</span>, <span class="title class_">TError</span>, <span class="title class_">TData</span>, <span class="title class_">TQueryKey</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// Svelte 5 的 `Snippet` 类型,它允许父组件向子组件传递一段可执行的 UI 模板。</span></span><br><span class="line"> <span class="comment">/// 这个 `Snippet` 会接收到 `createQuery` 返回的完整查询对象 `query`,</span></span><br><span class="line"> <span class="comment">/// 从而可以访问 `data`, `isLoading`, `error` 等所有状态。</span></span><br><span class="line"> <span class="attr">children</span>: <span class="title class_">Snippet</span><{</span><br><span class="line"> <span class="attr">query</span>: <span class="title class_">ReturnType</span><<span class="keyword">typeof</span> createQuery<<span class="title class_">TQueryFnData</span>, <span class="title class_">TError</span>, <span class="title class_">TData</span>, <span class="title class_">TQueryKey</span>>>;</span><br><span class="line"> }>;</span><br><span class="line">};</span><br></pre></td></tr></table></figure> - <p>二、组件逻辑:</p> <figure class="highlight html"><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="tag"><<span class="name">script</span> <span class="attr">lang</span>=<span class="string">"ts"</span>></span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { createQuery, type <span class="title class_">CreateQueryOptions</span>, type <span class="title class_">QueryKey</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { useIntersectionObserver } <span class="keyword">from</span> <span class="string">'runed'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> type { <span class="title class_">Snippet</span> } <span class="keyword">from</span> <span class="string">'svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// [上述的类型定义]</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">let</span> { queryOptions, children }: <span class="title class_">Props</span> = $props();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">let</span> <span class="attr">el</span>: <span class="title class_">Element</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> { isIntersecting } = <span class="title function_">useIntersectionObserver</span>(</span></span><br><span class="line"><span class="language-javascript"> <span class="function">() =></span> el,</span></span><br><span class="line"><span class="language-javascript"> {</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">rootMargin</span>: <span class="string">'200px'</span>,</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> );</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> query = <span class="title function_">createQuery</span>({</span></span><br><span class="line"><span class="language-javascript"> ...queryOptions,</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">get</span> <span class="title function_">enabled</span>() {</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> ($state.<span class="title function_">is</span>(isIntersecting) && (queryOptions.<span class="property">enabled</span> ?? <span class="literal">true</span>));</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> });</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">bind:this</span>=<span class="string">{el}</span>></span></span><br><span class="line"> {#snippet children({ query })}</span><br><span class="line"> {/snippet}</span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure> - <ul> <li>我们创建一个 <code>div</code> 元素作为哨兵(sentinel),并用 <code>bind:this={el}</code> 将其 DOM 引用绑定到变量 <code>el</code>。</li> -<li><code>useIntersectionObserver</code> 接收一个返回目标元素的函数 <code>() => el</code>。<ul> +<li><code>useIntersectionObserver</code> 接收一个返回目标元素的函数 <code>() => el</code>。 +<ul> <li>它返回一个响应式的状态对象,其中 <code>isIntersecting</code> 是一个布尔值的符文(rune),当 <code>div</code> 元素进入 viewpoint 时为 <code>true</code>,否则为 <code>false</code>。</li> </ul> </li> -<li><code>rootMargin: '200px'</code> 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。</li> +<li><code>rootMargin: '200px'</code> 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。</li> </ul> -<h4 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h4><p>使用 <code>LazyQuery</code> 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景:</p> +<h4 id="使用"><a class="header-anchor" href="#使用">¶</a>使用</h4> +<p>使用 <code>LazyQuery</code> 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景:</p> <figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">script</span> <span class="attr">lang</span>=<span class="string">"ts"</span>></span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { <span class="title class_">QueryClientProvider</span>, <span class="title class_">QueryClient</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> <span class="title class_">LazyQuery</span> <span class="keyword">from</span> <span class="string">'./LazyQuery.svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> <span class="title class_">CopdGoldGradingChart</span> <span class="keyword">from</span> <span class="string">'./CopdGoldGradingChart.svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// Define the query configuration object, just like with a standard `createQuery`.</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> queryOptions = {</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">queryKey</span>: [<span class="string">'goldGradingData'</span>],</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">queryFn</span>: <span class="title function_">async</span> () => {</span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// Simulate a network request</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">resolve</span> =></span> <span class="built_in">setTimeout</span>(resolve, <span class="number">1500</span>));</span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// In a real application, this would be an API call</span></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// const response = await fetch('/api/gold-grading');</span></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// return await response.json();</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> { <span class="attr">data</span>: { <span class="attr">totalPatients</span>: <span class="number">1234</span>, <span class="attr">chartPoints</span>: [<span class="comment">/* ... */</span>] } };</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> };</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">{queryClient}</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">style</span>=<span class="string">"height: 200vh;"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Scroll down to see the chart...<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">LazyQuery</span> {<span class="attr">queryOptions</span>}></span></span><br><span class="line"> {#snippet children({ query })}</span><br><span class="line"> {#if query.isLoading}</span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Loading chart data...<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> {:else if query.error}</span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Error: {query.error.message}<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> {:else if query.data}</span><br><span class="line"> <span class="tag"><<span class="name">CopdGoldGradingChart</span></span></span><br><span class="line"><span class="tag"> <span class="attr">isLoading</span>=<span class="string">{query.isFetching}</span></span></span><br><span class="line"><span class="tag"> <span class="attr">error</span>=<span class="string">{query.error}</span></span></span><br><span class="line"><span class="tag"> <span class="attr">refetch</span>=<span class="string">{()</span> =></span> query.refetch()}</span><br><span class="line"> data={query.data.data}</span><br><span class="line"> totalPatients={query.data.data.totalPatients}</span><br><span class="line"> /></span><br><span class="line"> {/if}</span><br><span class="line"> {/snippet}</span><br><span class="line"> <span class="tag"></<span class="name">LazyQuery</span>></span></span><br><span class="line"><span class="tag"></<span class="name">QueryClientProvider</span>></span></span><br></pre></td></tr></table></figure> - <ul> <li>页面初始加载时,<code>LazyQuery</code> 组件被渲染,但由于其 <code>div</code> 在 viewpoint 之外,<code>isIntersecting</code> 为 <code>false</code>。</li> <li><code>createQuery</code> 被调用,但因为 <code>enabled</code> 条件为 <code>false</code>,查询处于禁用状态,不会发起任何网络请求。</li> @@ -239,7 +245,8 @@ <li>这个变化被 <code>createQuery</code> 的 <code>enabled</code> 访问器捕获,查询被自动激活,<code>queryFn</code> 开始执行。</li> <li>TanStack Query 负责管理后续的状态变化(<code>isFetching</code>, <code>data</code>, <code>error</code>),并驱动 <code>children</code> 片段内的 UI 自动更新。</li> </ul> -<h4 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h4><ul> +<h4 id="参考"><a class="header-anchor" href="#参考">¶</a>参考</h4> +<ul> <li><a target="_blank" rel="noopener" href="https://runed.dev/docs/utilities/use-intersection-observer">https://runed.dev/docs/utilities/use-intersection-observer</a></li> <li><a target="_blank" rel="noopener" href="https://tanstack.com/table/latest/docs/introduction">https://tanstack.com/table/latest/docs/introduction</a></li> </ul> diff --git a/2025/08/13/nestjs-enableImplicitConversion-and-transform/index.html b/2025/08/13/nestjs-enableImplicitConversion-and-transform/index.html index 41575879..ec35c351 100644 --- a/2025/08/13/nestjs-enableImplicitConversion-and-transform/index.html +++ b/2025/08/13/nestjs-enableImplicitConversion-and-transform/index.html @@ -193,20 +193,19 @@ </div> <div class="post-content"> <p>在 NestJS 生态中,<code>class-validator</code> 和 <code>class-transformer</code> 这两个库提供了以声明式的方式对 DTO 进行验证和转换。然而在处理布尔值时,如果在全局验证管道或仅仅是在局部同时开启了 <code>enableImplicitConversion</code>,可能会引入一个极其隐蔽且违反直觉的 Bug:前端传过来的布尔值恒为 true。</p> -<h2 id="一个简单的筛选功能"><a href="#一个简单的筛选功能" class="headerlink" title="一个简单的筛选功能"></a>一个简单的筛选功能</h2><p>假设正在开发一个电子商务平台的 API,需要实现一个产品列表的筛选功能。希望能够根据产品是否有库存 (<code>hasStock</code>)、是否为特色产品 (<code>isFeatured</code>) 等布尔条件进行筛选。</p> +<h2 id="一个简单的筛选功能"><a class="header-anchor" href="#一个简单的筛选功能">¶</a>一个简单的筛选功能</h2> +<p>假设正在开发一个电子商务平台的 API,需要实现一个产品列表的筛选功能。希望能够根据产品是否有库存 (<code>hasStock</code>)、是否为特色产品 (<code>isFeatured</code>) 等布尔条件进行筛选。</p> <p>前端发出的请求 URL 可能如下所示: <code>/products?filter[hasStock]=true&filter[isFeatured]=false</code></p> <p>在 NestJS 后端,首先会在 <code>main.ts</code> 中配置一个全局的 <code>ValidationPipe</code>,以自动处理 DTO 的验证和转换。为了方便,通常会启用 <code>enableImplicitConversion</code>,期望它能自动将 URL 查询参数中的字符串(如 <code>"123"</code>, <code>"true"</code>)转换为 DTO 中定义的类型(<code>number</code>, <code>boolean</code>):</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// main.ts</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">ValidationPipe</span> } <span class="keyword">from</span> <span class="string">'@nestjs/common'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">NestFactory</span> } <span class="keyword">from</span> <span class="string">'@nestjs/core'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">AppModule</span> } <span class="keyword">from</span> <span class="string">'./app.module'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">bootstrap</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> app = <span class="keyword">await</span> <span class="title class_">NestFactory</span>.<span class="title function_">create</span>(<span class="title class_">AppModule</span>);</span><br><span class="line"></span><br><span class="line"> app.<span class="title function_">useGlobalPipes</span>(</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">ValidationPipe</span>({</span><br><span class="line"> <span class="attr">transform</span>: <span class="literal">true</span>, <span class="comment">// 启用转换</span></span><br><span class="line"> <span class="attr">whitelist</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">forbidNonWhitelisted</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">transformOptions</span>: {</span><br><span class="line"> <span class="comment">// 启用基于 TypeScript 类型的隐式转换</span></span><br><span class="line"> <span class="attr">enableImplicitConversion</span>: <span class="literal">true</span>, </span><br><span class="line"> },</span><br><span class="line"> }),</span><br><span class="line"> );</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// ... 其他配置</span></span><br><span class="line"> <span class="keyword">await</span> app.<span class="title function_">listen</span>(<span class="number">3000</span>);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></table></figure> - <p>接着,定义一个 <code>ProductFilterDto</code> 来接收这些筛选条件。</p> <p><strong>一个看似正确的 DTO 定义:</strong></p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// product-filter.dto.ts</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">IsBoolean</span>, <span class="title class_">IsOptional</span> } <span class="keyword">from</span> <span class="string">'class-validator'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductFilterDto</span> {</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@IsBoolean</span>()</span><br><span class="line"> <span class="attr">hasStock</span>?: <span class="built_in">boolean</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@IsBoolean</span>()</span><br><span class="line"> <span class="attr">isFeatured</span>?: <span class="built_in">boolean</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>在控制器中使用这个 DTO:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// products.controller.ts</span></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">'products'</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductsController</span> {</span><br><span class="line"> <span class="meta">@Get</span>()</span><br><span class="line"> <span class="title function_">find</span>(<span class="params"><span class="meta">@Query</span>(<span class="string">'filter'</span>) <span class="attr">filter</span>: <span class="title class_">ProductFilterDto</span></span>) {</span><br><span class="line"> <span class="comment">// 期望 filter.isFeatured 的值为 boolean false</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(filter); </span><br><span class="line"> <span class="comment">// ... 业务逻辑</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>当请求 <code>.../products?filter[isFeatured]=false</code> 到达时,本来期望在 <code>find</code> 方法中得到的 <code>filter.isFeatured</code> 的值是布尔类型的 <code>false</code>。然而,控制台输出的结果却令人意外:<code>{ isFeatured: true }</code></p> -<h2 id="问题剖析"><a href="#问题剖析" class="headerlink" title="问题剖析"></a>问题剖析</h2><p>这个问题的根源在于 <code>class-transformer</code> 内部的转换执行顺序,以及 JavaScript 中 <code>Boolean</code> 函数的类型转换行为。</p> +<h2 id="问题剖析"><a class="header-anchor" href="#问题剖析">¶</a>问题剖析</h2> +<p>这个问题的根源在于 <code>class-transformer</code> 内部的转换执行顺序,以及 JavaScript 中 <code>Boolean</code> 函数的类型转换行为。</p> <p>所有通过 URL 查询参数传递的值,其本质都是字符串。当 NestJS 接收到请求时,<code>filter.isFeatured</code> 的原始值是字符串 <code>"false"</code>。</p> <p><code>ValidationPipe</code> 启动 <code>class-transformer</code> 的转换流程。由于在全局管道中设置了 <code>enableImplicitConversion: true</code>,转换器会首先检查 DTO 属性的 TypeScript 类型。</p> <ol> @@ -217,21 +216,19 @@ <p>此时,即使尝试添加一个自定义的 <code>@Transform</code> 装饰器来手动处理这个问题,也为时已晚。</p> <p>例如,定义一个 <code>booleanTransformer</code>:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// boolean-transformer.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">booleanTransformer</span> = (<span class="params">{ value }: { value: <span class="built_in">any</span> }</span>) => {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> value === <span class="string">'string'</span>) {</span><br><span class="line"> <span class="keyword">return</span> value === <span class="string">'true'</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> value;</span><br><span class="line">};</span><br></pre></td></tr></table></figure> - <p>然后更新 dto:</p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// product-filter.dto.ts (错误的尝试)</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Transform</span> } <span class="keyword">from</span> <span class="string">'class-transformer'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">IsBoolean</span>, <span class="title class_">IsOptional</span> } <span class="keyword">from</span> <span class="string">'class-validator'</span>;</span><br><span class="line"><span class="keyword">import</span> { booleanTransformer } <span class="keyword">from</span> <span class="string">'./boolean-transformer'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductFilterDto</span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Transform</span>(booleanTransformer) <span class="comment">// 添加自定义转换</span></span><br><span class="line"> <span class="meta">@IsBoolean</span>()</span><br><span class="line"> <span class="attr">isFeatured</span>?: <span class="built_in">boolean</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>流程会变成这样:</p> <ol> <li>隐式转换首先执行:<code>Boolean("false")</code> -> <code>true</code>。</li> <li><code>@Transform</code> 装饰器执行:此时传递给 <code>booleanTransformer</code> 的 <code>value</code> 已经是上一步错误转换后的布尔值 <code>true</code>,而不是原始的字符串 <code>"false"</code>。转换函数无从下手。</li> </ol> <p>最终结果依然是 <code>true</code>。</p> -<h2 id="解决方案:用-any-绕过隐式转换"><a href="#解决方案:用-any-绕过隐式转换" class="headerlink" title="## 解决方案:用 any 绕过隐式转换"></a>## 解决方案:用 <code>any</code> 绕过隐式转换</h2><p>要解决这个问题,核心在于阻止 <code>class-transformer</code> 进行那次错误的、优先的隐式转换,从而确保自定义 <code>@Transform</code> 函数能接收到最原始的字符串值。</p> +<h2 id="解决方案:用-any-绕过隐式转换"><a class="header-anchor" href="#解决方案:用-any-绕过隐式转换">¶</a>## 解决方案:用 <code>any</code> 绕过隐式转换</h2> +<p>要解决这个问题,核心在于阻止 <code>class-transformer</code> 进行那次错误的、优先的隐式转换,从而确保自定义 <code>@Transform</code> 函数能接收到最原始的字符串值。</p> <p>最直接且侵入性最小的方法,是将 DTO 中相关属性的 TypeScript 类型从 <code>boolean</code> 改为 <code>any</code>。</p> <p><strong>修正后的 DTO 定义:</strong></p> <figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// product-filter.dto.ts (正确的实现)</span></span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">Transform</span> } <span class="keyword">from</span> <span class="string">'class-transformer'</span>;</span><br><span class="line"><span class="keyword">import</span> { <span class="title class_">IsBoolean</span>, <span class="title class_">IsOptional</span> } <span class="keyword">from</span> <span class="string">'class-validator'</span>;</span><br><span class="line"><span class="keyword">import</span> { booleanTransformer } <span class="keyword">from</span> <span class="string">'./boolean-transformer'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 一个更健壮的 booleanTransformer</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">robustBooleanTransformer</span> = (<span class="params">{ value }: { value: <span class="built_in">string</span> }</span>) =></span><br><span class="line"> value === <span class="string">'true'</span> ? <span class="literal">true</span> : value === <span class="string">'false'</span> ? <span class="literal">false</span> : value;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ProductFilterDto</span> {</span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Transform</span>(robustBooleanTransformer)</span><br><span class="line"> <span class="meta">@IsBoolean</span>()</span><br><span class="line"> <span class="attr">hasStock</span>?: <span class="built_in">any</span>; <span class="comment">// <-- 类型从 boolean 改为 any</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@IsOptional</span>()</span><br><span class="line"> <span class="meta">@Transform</span>(robustBooleanTransformer)</span><br><span class="line"> <span class="meta">@IsBoolean</span>()</span><br><span class="line"> <span class="attr">isFeatured</span>?: <span class="built_in">any</span>; <span class="comment">// <-- 类型从 boolean 改为 any</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure> - <p>这个改动虽然看起来放弃了 TypeScript 的类型检查,但在这个特定的场景下,它非常安全且有效。原因如下:</p> <ol> <li><strong>阻止隐式转换</strong>:当 <code>class-transformer</code> 看到属性类型是 <code>any</code> 时,它不知道该隐式转换成什么目标类型,因此会“跳过”这个属性的隐式转换步骤。</li> diff --git a/2025/09/09/database-fk-design-in-clinical-medicine/index.html b/2025/09/09/database-fk-design-in-clinical-medicine/index.html index 78b23495..803efcd3 100644 --- a/2025/09/09/database-fk-design-in-clinical-medicine/index.html +++ b/2025/09/09/database-fk-design-in-clinical-medicine/index.html @@ -198,41 +198,44 @@ </div> </div> <div class="post-content"> - <p>从关系模型的理论视角看,外键作为参照完整性约束的实现机制,理论上能确保跨表数据的逻辑一致性,避免出现孤立记录(orphaned records)[^theoretical_consistency]。但在高并发、分布式、快速迭代的业务场景中,强制外键约束会引入显著的运行时开销:每次写操作都需要执行跨表的锁检查与索引查询,在 OLTP 系统中尤其影响吞吐量。以 TPC-C 基准测试为例,启用外键的订单创建事务延迟可能增加 15%-30%,因为数据库引擎必须验证客户表与订单项表的关联有效性,这在每秒数千次写入的场景下会成为锁竞争瓶颈[^tpc_c_performance]。</p> -<p>而在分库分表架构中,外键约束跨物理节点时的实现复杂度呈指数级上升——分布式事务的两阶段提交协议(2PC)不仅降低性能,还可能因网络分区导致事务悬挂,这与 CAP 定理中 “分布式系统无法同时满足一致性、可用性与分区容错性” 的根本限制直接冲突[^cap_theorem]。</p> -<p>互联网行业的大规模实践表明,当单表数据量超过千万级或需要水平扩展时,放弃外键往往成为必然选择[^distributed_systems]。</p> -<p>这种设计取舍背后还隐藏着更深层的工程哲学转变。传统单体架构中,数据库承担了业务规则的核心验证职责,而微服务与领域驱动设计(DDD)的兴起将数据一致性边界从存储层上移到应用层[^fowler_architecture]。例如在电商订单履约流程中,订单服务与库存服务的关联不再依赖数据库外键,而是通过 Saga 事务模式或事件溯源(Event Sourcing)机制实现最终一致性。</p> + <p>从关系模型的理论视角看,外键作为参照完整性约束的实现机制,理论上能确保跨表数据的逻辑一致性,避免出现孤立记录(orphaned records)<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。但在高并发、分布式、快速迭代的业务场景中,强制外键约束会引入显著的运行时开销:每次写操作都需要执行跨表的锁检查与索引查询,在 OLTP 系统中尤其影响吞吐量。以 TPC-C 基准测试为例,启用外键的订单创建事务延迟可能增加 15%-30%,因为数据库引擎必须验证客户表与订单项表的关联有效性,这在每秒数千次写入的场景下会成为锁竞争瓶颈<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>。</p> +<p>而在分库分表架构中,外键约束跨物理节点时的实现复杂度呈指数级上升——分布式事务的两阶段提交协议(2PC)不仅降低性能,还可能因网络分区导致事务悬挂,这与 CAP 定理中 “分布式系统无法同时满足一致性、可用性与分区容错性” 的根本限制直接冲突<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>。</p> +<p>互联网行业的大规模实践表明,当单表数据量超过千万级或需要水平扩展时,放弃外键往往成为必然选择<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>。</p> +<p>这种设计取舍背后还隐藏着更深层的工程哲学转变。传统单体架构中,数据库承担了业务规则的核心验证职责,而微服务与领域驱动设计(DDD)的兴起将数据一致性边界从存储层上移到应用层<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>。例如在电商订单履约流程中,订单服务与库存服务的关联不再依赖数据库外键,而是通过 Saga 事务模式或事件溯源(Event Sourcing)机制实现最终一致性。</p> <p>应用层通过领域事件(如 OrderCreated 事件)触发库存预占操作,并在消息队列保障下实现跨服务协调。这种方式虽然增加了业务代码的复杂度,但换取了服务解耦与独立部署能力, 即, 当库存服务需要重构时,无需协调订单服务的数据库变更,这大大提升了敏捷开发效率。</p> -<p>阿里的《企业级分布式应用架构》中表示, 其核心交易系统在数据库层面移除外键后,需求迭代周期缩短40%,因为团队不再受制于跨服务的数据库变更审批流程[^alibaba_architecture]。</p> +<p>阿里的《企业级分布式应用架构》中表示, 其核心交易系统在数据库层面移除外键后,需求迭代周期缩短40%,因为团队不再受制于跨服务的数据库变更审批流程<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>。</p> <p>然而放弃外键绝非没有代价。</p> -<p>最直接的影响是数据一致性的保障责任从 DBA 转移至应用开发团队,当业务逻辑存在缺陷时,极易产生逻辑断裂的数据,例如支付成功但订单状态未更新的场景[^data_integrity_issues]。这类问题往往在特定异常路径下才暴露,调试难度远高于数据库层面的即时约束报错。</p> -<p>对于历史数据迁移和数据分析,缺乏外键约束的模型在构建数仓时,ETL 过程必须额外实现参照验证逻辑,否则维度表与事实表的断裂关联会导致分析结论失真。Netflix 在其技术博客中披露,早期用户观看记录与内容元数据的关联缺失曾导致推荐模型准确率下降 7%,最终通过构建独立的数据校验服务补救[^data_warehouse_challenges]。</p> +<p>最直接的影响是数据一致性的保障责任从 DBA 转移至应用开发团队,当业务逻辑存在缺陷时,极易产生逻辑断裂的数据,例如支付成功但订单状态未更新的场景<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup>。这类问题往往在特定异常路径下才暴露,调试难度远高于数据库层面的即时约束报错。</p> +<p>对于历史数据迁移和数据分析,缺乏外键约束的模型在构建数仓时,ETL 过程必须额外实现参照验证逻辑,否则维度表与事实表的断裂关联会导致分析结论失真。Netflix 在其技术博客中披露,早期用户观看记录与内容元数据的关联缺失曾导致推荐模型准确率下降 7%,最终通过构建独立的数据校验服务补救<sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup>。</p> <blockquote> <p>真正专业的架构决策需要基于量化指标进行场景化评估。</p> </blockquote> <p>对于交易系统等强一致性场景,普遍建议仅在单数据库实例内保留关键外键(如订单与订单项的关联),而跨服务关系则采用异步校验;</p> -<p>对于分析型系统或写入吞吐要求极高的场景(如IoT设备数据采集),可完全放弃外键,但必须配套实施三大保障措施:一是在应用层实现幂等写入与版本控制(如使用乐观锁的CAS操作),二是部署定时数据稽核任务(如每日扫描账户余额负值等异常状态),三是通过变更数据捕获(CDC)技术将操作日志实时写入审计表[^engineering_guidelines]。</p> +<p>对于分析型系统或写入吞吐要求极高的场景(如IoT设备数据采集),可完全放弃外键,但必须配套实施三大保障措施:一是在应用层实现幂等写入与版本控制(如使用乐观锁的CAS操作),二是部署定时数据稽核任务(如每日扫描账户余额负值等异常状态),三是通过变更数据捕获(CDC)技术将操作日志实时写入审计表<sup class="footnote-ref"><a href="#fn9" id="fnref9">[9]</a></sup>。</p> <blockquote> -<p>现代云数据库如 Amazon Aurora 已提供逻辑外键(logical foreign keys)的折中方案:它不强制运行时约束,但通过存储过程与触发器记录关联规则,在数据导出或特定查询时触发验证,这在保持写性能的同时保留了部分数据治理能力[^aurora_logical_foreign_keys]。</p> +<p>现代云数据库如 Amazon Aurora 已提供逻辑外键(logical foreign keys)的折中方案:它不强制运行时约束,但通过存储过程与触发器记录关联规则,在数据导出或特定查询时触发验证,这在保持写性能的同时保留了部分数据治理能力<sup class="footnote-ref"><a href="#fn10" id="fnref10">[10]</a></sup>。</p> </blockquote> <p>所以最终判断是否使用外键应基于四个维度的具体测量:</p> -<p>一、业务容忍的数据不一致窗口(如金融系统要求秒级,内容推荐可接受小时级)。<br>二、峰值QPS与事务复杂度。<br>三、团队对分布式一致性的掌控能力。<br>四、监控修复工具链的完备性。</p> +<p>一、业务容忍的数据不一致窗口(如金融系统要求秒级,内容推荐可接受小时级)。<br> +二、峰值QPS与事务复杂度。<br> +三、团队对分布式一致性的掌控能力。<br> +四、监控修复工具链的完备性。</p> <blockquote> <p>当系统处于初创期时保留外键可降低认知负担,但进入高速增长期后需有计划地将约束责任前移至应用层。</p> </blockquote> -<p>在临床医疗领域的药物研发项目与慢病管理系统中,数据库外键的取舍决策必须超越传统性能权衡,这是因为医疗数据承担着生命安全关联性、法规强制性约束与临床逻辑不可妥协性这些特殊情况[^medical_data_criticality]。</p> +<p>在临床医疗领域的药物研发项目与慢病管理系统中,数据库外键的取舍决策必须超越传统性能权衡,这是因为医疗数据承担着生命安全关联性、法规强制性约束与临床逻辑不可妥协性这些特殊情况<sup class="footnote-ref"><a href="#fn11" id="fnref11">[11]</a></sup>。</p> <p>这类系统的核心矛盾在于:医疗数据的完整性缺陷可能直接导致误诊、用药错误甚至患者死亡,而过度依赖外键又可能阻碍紧急场景下的操作敏捷性(如 ICU 实时数据录入)。因此,外键策略需分层设计,依据数据域的风险等级与业务场景动态调整。</p> -<p>以药物研发项目数据库为例,其数据模型涉及化合物结构、临床试验阶段、受试者信息及不良事件报告等强依赖实体。在 I 期临床试验阶段(单中心小规模数据),保留外键是合规刚需:当录入受试者用药事件时,必须强制关联有效的伦理委员会批准编号(protocol_id)和药品批号(lot_number)。这并非仅出于数据规范性,FDA 21 CFR Part 11 明确规定电子记录需具备 “可靠的归属关系”,若不良事件记录无法追溯到具体药物批次,将导致整个试验数据被判定为无效[^fda_regulations]。</p> -<p>实测数据显示,在 PostgreSQL 中启用外键约束后,单次药物不良反应事件录入延迟仅增加0.8ms(从3.2ms到4.0ms),但可100%拦截”批号不存在”类错误。这类错误在无外键环境下平均占人工稽查量的 37%(基于 Pfizer 2022 年临床数据质量报告),其修正成本是预防成本的22倍[^pfa_data_quality]。因此,在核心试验元数据层(研究方案、受试者登记表),外键应作为技术强制项而非可选项。</p> +<p>以药物研发项目数据库为例,其数据模型涉及化合物结构、临床试验阶段、受试者信息及不良事件报告等强依赖实体。在 I 期临床试验阶段(单中心小规模数据),保留外键是合规刚需:当录入受试者用药事件时,必须强制关联有效的伦理委员会批准编号(protocol_id)和药品批号(lot_number)。这并非仅出于数据规范性,FDA 21 CFR Part 11 明确规定电子记录需具备 “可靠的归属关系”,若不良事件记录无法追溯到具体药物批次,将导致整个试验数据被判定为无效<sup class="footnote-ref"><a href="#fn12" id="fnref12">[12]</a></sup>。</p> +<p>实测数据显示,在 PostgreSQL 中启用外键约束后,单次药物不良反应事件录入延迟仅增加0.8ms(从3.2ms到4.0ms),但可100%拦截"批号不存在"类错误。这类错误在无外键环境下平均占人工稽查量的 37%(基于 Pfizer 2022 年临床数据质量报告),其修正成本是预防成本的22倍<sup class="footnote-ref"><a href="#fn13" id="fnref13">[13]</a></sup>。因此,在核心试验元数据层(研究方案、受试者登记表),外键应作为技术强制项而非可选项。</p> <p>然而当进入 III 期多中心试验阶段,分布式数据采集就会出现问题,某中心可能临时使用本地 SQLite 数据库录入紧急病例,此时跨地域外键校验会因网络延迟导致生命体征数据积压。</p> <p>此处的取舍在于 “约束分级”:对患者身份标识符(如随机化编号)等关键关系保留外键,而对非致命性关联(如患者地址分类码)改用应用层校验。</p> -<p>更前沿的实践是采用基于 FHIR(Fast Healthcare Interoperability Resources)标准的松散耦合架构——当录入不良事件时,系统不强制外键,但通过 HL7 FHIR 的 Reference 机制调用中央受试者服务的实时验证 API。这既满足了 EudraCT(欧盟临床试验数据库)要求的”数据关联可追溯性”,又避免了传统外键的分布式锁竞争[^fhir_standard]。默克药厂的临床数据管理平台实测表明,该方案将跨中心数据整合延迟降低 63%,同时通过 OAuth 2.0 保障验证请求的原子性。</p> +<p>更前沿的实践是采用基于 FHIR(Fast Healthcare Interoperability Resources)标准的松散耦合架构——当录入不良事件时,系统不强制外键,但通过 HL7 FHIR 的 Reference 机制调用中央受试者服务的实时验证 API。这既满足了 EudraCT(欧盟临床试验数据库)要求的"数据关联可追溯性",又避免了传统外键的分布式锁竞争<sup class="footnote-ref"><a href="#fn14" id="fnref14">[14]</a></sup>。默克药厂的临床数据管理平台实测表明,该方案将跨中心数据整合延迟降低 63%,同时通过 OAuth 2.0 保障验证请求的原子性。</p> <hr> <p>慢病管理系统的决策逻辑更为精细。以糖尿病患者管理系统为例,血糖监测记录表(blood_glucose_readings)与患者档案表(patient_profiles)的关联存在三种典型场景:</p> <ul> -<li><strong>预防性场景</strong>(门诊定期随访):外键应强制存在。每次录入糖化血红蛋白值必须关联有效患者ID,否则会导致个性化治疗方案生成错误。英国 NHS 的实践证明,启用外键后糖尿病并发症误判率下降 21%,因为系统不再出现 “记录归属未知患者” 的脏数据[^nhs_data_quality]。</li> -<li><strong>应急场景</strong>(急救车实时数据传输):需临时禁用外键。当急救人员通过移动设备录入昏迷患者的血糖值时,若因患者 ID 验证失败导致写入中断,将危及生命。此时系统应将数据写入隔离缓冲区(quarantine zone),外键校验延迟至网络恢复后由后台服务补做。这种设计已进入 ISO/TR 20514 医疗设备通信规范[^iso_medical_standard]。</li> -<li><strong>分析场景</strong>(长期队列研究):采用逻辑外键替代物理外键。研究型数据库中,患者 ID 仅作为业务键存在,可以通过 Apache Atlas 等数据治理工具构建血缘关系图谱,在 ETL 过程中自动执行完整性检查。约翰霍普金斯大学的慢病研究平台显示,该方式在保持数据挖掘效率的同时,将人工数据清洗工时减少 58%[^data_governance_tools]。</li> +<li><strong>预防性场景</strong>(门诊定期随访):外键应强制存在。每次录入糖化血红蛋白值必须关联有效患者ID,否则会导致个性化治疗方案生成错误。英国 NHS 的实践证明,启用外键后糖尿病并发症误判率下降 21%,因为系统不再出现 “记录归属未知患者” 的脏数据<sup class="footnote-ref"><a href="#fn15" id="fnref15">[15]</a></sup>。</li> +<li><strong>应急场景</strong>(急救车实时数据传输):需临时禁用外键。当急救人员通过移动设备录入昏迷患者的血糖值时,若因患者 ID 验证失败导致写入中断,将危及生命。此时系统应将数据写入隔离缓冲区(quarantine zone),外键校验延迟至网络恢复后由后台服务补做。这种设计已进入 ISO/TR 20514 医疗设备通信规范<sup class="footnote-ref"><a href="#fn16" id="fnref16">[16]</a></sup>。</li> +<li><strong>分析场景</strong>(长期队列研究):采用逻辑外键替代物理外键。研究型数据库中,患者 ID 仅作为业务键存在,可以通过 Apache Atlas 等数据治理工具构建血缘关系图谱,在 ETL 过程中自动执行完整性检查。约翰霍普金斯大学的慢病研究平台显示,该方式在保持数据挖掘效率的同时,将人工数据清洗工时减少 58%<sup class="footnote-ref"><a href="#fn17" id="fnref17">[17]</a></sup>。</li> </ul> <p>对于法规与安全维度。HIPAA 要求所有患者数据关联必须可审计,而 GDPR “被遗忘权” 又要求能彻底解耦数据。若使用传统 ON DELETE CASCADE 外键,删除患者记录会级联抹除所有医疗历史,违反 FDA 要求的 “试验数据永久保留” 原则。</p> <p>一个可行的方案是设计<strong>策略性外键</strong>:</p> @@ -241,12 +244,55 @@ <li>关键表(如用药记录)不设置 ON DELETE 动作,改用逻辑删除标记(<code>is_active BOOLEAN</code>)</li> <li>当触发患者数据删除请求时,外键约束转换为业务规则检查——确保研究数据经脱敏后转入归档库(符合 21 CFR 11.10(e))</li> </ol> -<p>这种设计在 Mayo Clinic 的慢病管理系统中得到验证:既满足每秒 300 次的生命体征写入吞吐(外键校验异步化至Kafka流处理),又通过区块链存证保留所有关联操作的审计轨迹[^mayo_clinic_case_study]。</p> +<p>这种设计在 Mayo Clinic 的慢病管理系统中得到验证:既满足每秒 300 次的生命体征写入吞吐(外键校验异步化至Kafka流处理),又通过区块链存证保留所有关联操作的审计轨迹<sup class="footnote-ref"><a href="#fn18" id="fnref18">[18]</a></sup>。</p> <blockquote> <p>核心原则是:<strong>外键的存在与否取决于业务操作的后果严重性,而非单纯的技术指标</strong>。当数据断裂可能直接伤害患者时,必须用外键,当系统响应速度关乎生命时,则需设计更智能的补偿机制。</p> </blockquote> -<p>不过现代云医疗数据库(如AWS HealthLake)已内置此类混合策略:在 OLTP 层保留关键外键,同时提供 FHIR 资源引用的逻辑一致性检查[^aws_healthlake]。开发者可以定期通过 Chaos Engineering 测试数据断裂场景,模拟删除患者记录后检查系统恢复能力。</p> -<h2 id="参考文献:"><a href="#参考文献:" class="headerlink" title="参考文献:"></a>参考文献:</h2><p>[^theoretical_consistency]: Gray, J., & Reuter, A. (1993). Transaction Processing: Concepts and Techniques. Morgan Kaufmann.<br>[^tpc_c_performance]: CockroachDB 团队性能基准测试报告 (2020-2022)<br>[^cap_theorem]: Taft, R., et al. (2020). CockroachDB: The Resilient Geo-Distributed SQL Database. SIGMOD.<br>[^distributed_systems]: Google Cloud Architecture Center. (2022). Designing for Consistency in Distributed Databases.<br>[^fowler_architecture]: Fowler, M. (2003). Patterns of Enterprise Application Architecture. Addison-Wesley.<br>[^alibaba_architecture]: Alibaba Group. (2019). Nacos: A Dynamic Naming and Configuration Service for Cloud Native Applications.<br>[^data_integrity_issues]: Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly.<br>[^data_warehouse_challenges]: Netflix Technology Blog (2018). “When Data Relationships Break: Lessons from Recommendation Systems”<br>[^engineering_guidelines]: Kleppmann, M. (2020). Transaction Processing in Healthcare Systems. Communications of the ACM, 63(7).<br>[^aurora_logical_foreign_keys]: AWS Database Blog. (2021). Logical foreign keys in Amazon Aurora.<br>[^medical_data_criticality]: Jensen, P. B., et al. (2019). Mining Electronic Health Records: Towards Better Research Applications and Clinical Care. Nature Reviews Genetics.<br>[^fda_regulations]: U.S. Food and Drug Administration. (2023). 21 CFR Part 11: Electronic Records; Electronic Signatures.<br>[^pfa_data_quality]: Pfizer Clinical Data Science Team. (2022). Annual Data Quality Report. Internal Publication.<br>[^fhir_standard]: HL7 International. (2022). FHIR R4 Clinical Reasoning Module.<br>[^nhs_data_quality]: NHS Digital. (2022). Data Quality Framework for Healthcare Systems.<br>[^iso_medical_standard]: ISO/TR 20514:2021. Health informatics — Framework for integrity of health information.<br>[^data_governance_tools]: Johns Hopkins Medical Center Technical Report (2022). Data Governance in Chronic Disease Research.<br>[^mayo_clinic_case_study]: Mayo Clinic Proceedings. (2021). Design Patterns for Resilient Chronic Disease Management Systems. 96(8).<br>[^aws_healthlake]: AWS. (2023). HealthLake Security and Compliance Controls.</p> +<p>不过现代云医疗数据库(如AWS HealthLake)已内置此类混合策略:在 OLTP 层保留关键外键,同时提供 FHIR 资源引用的逻辑一致性检查<sup class="footnote-ref"><a href="#fn19" id="fnref19">[19]</a></sup>。开发者可以定期通过 Chaos Engineering 测试数据断裂场景,模拟删除患者记录后检查系统恢复能力。</p> +<h2 id="参考文献:"><a class="header-anchor" href="#参考文献:">¶</a>参考文献:</h2> +<hr class="footnotes-sep"> +<section class="footnotes"> +<ol class="footnotes-list"> +<li id="fn1" class="footnote-item"><p>Gray, J., & Reuter, A. (1993). Transaction Processing: Concepts and Techniques. Morgan Kaufmann. <a href="#fnref1" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn2" class="footnote-item"><p>CockroachDB 团队性能基准测试报告 (2020-2022) <a href="#fnref2" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn3" class="footnote-item"><p>Taft, R., et al. (2020). CockroachDB: The Resilient Geo-Distributed SQL Database. SIGMOD. <a href="#fnref3" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn4" class="footnote-item"><p>Google Cloud Architecture Center. (2022). Designing for Consistency in Distributed Databases. <a href="#fnref4" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn5" class="footnote-item"><p>Fowler, M. (2003). Patterns of Enterprise Application Architecture. Addison-Wesley. <a href="#fnref5" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn6" class="footnote-item"><p>Alibaba Group. (2019). Nacos: A Dynamic Naming and Configuration Service for Cloud Native Applications. <a href="#fnref6" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn7" class="footnote-item"><p>Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly. <a href="#fnref7" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn8" class="footnote-item"><p>Netflix Technology Blog (2018). “When Data Relationships Break: Lessons from Recommendation Systems” <a href="#fnref8" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn9" class="footnote-item"><p>Kleppmann, M. (2020). Transaction Processing in Healthcare Systems. Communications of the ACM, 63(7). <a href="#fnref9" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn10" class="footnote-item"><p>AWS Database Blog. (2021). Logical foreign keys in Amazon Aurora. <a href="#fnref10" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn11" class="footnote-item"><p>Jensen, P. B., et al. (2019). Mining Electronic Health Records: Towards Better Research Applications and Clinical Care. Nature Reviews Genetics. <a href="#fnref11" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn12" class="footnote-item"><p>U.S. Food and Drug Administration. (2023). 21 CFR Part 11: Electronic Records; Electronic Signatures. <a href="#fnref12" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn13" class="footnote-item"><p>Pfizer Clinical Data Science Team. (2022). Annual Data Quality Report. Internal Publication. <a href="#fnref13" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn14" class="footnote-item"><p>HL7 International. (2022). FHIR R4 Clinical Reasoning Module. <a href="#fnref14" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn15" class="footnote-item"><p>NHS Digital. (2022). Data Quality Framework for Healthcare Systems. <a href="#fnref15" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn16" class="footnote-item"><p>ISO/TR 20514:2021. Health informatics — Framework for integrity of health information. <a href="#fnref16" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn17" class="footnote-item"><p>Johns Hopkins Medical Center Technical Report (2022). Data Governance in Chronic Disease Research. <a href="#fnref17" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn18" class="footnote-item"><p>Mayo Clinic Proceedings. (2021). Design Patterns for Resilient Chronic Disease Management Systems. 96(8). <a href="#fnref18" class="footnote-backref">↩︎</a></p> +</li> +<li id="fn19" class="footnote-item"><p>AWS. (2023). HealthLake Security and Compliance Controls. <a href="#fnref19" class="footnote-backref">↩︎</a></p> +</li> +</ol> +</section> </div> |
