<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Melusine Blog</title><description>项目实战 · 源码学习</description><link>https://blog-tyb.pages.dev/</link><templateTheme>Firefly</templateTheme><templateThemeVersion>6.16.8</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>2026年9月25日 02:41:43</lastBuildDate><item><title>一个 HTML 文件的分账工具：bill-splitter 纯函数设计赏析</title><link>https://blog-tyb.pages.dev/posts/bill-splitter-analysis/</link><guid isPermaLink="true">https://blog-tyb.pages.dev/posts/bill-splitter-analysis/</guid><description>分析 bill-splitter 的 __split 纯函数：四条不变量约束、最大余数法零头分配、以及为什么防御性编程在小工具里同样重要。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;bill-splitter 源码赏析：一个 HTML 文件的工程哲学&lt;a href=&quot;#bill-splitter-源码赏析一个-html-文件的工程哲学&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Melusine-ichnose/bill-splitter&quot; target=&quot;_blank&quot;&gt;bill-splitter&lt;/a&gt; 是一个合租账单分摊工具，整个应用只有一个 HTML 文件。但别被体积骗了——它的核心算法设计得非常严谨。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;核心：&lt;code&gt;__split&lt;/code&gt; 纯函数&lt;a href=&quot;#核心__split-纯函数&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;整个应用的计算逻辑收敛在一个函数里：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;__split&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;totalCents&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;weights&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;&lt;span&gt;rest&lt;/span&gt;&lt;span&gt;) → &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;作者给这个函数立了四条&lt;strong&gt;不变量&lt;/strong&gt;（无条件成立）：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;编号&lt;/th&gt;&lt;th&gt;约束&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;I1&lt;/td&gt;&lt;td&gt;长度&lt;/td&gt;&lt;td&gt;返回数组长度 === 权重数组长度&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;I2&lt;/td&gt;&lt;td&gt;整数&lt;/td&gt;&lt;td&gt;每个元素都是整数（金额用分表示）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;I3&lt;/td&gt;&lt;td&gt;守恒&lt;/td&gt;&lt;td&gt;所有元素之和 === totalCents&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;I4&lt;/td&gt;&lt;td&gt;纯&lt;/td&gt;&lt;td&gt;同样输入永远得到同样输出&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这四条不变量就是一份&lt;strong&gt;可执行的规格文档&lt;/strong&gt;。不需要读实现，光看这四条就知道函数的行为边界。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;最大余数法 + 零头集中&lt;a href=&quot;#最大余数法--零头集中&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;分摊算法用的是&lt;strong&gt;最大余数法&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 1. 权重预处理：NaN 和负数 → 0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 2. 退化处理：权重总和为 0 → 全部置 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 3. 按比例取整：floor(总额 × 权重/总权重)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 4. 零头分配：剩余零头全部给余数最大的人&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;举个例子，&lt;code&gt;__split(101, [1, 1, 1])&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// floor(101/3) = 33, 33, 33 → 剩余 2 分&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 余数分别是 0.67, 0.67, 0.67（相等）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 按下标从小到大，第一个承担全部零头&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 结果：[35, 33, 33]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;为什么零头要集中给一个人？&lt;/strong&gt; 作者的理由很实用——房东一眼能看出每月零头算在谁头上。这是工程思维：算法上的「公平」（轮流承担）在这里不如「可解释性」重要。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;防御性边界处理&lt;a href=&quot;#防御性边界处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;函数对异常输入的处理也很教科书：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;__split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;])     &lt;/span&gt;&lt;span&gt;// 权重全0 → 均分 [34, 33, 33]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;__split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;NaN&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt;])  &lt;/span&gt;&lt;span&gt;// NaN/负数 → 0 → [0, 0, 50]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;__split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;])    &lt;/span&gt;&lt;span&gt;// 负金额 → 守恒=-100&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;__split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;])       &lt;/span&gt;&lt;span&gt;// 零金额 → [0, 0, 0]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;每个边界 case 都有明确的行为定义，不抛异常、不返回 NaN。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;架构：数据驱动 + 全量重渲染&lt;a href=&quot;#架构数据驱动--全量重渲染&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;页面架构极其简单：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;配置数据（PERSONS / BILLS / DAYS）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ input 事件&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;render() 全量重渲染&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ 每笔账单调 __split()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;没有框架、没有虚拟 DOM、没有状态管理。因为数据量小（3 人 × 4 笔账单），全量重渲染的性能完全够用。&lt;strong&gt;在正确的场景用正确的复杂度&lt;/strong&gt;——这也是工程能力。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;学到的三件事&lt;a href=&quot;#学到的三件事&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不变量比实现重要。&lt;/strong&gt; 先定义行为约束，再写代码，测试和 review 都有据可依。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金额必须用整数（分）计算。&lt;/strong&gt; 浮点数在金额场景是万恶之源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零头集中不是 bug 是 feature。&lt;/strong&gt; 工程决策要考虑业务可解释性，不只是数学正确性。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Melusine-ichnose/bill-splitter&quot; target=&quot;_blank&quot;&gt;github.com/Melusine-ichnose/bill-splitter&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>DeepStudy 源码解析：三层卷积 CNN 的标准训练范式</title><link>https://blog-tyb.pages.dev/posts/deepstudy-source-analysis/</link><guid isPermaLink="true">https://blog-tyb.pages.dev/posts/deepstudy-source-analysis/</guid><description>拆解 DeepStudy 的 MNIST CNN 实现：三层卷积的尺寸推导、log_softmax+NLLLoss 组合、训练五步循环与完整的保存加载闭环。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;DeepStudy 源码解析：三层卷积 CNN 的标准训练范式&lt;a href=&quot;#deepstudy-源码解析三层卷积-cnn-的标准训练范式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Melusine-ichnose/DeepStudy&quot; target=&quot;_blank&quot;&gt;DeepStudy&lt;/a&gt; 是一个 PyTorch 手写数字识别项目：用三层卷积网络在 MNIST 上做 0–9 分类，官方 README 写明 5 轮训练后测试集准确率约 99%。整个仓库只有两个 Python 文件——&lt;code&gt;SY8.py&lt;/code&gt; 负责模型定义、训练、评估、保存，&lt;code&gt;test.py&lt;/code&gt; 负责加载、推理、可视化，加起来不到两百行。&lt;/p&gt;&lt;p&gt;但读下来你会发现，这两百行把深度学习训练的完整闭环都走了一遍：&lt;strong&gt;定义 → 训练 → 评估 → 保存 → 加载 → 推理 → 可视化&lt;/strong&gt;。麻雀虽小，五脏俱全。这篇笔记记录我认为值得展开的几个点。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;项目结构：一个文件训练，一个文件推理&lt;a href=&quot;#项目结构一个文件训练一个文件推理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;仓库结构非常干净（来自 &lt;a href=&quot;https://github.com/Melusine-ichnose/DeepStudy/blob/main/README.md&quot; target=&quot;_blank&quot;&gt;README.md&lt;/a&gt;）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DeepStudy/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── SY8.py     # 定义 CNN 模型，完成训练、评估、模型保存与样例可视化&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── test.py    # 加载已训练模型，对测试集图像进行预测并以 3×3 网格展示&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── README.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;训练和推理拆成两个文件，但模型定义只有一份——&lt;code&gt;test.py&lt;/code&gt; 通过 &lt;code&gt;from DeepStudy.SY8 import CNN&lt;/code&gt; 直接导入，而不是复制一份过来。这个选择后面再展开，先看模型本身。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;网络定义：为 28×28 量身定制的尺寸推导&lt;a href=&quot;#网络定义为-2828-量身定制的尺寸推导&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;模型定义在 &lt;a href=&quot;https://github.com/Melusine-ichnose/DeepStudy/blob/main/DeepStudy/SY8.py&quot; target=&quot;_blank&quot;&gt;SY8.py&lt;/a&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CNN&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;torch&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;nn&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Module&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;__init__&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;super&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;__init__&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.conv1 &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;Conv2d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;32&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;kernel_size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;stride&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.conv2 &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;Conv2d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;32&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;64&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;kernel_size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;stride&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.conv3 &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;Conv2d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;64&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;128&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;kernel_size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;stride&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.fc1 &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;Linear&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;128&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;128&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.fc2 &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;Linear&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;128&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.pool &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;MaxPool2d&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.dropout &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;Dropout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;forward&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.functional.&lt;/span&gt;&lt;span&gt;relu&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;conv1&lt;/span&gt;&lt;span&gt;(x))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;(x)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.functional.&lt;/span&gt;&lt;span&gt;relu&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;conv2&lt;/span&gt;&lt;span&gt;(x))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;(x)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.functional.&lt;/span&gt;&lt;span&gt;relu&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;conv3&lt;/span&gt;&lt;span&gt;(x))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;(x)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; x.&lt;/span&gt;&lt;span&gt;view&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;128&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.functional.&lt;/span&gt;&lt;span&gt;relu&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fc1&lt;/span&gt;&lt;span&gt;(x))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dropout&lt;/span&gt;&lt;span&gt;(x)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.functional.&lt;/span&gt;&lt;span&gt;log_softmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fc2&lt;/span&gt;&lt;span&gt;(x), &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dim&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;三个卷积层完全同构：3×3 卷积、stride=1、padding=1，也就是说&lt;strong&gt;卷积只换通道数、不动空间尺寸&lt;/strong&gt;，尺寸全靠后面的 MaxPool2d(2, 2) 减半。于是空间维度的演化是一条确定的直线：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;28×28 → pool → 14×14 → pool → 7×7 → pool → 3×3（向下取整）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;理解了这条线，&lt;code&gt;x.view(-1, 128 * 3 * 3)&lt;/code&gt; 里的 &lt;code&gt;3 * 3&lt;/code&gt; 就不是魔法数字，而是 28 连续三次减半（下取整）的结果。代价是这个网络是&lt;strong&gt;为 MNIST 硬编码的&lt;/strong&gt;——输入一变，flatten 处直接 shape mismatch。对一个专攻 MNIST 的教学项目来说，这种取舍完全合理，但如果你要把它迁移到别的数据集，这三处地方（三个 conv 通道数、fc1 的输入维度、view 的参数）得连着改。&lt;/p&gt;&lt;p&gt;另一个细节：Dropout(0.5) 只放在 fc1 之后，三个卷积层一个都没加。这是教科书做法——卷积层的参数在感受野内共享，本身自带正则化，而过参数化最严重的全连接层才是过拟合的重灾区，dropout 该花在刀刃上。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;log_softmax + NLLLoss：一对老搭档&lt;a href=&quot;#log_softmax--nllloss一对老搭档&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;forward&lt;/code&gt; 的最后一层是 &lt;code&gt;log_softmax(self.fc2(x), dim=1)&lt;/code&gt;，而 main 函数里配的损失函数是：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#负对数似然损失函数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;criterion &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.nn.&lt;/span&gt;&lt;span&gt;NLLLoss&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这两个必须成对出现：log_softmax 输出的是&lt;strong&gt;对数概率&lt;/strong&gt;，NLLLoss 做的事情只有一个——取出真实类别对应的那个 log 概率，取负。合起来正好是交叉熵。&lt;/p&gt;&lt;p&gt;初学者常见的疑惑是：为什么不直接 Softmax + CrossEntropyLoss？因为 PyTorch 的 &lt;code&gt;CrossEntropyLoss&lt;/code&gt; 内部本来就是 &lt;code&gt;log_softmax + NLLLoss&lt;/code&gt; 的合体，如果先手动 Softmax 再喂 CrossEntropyLoss，等于 softmax 算了两遍，而且第一遍没有 log-sum-exp 的数值保护，大 logits 下容易溢出。这里的写法是”分开放”的流派：网络自己保证输出语义是 log 概率，损失函数只管挑类别。两种流派都对，唯独”Softmax + CrossEntropyLoss”这个直觉组合是错的。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;训练循环：教科书五步，一个都不能少&lt;a href=&quot;#训练循环教科书五步一个都不能少&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;主训练循环同样在 &lt;a href=&quot;https://github.com/Melusine-ichnose/DeepStudy/blob/main/DeepStudy/SY8.py&quot; target=&quot;_blank&quot;&gt;SY8.py&lt;/a&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; epoch &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;range&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;running_loss &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; i, (x, y) &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enumerate&lt;/span&gt;&lt;span&gt;(train_data):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;optimizer.&lt;/span&gt;&lt;span&gt;zero_grad&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;output &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; net.&lt;/span&gt;&lt;span&gt;forward&lt;/span&gt;&lt;span&gt;(x)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;loss &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;criterion&lt;/span&gt;&lt;span&gt;(output, y)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;loss.&lt;/span&gt;&lt;span&gt;backward&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;optimizer.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;running_loss &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; loss.&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;199&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&lt;span&gt;epoch &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1}&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&lt;span&gt;i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1}&lt;/span&gt;&lt;span&gt;] 损失: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&lt;span&gt;running_loss &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;:.3f&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;running_loss &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;accuracy &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;evaluate&lt;/span&gt;&lt;span&gt;(test_data, net)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;第 &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&lt;span&gt;epoch &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1}&lt;/span&gt;&lt;span&gt; 轮准确率: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&lt;span&gt;accuracy &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;:.2f&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;%&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;zero_grad → forward → loss → backward → step&lt;/code&gt; 这五步是 PyTorch 训练的骨架，其中最容易被初学者漏掉的是第一步。PyTorch 的梯度默认&lt;strong&gt;累积&lt;/strong&gt;：&lt;code&gt;loss.backward()&lt;/code&gt; 是往参数的 &lt;code&gt;.grad&lt;/code&gt; 上做加法，不清零的话，上一个 batch 的梯度会叠加进这个 batch，等于在用一个脏梯度更新参数。这个坑几乎人人踩过，这里写对了。&lt;/p&gt;&lt;p&gt;日志节奏也处理得克制：不是每个 batch 都打印，而是攒够 200 个 batch 打一次平均损失，然后把 &lt;code&gt;running_loss&lt;/code&gt; 清零重新累计。每 200 个 batch 一次、每个 epoch 结束评一次准确率——信息密度刚好够观察训练是否收敛，又不至于刷屏。&lt;/p&gt;&lt;p&gt;优化器与学习率没什么花哨的：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;optimizer &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.optim.&lt;/span&gt;&lt;span&gt;Adam&lt;/span&gt;&lt;span&gt;(net.&lt;/span&gt;&lt;span&gt;parameters&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lr&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0.001&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;evaluate：朴素但正确的评估&lt;a href=&quot;#evaluate朴素但正确的评估&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;evaluate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;test_data&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;net&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;n_correct &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;n_total &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt;&lt;span&gt; torch.&lt;/span&gt;&lt;span&gt;no_grad&lt;/span&gt;&lt;span&gt;():&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (x, y) &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; test_data:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;outputs &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; net.&lt;/span&gt;&lt;span&gt;forward&lt;/span&gt;&lt;span&gt;(x)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; i, output &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enumerate&lt;/span&gt;&lt;span&gt;(outputs):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; torch.&lt;/span&gt;&lt;span&gt;argmax&lt;/span&gt;&lt;span&gt;(output) &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; y[i]:&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;n_correct &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;n_total &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; n_correct &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; n_total&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;两个值得说的点：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;torch.no_grad()&lt;/code&gt; 包住了整个循环。&lt;/strong&gt; 评估不需要梯度，关掉自动求导的图构建能省显存、提速。评估函数忘写 no_grad 是另一个经典失误，尤其配合 &lt;code&gt;retain_graph&lt;/code&gt; 相关的显存爆掉问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐样本的 Python 循环很朴素。&lt;/strong&gt; 每个输出挨个 &lt;code&gt;argmax&lt;/code&gt; 再比较，其实一行向量化就能干完：&lt;code&gt;(outputs.argmax(dim=1) == y).sum().item()&lt;/code&gt;。MNIST 上无所谓，但这是从”能跑”到”会写”的分界线，值得留意。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;比实现本身更有意思的是 main 里的这个调用位置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;初始准确率:&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;evaluate&lt;/span&gt;&lt;span&gt;(test_data, net))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;训练还没开始，先评估一次随机初始化的网络。在 MNIST 十分类上，随机网络准确率应该在 10% 附近——正好是瞎猜的水平。这个 baseline 一举两得：既验证了评估函数没有 bug（如果初始准确率明显不是 10%，多半是评估或数据加载出了问题），又给后面每轮的 99% 一个”从哪爬上来的”参照。很小的习惯，但反映出的工程意识不小时。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;test.py：推理侧的三个细节&lt;a href=&quot;#testpy推理侧的三个细节&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;推理脚本 &lt;a href=&quot;https://github.com/Melusine-ichnose/DeepStudy/blob/main/DeepStudy/test.py&quot; target=&quot;_blank&quot;&gt;test.py&lt;/a&gt; 不长，但每一步都踩在正确的点上：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;load_model&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;model_path&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;model &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CNN&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;model.&lt;/span&gt;&lt;span&gt;load_state_dict&lt;/span&gt;&lt;span&gt;(torch.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;(model_path))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;model.&lt;/span&gt;&lt;span&gt;eval&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; model&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#预测单张图像&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;predict_single_image&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;image_tensor&lt;/span&gt;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt;&lt;span&gt; torch.&lt;/span&gt;&lt;span&gt;no_grad&lt;/span&gt;&lt;span&gt;():&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;output &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt;(image_tensor.&lt;/span&gt;&lt;span&gt;unsqueeze&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;prediction &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; torch.&lt;/span&gt;&lt;span&gt;argmax&lt;/span&gt;&lt;span&gt;(output).&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; prediction&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;细节一：state_dict 保存与加载。&lt;/strong&gt; 训练脚本结束时用的是 &lt;code&gt;torch.save(net.state_dict(), &apos;mnist_cnn.pth&apos;)&lt;/code&gt;——只保存参数张量，不 pickle 整个模型对象。这带来了推理侧的一个必然结果：必须先实例化一个同结构的 &lt;code&gt;CNN()&lt;/code&gt;，再把权重灌进去。这是 PyTorch 官方推荐的保存方式，比 pickle 整个对象更可移植（不依赖源码路径和类定义的 pickle 兼容性）。副作用是模型定义必须单点维护，于是 &lt;code&gt;test.py&lt;/code&gt; 用 &lt;code&gt;from DeepStudy.SY8 import CNN&lt;/code&gt; 导入——定义只有一份，保存和加载永远不会漂移。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;细节二：&lt;code&gt;model.eval()&lt;/code&gt; 不可省。&lt;/strong&gt; 训练时 Dropout(0.5) 会随机置零一半的激活，推理时如果忘了 &lt;code&gt;eval()&lt;/code&gt;，dropout 还在随机丢，预测结果就是随机化的——99% 的准确率会瞬间掉到不可用的水平。这个 bug 不报错、能跑完，只是结果莫名变差，属于最阴险的一类。这里写对了。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;细节三：&lt;code&gt;unsqueeze(0)&lt;/code&gt; 补 batch 维。&lt;/strong&gt; 从数据集里取出的单张图是 &lt;code&gt;(1, 28, 28)&lt;/code&gt; 三维张量，而 &lt;code&gt;Conv2d&lt;/code&gt; 只接受 &lt;code&gt;(batch, channel, h, w)&lt;/code&gt; 四维输入，所以推理前要 &lt;code&gt;unsqueeze(0)&lt;/code&gt; 补上 batch 维变成 &lt;code&gt;(1, 1, 28, 28)&lt;/code&gt;。单样本推理和批量的维度差异，是写推理代码时最常见的报错来源。&lt;/p&gt;&lt;p&gt;顺带一提，README 专门写了这个项目必须这样运行：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 在 DeepStudy 的父目录中执行&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;python&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-m&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DeepStudy.test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;原因正是上面的包导入：&lt;code&gt;from DeepStudy.SY8 import CNN&lt;/code&gt; 是包导入语法，直接在仓库目录里 &lt;code&gt;python test.py&lt;/code&gt; 会 import 失败，必须从父目录以模块方式跑。README 把这个坑提前写清楚了，省了后来者一次报错。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;值得学习的地方&lt;a href=&quot;#值得学习的地方&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;读完整仓源码，我认为有四点值得带走：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;闭环完整。&lt;/strong&gt; 不到两百行，训练、评估、保存、加载、推理、可视化全都有，而且职责边界清楚：&lt;code&gt;SY8.py&lt;/code&gt; 只管”从数据到权重”，&lt;code&gt;test.py&lt;/code&gt; 只管”从权重到预测”。入门项目最怕只写训练不写推理，权重存下来就没人管了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Baseline 意识。&lt;/strong&gt; 训练前先测一次随机准确率，10% 的数字同时验证了评估函数和数据管线。以后写任何训练脚本，我都建议保留这一行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组合拳用得规范。&lt;/strong&gt; log_softmax + NLLLoss、dropout 只放全连接层、state_dict 保存、eval() 切推理模式——这些不是炫技点，而是保证结果可复现、推理不翻车的地基。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把坑写在文档里。&lt;/strong&gt; &lt;code&gt;python -m&lt;/code&gt; 的运行方式、初始准确率预期、每轮准确率预期，README 都提前交代了。代码短，文档不糊弄。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;作为 MNIST 级别的入门项目，DeepStudy 没有任何多余的东西，也没有缺关键的东西——这大概就是对”标准训练范式”四个字最好的注解。&lt;/p&gt;&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Melusine-ichnose/DeepStudy&quot; target=&quot;_blank&quot;&gt;github.com/Melusine-ichnose/DeepStudy&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>如何阅读优秀开源项目的源码</title><link>https://blog-tyb.pages.dev/posts/how-to-read-source-code/</link><guid isPermaLink="true">https://blog-tyb.pages.dev/posts/how-to-read-source-code/</guid><description>分享一套可复用的源码阅读方法论：从宏观架构到核心链路，再到设计模式的落地应用。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;如何阅读优秀开源项目的源码&lt;a href=&quot;#如何阅读优秀开源项目的源码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;阅读源码是程序员进阶的必经之路。但很多人打开一个开源项目，面对成千上万的文件往往无从下手。这篇文章总结我在阅读开源项目时的一套方法论。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;第一步：先用起来，再读源码&lt;a href=&quot;#第一步先用起来再读源码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;永远不要在没有使用过某个框架的情况下直接读它的源码。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;正确姿势：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;先阅读官方文档的 Quick Start，把 demo 跑起来&lt;/li&gt;
&lt;li&gt;在自己的项目里实际使用它，踩几个坑&lt;/li&gt;
&lt;li&gt;带着「它是怎么做到的」这个问题去读源码&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;只有真正用过，你才会对框架的能力边界有体感，读源码时才知道哪些模块是重点。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;第二步：从宏观架构入手&lt;a href=&quot;#第二步从宏观架构入手&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;打开一个项目，不要急着钻进某个类里。先做三件事：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;看目录结构&lt;/strong&gt;：&lt;code&gt;src/main&lt;/code&gt; 下有哪些顶级包？通常包名就是模块划分&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;看 README 和官方架构文档&lt;/strong&gt;：作者通常会介绍核心概念和设计哲学&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;看依赖关系&lt;/strong&gt;：&lt;code&gt;pom.xml&lt;/code&gt; / &lt;code&gt;package.json&lt;/code&gt; 能看出它依赖了哪些底层库&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;以 SpringBoot 为例，顶级结构里 &lt;code&gt;spring-boot-autoconfigure&lt;/code&gt; 就是自动装配的核心，&lt;code&gt;spring-boot-starter-*&lt;/code&gt; 是各种场景入口。先建立这张「地图」，后面才不会迷路。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;第三步：跟踪核心链路&lt;a href=&quot;#第三步跟踪核心链路&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;每个框架都有几条「生命线」级的调用链路。抓住它们，就抓住了框架的骨架。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web 框架&lt;/strong&gt;：一次 HTTP 请求从进来到响应的完整链路&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORM 框架&lt;/strong&gt;：一条 SQL 从方法调用到数据库执行的链路&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RPC 框架&lt;/strong&gt;：一次远程调用的序列化、网络传输、反序列化链路&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;具体做法：找到一个入口方法（比如 Controller 的接口），用 IDE 的「调用层次结构」（Call Hierarchy）功能一路向下追，画出时序图。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;第四步：关注设计模式与取舍&lt;a href=&quot;#第四步关注设计模式与取舍&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;源码阅读的精华在于理解&lt;strong&gt;作者为什么这么设计&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;这里为什么用策略模式而不是 if-else？&lt;/li&gt;
&lt;li&gt;这里为什么用事件驱动而不是直接调用？&lt;/li&gt;
&lt;li&gt;这里的缓存为什么要分两级？&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;推荐边读边记笔记，把这些设计决策摘录下来，思考能不能用到自己的项目里。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;第五步：输出倒逼输入&lt;a href=&quot;#第五步输出倒逼输入&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;读完不写等于白读。输出的形式可以很多：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;写一篇源码解析博客（就像本站的文章）&lt;/li&gt;
&lt;li&gt;给项目提一个小的 PR（修 typo 也好）&lt;/li&gt;
&lt;li&gt;模仿它的设计，自己写一个 mini 版本&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;以 LangGraph 为例，如果你想真正理解它的状态图编排，最好的方式是动手写一个简化版的 Agent 循环执行框架。写的过程中遇到的问题，会逼着你回头把源码彻底吃透。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;推荐几个值得精读的项目&lt;a href=&quot;#推荐几个值得精读的项目&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;/th&gt;&lt;th&gt;语言&lt;/th&gt;&lt;th&gt;为什么值得读&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;SpringBoot&lt;/td&gt;&lt;td&gt;Java&lt;/td&gt;&lt;td&gt;自动装配、条件注解的教科书级实现&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Netty&lt;/td&gt;&lt;td&gt;Java&lt;/td&gt;&lt;td&gt;高性能网络编程、Reactor 模式的最佳实践&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Redis&lt;/td&gt;&lt;td&gt;C&lt;/td&gt;&lt;td&gt;单线程模型、事件驱动、数据结构设计的典范&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LangChain / LangGraph&lt;/td&gt;&lt;td&gt;Python&lt;/td&gt;&lt;td&gt;AI Agent 编排的前沿设计，状态机思想的落地&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Vue 3&lt;/td&gt;&lt;td&gt;TypeScript&lt;/td&gt;&lt;td&gt;响应式系统、编译器优化的现代前端实践&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr /&gt;&lt;p&gt;读源码没有捷径，但有方法。希望这篇文章能帮你少走弯路。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>多商户商城系统源码解析：一笔订单背后的拆单、渲染与兜底</title><link>https://blog-tyb.pages.dev/posts/mall-source-analysis/</link><guid isPermaLink="true">https://blog-tyb.pages.dev/posts/mall-source-analysis/</guid><description>深入一个 B2B2C 多商户商城的 Java 源码，拆解交易-订单两级拆单、购物车渲染流水线、定时兜底与商家端数据隔离四个核心设计。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;多商户商城系统源码解析：一笔订单背后的拆单、渲染与兜底&lt;a href=&quot;#多商户商城系统源码解析一笔订单背后的拆单渲染与兜底&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System&quot; target=&quot;_blank&quot;&gt;Multi-merchant-Mall-System&lt;/a&gt; 是一个 B2B2C 多商户商城系统的后端仓库：Java + Spring Boot，九个后端模块、一千五百多个源文件，消息队列用 RocketMQ，搜索用 Elasticsearch，定时任务用 XXL-Job。翻代码的时候能明显看出这套代码与开源项目 lilishop 同源——包名是 &lt;code&gt;cn.lili&lt;/code&gt;，类注释里的署名是 Chopper、paulG 这些 lilishop 的作者。这篇文章不管归属，就把它当成一份完整的 B2B2C 教材，记录我读源码时认为最值得学的几个设计。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;一、模块划分：一个角色一个 API 服务&lt;a href=&quot;#一模块划分一个角色一个-api-服务&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;先看顶层结构。仓库不是单体大杂烩，而是按角色切了模块：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;admin          独立的管理端启动器&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;buyer-api      买家端接口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;seller-api     商家端接口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;manager-api    平台运营端接口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;common-api     公共接口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;consumer       消息消费者 + 定时任务&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;framework      实体、服务层等公共业务代码&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;im-api         即时通讯&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;xxl-job        分布式调度&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;业务代码几乎全下沉在 &lt;code&gt;framework&lt;/code&gt; 里，三个 API 模块只是薄薄的 Controller 层。买家、商家、平台三个角色各走各的服务，物理上就隔离开了——商家端被打穿不会波及买家端。这个拆法在商城这类多角色系统里非常实用。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;二、多商户的根基：Trade → Order 两级拆单&lt;a href=&quot;#二多商户的根基trade--order-两级拆单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;多商户商城和普通商城最本质的区别是：&lt;strong&gt;一次下单可能跨多个店铺，必须按店铺拆单&lt;/strong&gt;。这套代码的建模方式是”交易（Trade）- 订单（Order）“两级结构。&lt;/p&gt;&lt;p&gt;&lt;code&gt;TradeDTO&lt;/code&gt;（贯穿下单流程的视图对象）里有两条线索。一个是平台券和店铺券分开建模（来自 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/entity/dto/TradeDTO.java&quot; target=&quot;_blank&quot;&gt;TradeDTO.java&lt;/a&gt;）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 使用平台优惠券，一笔订单只能使用一个平台优惠券&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MemberCouponDTO&lt;/span&gt;&lt;span&gt; platformCoupon&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* key 为商家id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* value 为商家优惠券&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 店铺优惠券&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MemberCouponDTO&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; storeCoupons&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;平台券一张订单只能用一张，店铺券按商家 ID 分组、每个店铺各用各的——优惠券体系从数据结构上就按”平台/商户”分了层。&lt;/p&gt;&lt;p&gt;拆单发生在购物车阶段：购物车里每个 &lt;code&gt;CartVO&lt;/code&gt; 就是一个店铺的商品集合，下单时一个 &lt;code&gt;CartVO&lt;/code&gt; 生成一个 &lt;code&gt;Order&lt;/code&gt;。所以 &lt;code&gt;Order&lt;/code&gt; 实体里直接冗余了店铺字段（来自 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/entity/dos/Order.java&quot; target=&quot;_blank&quot;&gt;Order.java&lt;/a&gt;）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ApiModelProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;交易编号 关联Trade&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; tradeSn&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ApiModelProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;店铺ID&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; storeId&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ApiModelProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;店铺名称&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; storeName&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;入库的实现在 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/serviceimpl/OrderServiceImpl.java&quot; target=&quot;_blank&quot;&gt;OrderServiceImpl.java&lt;/a&gt; 的 &lt;code&gt;intoDB&lt;/code&gt;，整个方法包在一个事务里：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rollbackFor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;intoDB&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TradeDTO&lt;/span&gt;&lt;span&gt; tradeDTO) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//检查TradeDTO信息&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;checkTradeDTO&lt;/span&gt;&lt;span&gt;&lt;span&gt;(tradeDTO)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//存放购物车，即业务中的订单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; orders &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;tradeDTO&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCartList&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//存放自订单/订单日志&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;OrderItem&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; orderItems &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;OrderLog&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; orderLogs &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//循环购物车&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;tradeDTO&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCartList&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(item &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;(item, tradeDTO);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;orders&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(order);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//批量保存订单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;saveBatch&lt;/span&gt;&lt;span&gt;(orders);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//批量保存 子订单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;orderItemService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;saveBatch&lt;/span&gt;&lt;span&gt;(orderItems);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//批量记录订单操作日志&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;orderLogService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;saveBatch&lt;/span&gt;&lt;span&gt;(orderLogs);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;而支付是按交易维度进行的，&lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/serviceimpl/TradeServiceImpl.java&quot; target=&quot;_blank&quot;&gt;TradeServiceImpl.java&lt;/a&gt; 的 &lt;code&gt;payTrade&lt;/code&gt; 把一笔交易下的所有订单逐个推进支付流程：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rollbackFor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;payTrade&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; tradeSn&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; paymentName&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; receivableNo) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; orderQueryWrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;orderQueryWrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTradeSn, tradeSn);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; orders &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;(orderQueryWrapper);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt; order &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; orders) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;orderService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;payOrder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSn&lt;/span&gt;&lt;span&gt;(), paymentName, receivableNo);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;用户付一次钱，N 个店铺的订单各自流转各自的发货、售后。这就是 B2B2C 的”一次支付、拆单履约”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;三、价格计算：可编排的渲染流水线&lt;a href=&quot;#三价格计算可编排的渲染流水线&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;价格计算是商城里最乱的部分——商品促销、满减、优惠券、运费、佣金层层叠加，还要区分购物车展示、结算页、各种营销下单等不同场景。这套代码的解法我特别喜欢：&lt;strong&gt;把每一步计算做成一个 Bean，用数组声明流水线的顺序&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;先定义步骤接口（&lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/render/CartRenderStep.java&quot; target=&quot;_blank&quot;&gt;CartRenderStep.java&lt;/a&gt;）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartRenderStep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TradeDTO&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tradeDTO&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;然后每种场景声明自己的步骤组合（&lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/render/RenderStepStatement.java&quot; target=&quot;_blank&quot;&gt;RenderStepStatement.java&lt;/a&gt;）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 交易创建前渲染&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 渲染购物车 生成SN 》分销人员佣金渲染 》平台佣金渲染&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;[] tradeRender &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CHECKED_FILTER&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CHECK_DATA&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SKU_PROMOTION&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;FULL_DISCOUNT&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;COUPON&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SKU_FREIGHT&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CART_PRICE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CART_SN&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DISTRIBUTION&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PLATFORM_COMMISSION&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;执行器是 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/render/TradeBuilder.java&quot; target=&quot;_blank&quot;&gt;TradeBuilder.java&lt;/a&gt;，利用 Spring 把所有 &lt;code&gt;CartRenderStep&lt;/code&gt; 实现类注入成列表，再按声明的顺序依次调用：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;renderCartBySteps&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TradeDTO&lt;/span&gt;&lt;span&gt;&lt;span&gt; tradeDTO&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt;[] defaultRender) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;RenderStepEnums&lt;/span&gt;&lt;span&gt; step &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; defaultRender) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;CartRenderStep&lt;/span&gt;&lt;span&gt; render &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; cartRenderSteps) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(step)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;(tradeDTO);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;ServiceException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt;&lt;span&gt; e&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;购物车{}渲染异常：&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getClass&lt;/span&gt;&lt;span&gt;(), e);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;不同场景复用不同流水线：购物车展示只要校验、促销、满减、算价四步；结算页要加上优惠券和运费；积分/砍价这类单品购买跳过满减；普通下单则要再追加流水号、分销佣金、平台佣金。新增一种营销玩法，写一个 &lt;code&gt;CartRenderStep&lt;/code&gt; 实现类、往数组里插一个枚举就行，价格计算这个”重灾区”被治理得井井有条。&lt;/p&gt;&lt;p&gt;那段异常处理也值得品：&lt;code&gt;ServiceException&lt;/code&gt; 是业务校验失败（比如商品已下架），必须中断下单直接抛出；而其他未知异常只记日志、继续渲染——展示购物车时一个非关键步骤挂掉不应该让整个购物车打不开。区分”必须失败的异常”和”可以带病运行的异常”，这是业务系统里很见功力的细节。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;四、未支付订单的自动关闭：定时任务兜底&lt;a href=&quot;#四未支付订单的自动关闭定时任务兜底&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;和上一篇座位预约系统一样，这套代码对”应该发生但用户没做”的事情也用了定时任务兜底。订单超时未支付要自动释放库存、关闭订单，实现在 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/consumer/src/main/java/cn/lili/timetask/handler/impl/order/CancelOrderTaskExecute.java&quot; target=&quot;_blank&quot;&gt;CancelOrderTaskExecute.java&lt;/a&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;execute&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Setting&lt;/span&gt;&lt;span&gt; setting &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;settingService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SettingEnum&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ORDER_SETTING&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;OrderSetting&lt;/span&gt;&lt;span&gt; orderSetting &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JSONUtil&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;setting&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSettingValue&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;OrderSetting&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (orderSetting &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderSetting&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAutoCancel&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//订单自动取消时间 = 当前时间 - 自动取消时间分钟数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;DateTime&lt;/span&gt;&lt;span&gt; cancelTime &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;DateUtil&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;offsetMinute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;DateUtil&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderSetting&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAutoCancel&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; queryWrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;queryWrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getOrderStatus, &lt;/span&gt;&lt;span&gt;OrderStatusEnum&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UNPAID&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//订单创建时间 &amp;lt;= 订单自动取消时间&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;queryWrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;le&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime, cancelTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; list &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;(queryWrapper);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; cancelSnList &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getSn).&lt;/span&gt;&lt;span&gt;collect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Collectors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toList&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; sn &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; cancelSnList) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;orderService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;systemCancel&lt;/span&gt;&lt;span&gt;(sn, &lt;/span&gt;&lt;span&gt;&quot;超时未支付自动取消&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;三个细节：超时时长从系统设置表里读而不是写死在代码里，运营可以在后台调；查询条件是”状态 = UNPAID 且创建时间早于临界点”，扫描完逐单走统一的 &lt;code&gt;systemCancel&lt;/code&gt; 流程（而不是直接改状态，保证取消副作用一致）；任务类实现 &lt;code&gt;EveryMinuteExecute&lt;/code&gt; 接口，由 consumer 模块统一驱动。订单状态本身则收敛在 &lt;code&gt;OrderStatusEnum&lt;/code&gt;：&lt;code&gt;UNPAID → PAID → UNDELIVERED → PARTS_DELIVERED → DELIVERED → COMPLETED&lt;/code&gt;，外加待自提、待核验、已关闭，所有操作都翻译成状态流转。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;五、下单事件：事务提交后再发消息&lt;a href=&quot;#五下单事件事务提交后再发消息&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;下单成功后的积分扣减、砍价收尾、消息通知这些周边动作，没有塞在下单事务里，而是走 RocketMQ 异步。这里有个容易踩坑的时序问题：&lt;strong&gt;如果事务还没提交就把消息发出去，消费者可能读到不存在的数据&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/serviceimpl/TradeServiceImpl.java&quot; target=&quot;_blank&quot;&gt;TradeServiceImpl.java&lt;/a&gt; 的处理分两步。先把 &lt;code&gt;TradeDTO&lt;/code&gt; 整体写进缓存，MQ 消息体只携带一个 key：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;//写入缓存，给消费者调用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(key, &lt;/span&gt;&lt;span&gt;JSONUtil&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toJsonStr&lt;/span&gt;&lt;span&gt;(tradeDTO));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;applicationEventPublisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;publishEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TransactionCommitSendMQEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;订单创建消息&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rocketmqCustomProperties&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOrderTopic&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;OrderTagsEnum&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ORDER_CREATE&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;(), key));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;TransactionCommitSendMQEvent&lt;/code&gt; 借助 Spring 事件机制，把真正的 MQ 发送推迟到事务提交之后——避免”消息先到、数据未落库”。消费者端 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/consumer/src/main/java/cn/lili/listener/OrderMessageListener.java&quot; target=&quot;_blank&quot;&gt;OrderMessageListener.java&lt;/a&gt; 再从缓存取回完整数据：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; ORDER_CREATE&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;messageExt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getBody&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;TradeDTO&lt;/span&gt;&lt;span&gt; tradeDTO &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JSONUtil&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getString&lt;/span&gt;&lt;span&gt;(key), &lt;/span&gt;&lt;span&gt;TradeDTO&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;TradeEvent&lt;/span&gt;&lt;span&gt; event &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; tradeEvent) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;event&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderCreate&lt;/span&gt;&lt;span&gt;(tradeDTO);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;交易{}入库,在{}业务中，状态修改事件执行异常&quot;&lt;/span&gt;&lt;span&gt;, ...);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//如所有步骤顺利完成&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Boolean&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;TRUE&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(result)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;//清除记录信息的trade cache key&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(key);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;注入的是 &lt;code&gt;List&amp;lt;TradeEvent&amp;gt;&lt;/code&gt;——每个下游业务实现同一个事件接口，消费者遍历广播，谁都不依赖谁。任何一个业务失败就保留缓存 key 不删除，配合消息重试还能再跑一轮。订单状态变更（&lt;code&gt;STATUS_CHANGE&lt;/code&gt;）同理广播给所有 &lt;code&gt;OrderStatusChangeEvent&lt;/code&gt; 实现。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;六、商家端的数据隔离：每个查询都带着 storeId&lt;a href=&quot;#六商家端的数据隔离每个查询都带着-storeid&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;多商户系统最敏感的问题是越权：A 商家能不能改 B 商家的订单？这套代码的答案是把”判断归属”做成了惯用法。商家端控制器 &lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/seller-api/src/main/java/cn/lili/controller/order/OrderStoreController.java&quot; target=&quot;_blank&quot;&gt;OrderStoreController.java&lt;/a&gt; 里，凡是按订单号操作的地方都先过一道 &lt;code&gt;OperationalJudgment.judgment&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;/{orderSn}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ResultMessage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;OrderDetailVO&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;detail&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;NotNull&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;PathVariable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; orderSn) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;OperationalJudgment&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;judgment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;orderService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getBySn&lt;/span&gt;&lt;span&gt;(orderSn));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ResultUtil&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;orderService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;queryDetail&lt;/span&gt;&lt;span&gt;(orderSn));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;服务层内部则统一从 &lt;code&gt;UserContext&lt;/code&gt; 取当前登录商家的店铺 ID 作为查询条件，比如核验自提订单：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; storeId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Objects&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requireNonNull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserContext&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCurrentUser&lt;/span&gt;&lt;span&gt;()).&lt;/span&gt;&lt;span&gt;getStoreId&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt; order &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOne&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getOrderStatus, &lt;/span&gt;&lt;span&gt;OrderStatusEnum&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;TAKE&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;OrderStatusEnum&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;STAY_PICKED_UP&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getStoreId, storeId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(Order&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getVerificationCode, verificationCode));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;没有做复杂的行级权限框架，就是最朴素的原则：&lt;strong&gt;商家端所有查询都显式带 storeId 条件&lt;/strong&gt;。防重复提交则统一挂在方法注解上（&lt;code&gt;@PreventDuplicateSubmissions&lt;/code&gt;），发货、改价、取消这些写操作全都加了一遍。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;值得学习的地方&lt;a href=&quot;#值得学习的地方&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;按角色拆 API 服务，业务下沉 framework。&lt;/strong&gt; 多角色系统的天然切分线就是角色，物理隔离比一堆 if-else 判断身份可靠得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade → Order 两级模型支撑拆单。&lt;/strong&gt; 平台券/店铺券分层、订单冗余店铺信息、支付按交易、履约按订单，“多商户”不是一个字段，而是一整套建模。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渲染流水线治理价格计算。&lt;/strong&gt; 步骤即 Bean、顺序即数组，扩展营销玩法不改老代码；同时区分业务异常（中断）与系统异常（降级继续）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事务提交后再发消息 + 缓存中转大报文。&lt;/strong&gt; 消息只传 key，时序正确性和消息体大小两个问题一起解决。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据隔离靠纪律而不是框架。&lt;/strong&gt; 商家端每个查询显式带 storeId，归属校验做成 &lt;code&gt;OperationalJudgment&lt;/code&gt; 这样的惯用法，简单但有效。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;作为一个学习样本，它把 B2B2C 商城会遇到的典型问题——拆单、价格叠加、异步履约、越权防护——都给出了一种可参考的工程解法，值得通读一遍。&lt;/p&gt;&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Melusine-ichnose/Multi-merchant-Mall-System&quot; target=&quot;_blank&quot;&gt;github.com/Melusine-ichnose/Multi-merchant-Mall-System&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>源码阅读笔记：Seat Reservation System 的定时任务与状态机设计</title><link>https://blog-tyb.pages.dev/posts/seat-reservation-analysis/</link><guid isPermaLink="true">https://blog-tyb.pages.dev/posts/seat-reservation-analysis/</guid><description>分析自习室座位预约系统的核心设计：座位状态机流转、TimerTask 定时异常处理、信用分奖惩机制。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;Seat Reservation System 源码阅读笔记&lt;a href=&quot;#seat-reservation-system-源码阅读笔记&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Melusine-ichnose/Seat-Reservation-System&quot; target=&quot;_blank&quot;&gt;Seat Reservation System&lt;/a&gt; 是一个前后端分离的自习室座位预约系统（Spring Boot 2.7 + Vue 2）。这篇文章记录我阅读它的源码时，认为最值得学习的三个设计。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;一、座位状态机：用数字流转约束业务规则&lt;a href=&quot;#一座位状态机用数字流转约束业务规则&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;自习室座位的核心是一个状态机。&lt;code&gt;reservation&lt;/code&gt; 表的 &lt;code&gt;status&lt;/code&gt; 字段定义了完整流转：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;状态值&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;th&gt;触发动作&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;待签到&lt;/td&gt;&lt;td&gt;用户预约成功&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;使用中&lt;/td&gt;&lt;td&gt;到场扫码签到&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;未及时签到&lt;/td&gt;&lt;td&gt;超时自动判违规&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;暂离&lt;/td&gt;&lt;td&gt;用户主动暂离&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;暂离超时&lt;/td&gt;&lt;td&gt;定时任务检测&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;-1&lt;/td&gt;&lt;td&gt;完成&lt;/td&gt;&lt;td&gt;使用结束，释放座位&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这个设计的价值在于：&lt;strong&gt;所有业务操作最终都收敛为状态变更&lt;/strong&gt;。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用户签到 → &lt;code&gt;0 → 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用户暂离 → &lt;code&gt;1 → 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用户回来 → &lt;code&gt;3 → 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;暂离超时 → &lt;code&gt;3 → 4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用结束 → &lt;code&gt;1 → -1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;每个状态变更都绑定一个副作用（释放座位 / 扣信用分 / 记违规），代码路径非常清晰。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;二、TimerTask：异常场景的自动兜底&lt;a href=&quot;#二timertask异常场景的自动兜底&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;系统最复杂的地方不是正常流程，而是&lt;strong&gt;异常场景&lt;/strong&gt;。预约了不来怎么办？暂离了一直不回来怎么办？&lt;/p&gt;&lt;p&gt;项目的答案是 &lt;code&gt;TimerTask&lt;/code&gt; 定时任务：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 伪代码示意：预约超时未签到自动判违规&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;TimerTask&lt;/span&gt;&lt;span&gt; checkSignTimeout &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TimerTask&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 找到所有 status=0 且超过签到时间的预约&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Reservation&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt; &lt;/span&gt;&lt;span&gt;timeoutList&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; reservationMapper&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findTimeoutReservations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Reservation&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; timeoutList) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 未及时签到&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;seatService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;release&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSeatId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;creditService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;deduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 扣信用分&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;violationService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;&quot;超时未签到&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;定时任务每 N 分钟扫一次，把「应该发生但用户没做」的事情自动补掉。这是典型的&lt;strong&gt;最终一致性&lt;/strong&gt;思路——不依赖用户操作，系统自己兜底。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;三、信用分：约束行为的奖惩机制&lt;a href=&quot;#三信用分约束行为的奖惩机制&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;信用分是这个系统里很巧妙的约束设计：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;初始 100 分&lt;/li&gt;
&lt;li&gt;超时未签到 → 扣分&lt;/li&gt;
&lt;li&gt;暂离超时 → 扣分&lt;/li&gt;
&lt;li&gt;教师可手动加/扣分&lt;/li&gt;
&lt;li&gt;信用分过低 → 限制预约权限&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;它不是简单的惩罚，而是&lt;strong&gt;用分数门槛间接控制资源分配&lt;/strong&gt;。分数低的人自动失去预约资格，不需要管理员手动拉黑。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;前端：双 UI 库的组合&lt;a href=&quot;#前端双-ui-库的组合&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这个项目的前端架构也值得注意：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PC 端管理界面&lt;/strong&gt; → Element UI 2.15（给管理员/教师用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动端 H5&lt;/strong&gt; → Vant 2.12（给学生用）&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;同一套后端 API，根据设备类型加载不同的 UI 库。Axios 统一封装，通过 &lt;code&gt;devServer.proxy&lt;/code&gt; 把 &lt;code&gt;/api&lt;/code&gt; 代理到后端 9003 端口。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这个项目的复杂度不高，但&lt;strong&gt;异常处理的设计意识&lt;/strong&gt;很到位。定时任务兜底、状态机约束流转、信用分约束行为，这三板斧在很多业务系统里都适用。&lt;/p&gt;&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Melusine-ichnose/Seat-Reservation-System&quot; target=&quot;_blank&quot;&gt;github.com/Melusine-ichnose/Seat-Reservation-System&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>TaskWeaver 源码解析：LangGraph 四节点循环 Agent 架构</title><link>https://blog-tyb.pages.dev/posts/taskweaver-source-analysis/</link><guid isPermaLink="true">https://blog-tyb.pages.dev/posts/taskweaver-source-analysis/</guid><description>深入 TaskWeaver 源码，拆解 Planner-Executor-Reviewer-Responder 四节点循环图的实现，以及它如何用双计数器防止 Agent 死循环。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;TaskWeaver 源码解析：LangGraph 四节点循环 Agent 架构&lt;a href=&quot;#taskweaver-源码解析langgraph-四节点循环-agent-架构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Melusine-ichnose/TaskWeaver&quot; target=&quot;_blank&quot;&gt;TaskWeaver&lt;/a&gt; 是一个基于 LangChain + LangGraph + MCP 协议 + FastAPI 的生产级 AI Agent 服务。这篇文章带你读它的核心源码，看看一个可落地的 Agent 循环是怎么搭起来的。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;整体架构：一张循环图&lt;a href=&quot;#整体架构一张循环图&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;项目的灵魂在 &lt;code&gt;app/agent/graph.py&lt;/code&gt;，整个 Agent 就是一张 LangGraph 状态图：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;START → planner → executor → reviewer ┐&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▲               │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── not done ───┤&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;done → responder → END&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;对应源码（来自 &lt;a href=&quot;https://github.com/Melusine-ichnose/TaskWeaver/blob/main/app/agent/graph.py&quot; target=&quot;_blank&quot;&gt;graph.py&lt;/a&gt;）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;StateGraph&lt;/span&gt;&lt;span&gt;(AgentState)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_node&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;planner&quot;&lt;/span&gt;&lt;span&gt;, planner_node)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_node&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;executor&quot;&lt;/span&gt;&lt;span&gt;, executor_node)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_node&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;reviewer&quot;&lt;/span&gt;&lt;span&gt;, reviewer_node)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_node&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;responder&quot;&lt;/span&gt;&lt;span&gt;, responder_node)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_edge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;START&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;planner&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_edge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;planner&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;executor&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_edge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;executor&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;reviewer&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 条件边：reviewer 决定继续执行还是收尾&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_conditional_edges&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;reviewer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;_route_after_review,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;executor&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;executor&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;responder&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;responder&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;graph.&lt;/span&gt;&lt;span&gt;add_edge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;responder&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;四个节点的职责划分非常清晰：&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;节点&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;th&gt;类比&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Planner&lt;/td&gt;&lt;td&gt;把用户问题拆解成步骤清单&lt;/td&gt;&lt;td&gt;项目经理排计划&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Executor&lt;/td&gt;&lt;td&gt;执行当前步骤，调工具或直接产出&lt;/td&gt;&lt;td&gt;干活的工程师&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Reviewer&lt;/td&gt;&lt;td&gt;评审当前步骤是否完成&lt;/td&gt;&lt;td&gt;质检/Code Review&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Responder&lt;/td&gt;&lt;td&gt;汇总所有步骤产出，生成最终回答&lt;/td&gt;&lt;td&gt;写交付报告&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;状态设计：AgentState&lt;a href=&quot;#状态设计agentstate&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;LangGraph 的核心思想是&lt;strong&gt;节点之间不直接传参，而是读写共享状态&lt;/strong&gt;。&lt;code&gt;app/agent/state.py&lt;/code&gt; 定义了这个共享状态：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AgentState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TypedDict&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;messages: Annotated[list[BaseMessage], add_messages]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user_query: &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;plan: list[&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;current_step_index: &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;step_results: list[&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;is_complete: &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;final_response: &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;review_iterations: &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 防死循环计数器&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;两个值得注意的设计：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;messages&lt;/code&gt; 用了 &lt;code&gt;add_messages&lt;/code&gt; reducer&lt;/strong&gt;：LangGraph 默认对状态字段是覆盖更新，但 &lt;code&gt;messages&lt;/code&gt; 声明了 reducer 后，每个节点返回的新消息会&lt;strong&gt;自动追加合并&lt;/strong&gt;而不是覆盖，对话历史就这样自然累积起来了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;review_iterations&lt;/code&gt; 显式计数&lt;/strong&gt;：这是防止 Agent 死循环的关键，后面详细说。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;Reviewer：最值得学的节点&lt;a href=&quot;#reviewer最值得学的节点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;app/agent/nodes/reviewer.py&lt;/code&gt; 是整个项目里工程思考最密集的部分。它要回答一个问题：&lt;strong&gt;当前步骤算完成了吗？&lt;/strong&gt;&lt;/p&gt;&lt;section&gt;&lt;h3&gt;三层防护，防死循环烧 Token&lt;a href=&quot;#三层防护防死循环烧-token&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;MAX_REVIEW_ITERATIONS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;12&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 单次图执行最大评审次数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;MAX_STEP_RETRIES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# 单步最大重试次数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;第一层：总评审次数硬上限。&lt;/strong&gt; 达到 12 次直接强制收尾，而且这个判断&lt;strong&gt;先于 LLM 调用&lt;/strong&gt;，不消耗任何 token：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; iterations &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MAX_REVIEW_ITERATIONS&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&quot;review_iterations&quot;&lt;/span&gt;&lt;span&gt;: iterations, &lt;/span&gt;&lt;span&gt;&quot;is_complete&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;第二层：单步重试上限。&lt;/strong&gt; 同一步骤连续 2 次评审不过，强制推进到下一步——宁可带着不完美的结果往前走，也不在一个步骤上空转：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;step_retries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; state.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;step_retry_count&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; step_retries &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MAX_STEP_RETRIES&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 强制推进&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&quot;current_step_index&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;: next_index, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;第三层：LLM 输出兜底的「安全侧」判定。&lt;/strong&gt; Reviewer 让 LLM 输出 DONE/CONTINUE，但 LLM 的输出你永远不能完全信任。这里的取舍是：&lt;strong&gt;无法判定时默认完成&lt;/strong&gt;——&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; has_done:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;is_step_done &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;elif&lt;/span&gt;&lt;span&gt; has_continue:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;is_step_done &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 无法识别时默认完成（安全侧，防死循环烧 token）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;is_step_done &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;为什么默认完成而不是默认继续？因为「继续」意味着再跑一轮 Executor → 再调一次 LLM → 再烧一轮 token，而且可能永远不满足。默认完成是&lt;strong&gt;成本安全侧&lt;/strong&gt;的选择。这是做 Agent 系统和写普通业务代码思路差别最大的地方：你要时刻假设 LLM 会给出无法解析的输出。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;单步任务快速通道：省掉一次 LLM 调用&lt;a href=&quot;#单步任务快速通道省掉一次-llm-调用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;对于单步任务（&lt;code&gt;len(plan) == 1&lt;/code&gt;），reviewer 先跑一个&lt;strong&gt;不调 LLM&lt;/strong&gt; 的本地判定：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_autocomplete_single_step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;messages&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;) -&amp;gt; &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;last &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; messages[&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isinstance&lt;/span&gt;&lt;span&gt;(last, ToolMessage):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; content.&lt;/span&gt;&lt;span&gt;startswith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;错误&quot;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 工具失败，交给 LLM 评审&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# 工具成功 → 完成&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isinstance&lt;/span&gt;&lt;span&gt;(last, AIMessage):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; last.content &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; last.tool_calls:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# Executor 已直接产出答案 → 完成&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;# 不确定 → 走 LLM 评审&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;逻辑很朴素：Executor 刚跑完，如果消息序列末尾是一个成功的工具结果或一段正常的 AI 文本，这一步就完成了，没必要再问一次 LLM「你觉得完成了吗」。就这么一个判断，把简单请求的 LLM 调用次数砍掉了一大半。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;Executor：工具调用的经典模式&lt;a href=&quot;#executor工具调用的经典模式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;executor_node&lt;/code&gt; 展示了 LangChain 体系下标准的工具调用循环：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;response: AIMessage &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; llm.&lt;/span&gt;&lt;span&gt;ainvoke&lt;/span&gt;&lt;span&gt;(messages)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;tool_calls &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getattr&lt;/span&gt;&lt;span&gt;(response, &lt;/span&gt;&lt;span&gt;&quot;tool_calls&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; tool_call &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; tool_calls:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tool &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;next&lt;/span&gt;&lt;span&gt;((t &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; t &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; tools &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; t.name &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; tool_call[&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;]), &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; tool &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;content &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;错误：未找到工具 &apos;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;tool_call[&lt;/span&gt;&lt;span&gt;&apos;name&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&apos;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;&lt;span&gt; tool.&lt;/span&gt;&lt;span&gt;ainvoke&lt;/span&gt;&lt;span&gt;(tool_call[&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;content &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;(result)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; e:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;content &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;工具执行错误: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new_messages.&lt;/span&gt;&lt;span&gt;append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ToolMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;content&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;content, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;要点：&lt;strong&gt;工具的异常从不向上抛，而是包成错误文本写进 ToolMessage&lt;/strong&gt;。这样 LLM 下一轮能「看到」工具失败了，有机会自己换参数重试或换个工具——把错误也变成 LLM 可利用的上下文。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;学到的三件事&lt;a href=&quot;#学到的三件事&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agent 的可靠性来自工程防护，不是 Prompt。&lt;/strong&gt; 双计数器 + 兜底判定这些「无聊」的代码，才是生产环境和玩具 Demo 的分水岭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把 LLM 放在路径的关键决策点上，其他地方尽量不调。&lt;/strong&gt; 单步快速通道省的是真金白银的 token 和延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态图（StateGraph）天然适合表达 Agent 循环。&lt;/strong&gt; 比起手写 while 循环，图结构让「评审不过就回到 Executor」这种回边表达得直白且可视化（配合 LangSmith 能直接看到每次循环的轨迹）。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Melusine-ichnose/TaskWeaver&quot; target=&quot;_blank&quot;&gt;github.com/Melusine-ichnose/TaskWeaver&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item></channel></rss>