<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>个人博客</title><description>JIAN</description><link>https://benjian.xyz/</link><templateTheme>Firefly</templateTheme><templateThemeVersion>6.15.8</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>2026年9月12日 10:11:21</lastBuildDate><item><title>Via错题本：把英语错题整理做成一个离线 App</title><link>https://benjian.xyz/posts/via-mistake-book/</link><guid isPermaLink="true">https://benjian.xyz/posts/via-mistake-book/</guid><description>解析英语试卷 DOCX 结构、整理错题并生成可导出错题本的 Web 与 Android 工具。</description><pubDate>Fri, 04 Sep 2026 10:40:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;Via错题本：把英语错题整理做成一个离线 App&lt;a href=&quot;#via错题本把英语错题整理做成一个离线-app&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;跟 Via 老师学英语，也把每一次错题变成下一次进步的素材。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;网址：mistake.viaenglish.xyz&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;Via错题本是一个面向英语学习场景的错题整理工具。它可以读取学生上传的英语试卷 DOCX 文件，根据试卷结构识别 Part、Passage 和题号，再按学生选择的错题范围生成一份新的错题本 DOCX 文档。&lt;/p&gt;&lt;p&gt;这个项目最初是一个浏览器网页工具，后来在保留网站版本的基础上，增加了 Android App。App 使用 WebView 承载网页界面，同时通过 Android 原生能力完成文件选择和文档保存。&lt;/p&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;section&gt;&lt;h3&gt;1. 选择学生和排版方式&lt;a href=&quot;#1-选择学生和排版方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;用户输入学生姓名，并选择错题本的生成方式：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;按题型分类&lt;/li&gt;
&lt;li&gt;按考试分类&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 上传英语试卷&lt;a href=&quot;#2-上传英语试卷&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;工具支持选择 &lt;code&gt;.docx&lt;/code&gt; 格式的英语试卷。文件在本地解析，不需要上传到服务器。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 识别试卷结构&lt;a href=&quot;#3-识别试卷结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;程序会从 DOCX 内部的 XML 结构中读取段落、表格和图片，并尝试识别：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Part&lt;/li&gt;
&lt;li&gt;Passage&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;li&gt;文章内容&lt;/li&gt;
&lt;li&gt;题目中的图片&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 按题号添加错题&lt;a href=&quot;#4-按题号添加错题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;21, 22, 24&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;36-45&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;21, 24, 36-45&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;/section&gt;&lt;section&gt;&lt;h3&gt;5. 错题预览&lt;a href=&quot;#5-错题预览&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;App 将操作区和预览区拆成两个界面：&lt;/p&gt;&lt;ul&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;h3&gt;6. 生成并导出 DOCX&lt;a href=&quot;#6-生成并导出-docx&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;程序会基于内置的 DOCX 模板生成错题本，保留文字、表格和图片。Android App 点击“生成错题本”后，会调用系统保存界面，由用户选择文件保存位置。&lt;/p&gt;&lt;/section&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;App 的界面遵循“少说明、快操作”的原则：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;主标题使用“Via错题本”&lt;/li&gt;
&lt;li&gt;标题下方显示“跟Via老师学英语”&lt;/li&gt;
&lt;li&gt;右上角放置 Via 老师 Logo&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;相比桌面网页，App 版本隐藏了较长的帮助说明和规则文字，把信息分成操作流程和预览结果两个阶段。&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;section&gt;&lt;h3&gt;网页端&lt;a href=&quot;#网页端&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;原生 JavaScript&lt;/li&gt;
&lt;li&gt;JSZip 3.10.1&lt;/li&gt;
&lt;li&gt;Web APIs：File、FileReader、Blob、URL、DOMParser&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;网页端负责主要业务逻辑，包括：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;读取 DOCX 文件&lt;/li&gt;
&lt;li&gt;解压 DOCX ZIP 包&lt;/li&gt;
&lt;li&gt;解析 &lt;code&gt;word/document.xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;读取关系文件和媒体资源&lt;/li&gt;
&lt;li&gt;识别题号、Part、Passage 和文章结构&lt;/li&gt;
&lt;li&gt;生成新的 &lt;code&gt;document.xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把内容重新写回 DOCX ZIP 包&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Android 端&lt;a href=&quot;#android-端&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Android App 使用 Java 编写原生外壳，WebView 加载 App 内置的网页资源。&lt;/p&gt;&lt;p&gt;原生层主要负责：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;初始化 WebView&lt;/li&gt;
&lt;li&gt;加载内置 &lt;code&gt;index.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 Android 系统文件选择器读取试卷&lt;/li&gt;
&lt;li&gt;从 App 内置资源读取 DOCX 输出模板&lt;/li&gt;
&lt;li&gt;接收网页生成的 Base64 文档数据&lt;/li&gt;
&lt;li&gt;调用系统“另存为”界面保存 DOCX&lt;/li&gt;
&lt;li&gt;处理 Android 返回键&lt;/li&gt;
&lt;li&gt;在 WebView 初始化失败时显示兜底错误页面&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;网页和原生层之间通过一个范围受限的 JavaScript Bridge 通信：&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;网页 index.html&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;├── AndroidApp.loadAsset(&quot;output-template.docx&quot;)&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;└── AndroidApp.saveDocx(base64Data, fileName)&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;/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;└── Android 系统文件保存界面&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;section&gt;&lt;h2&gt;五、DOCX 处理思路&lt;a href=&quot;#五docx-处理思路&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;DOCX 文件本质上是一个 ZIP 压缩包，里面包含 XML 文档和媒体资源。项目没有依赖服务器端转换，而是在浏览器或 WebView 中直接处理文件。&lt;/p&gt;&lt;p&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;选择试卷 DOCX&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;读取 ZIP 文件&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;解析 document.xml&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;↓&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;/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;/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;识别 Part / Passage / 题号&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;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;/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;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;重新打包 DOCX&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;↓&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;/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;/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;section&gt;&lt;h3&gt;1. DOCX 不是简单的纯文本&lt;a href=&quot;#1-docx-不是简单的纯文本&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;题目可能出现在普通段落、表格、图片或多个 XML 节点中。解析时不能只读取页面上看到的文字，还要处理 Word 文档中的关系文件、媒体文件和格式结构。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 不同试卷的结构并不完全一致&lt;a href=&quot;#2-不同试卷的结构并不完全一致&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;同样是 Part one，有的试卷会明确写出 Passage，有的试卷只通过题号和段落顺序表达结构。因此项目使用了多种识别规则，并为没有显式 Passage 的情况提供回退逻辑。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 网页下载和 Android 保存方式不同&lt;a href=&quot;#3-网页下载和-android-保存方式不同&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;浏览器可以通过 Blob 和 &lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt; 下载文件，但 Android App 更适合调用系统文件保存器。最终方案保留了浏览器下载逻辑，同时在 App 中切换到原生保存流程。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 离线运行&lt;a href=&quot;#4-离线运行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最初网页通过 CDN 加载 JSZip。为了让 App 在没有网络的环境中也可以使用，Android 版本把 JSZip 复制到 &lt;code&gt;assets&lt;/code&gt; 目录，并把输出模板一并内置。&lt;/p&gt;&lt;/section&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;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;├── 错题本生成器2.4.html          # 原始网站版本&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;├── 输出模版.docx                  # 原始输出模板&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;├── logo.jpg                       # Via 老师 Logo&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;├── android-app/                   # Android Studio 工程&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;│   ├── app/&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;│   │   └── src/main/&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;│   │       ├── assets/&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;│   │       │   ├── index.html     # App 内置页面&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;│   │       │   ├── jszip.min.js   # 本地 JSZip&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;│   │       │   ├── logo.jpg       # App 顶部 Logo&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;│   │       │   └── output-template.docx&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;│   │       ├── java/&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;│   │       │   └── .../MainActivity.java&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;│   │       └── res/&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;│   ├── build.gradle.kts&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;│   ├── settings.gradle.kts&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;│   └── README.md&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;└── Via错题本项目介绍.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;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&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;ul&gt;
&lt;li&gt;应用名称：Via错题本&lt;/li&gt;
&lt;li&gt;Android Application ID：&lt;code&gt;com.mistakebook.app&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当前版本：1.0.5&lt;/li&gt;
&lt;li&gt;最低支持版本：Android 7.0（API 24）&lt;/li&gt;
&lt;li&gt;编译版本：Android 15（API 35）&lt;/li&gt;
&lt;li&gt;Android Gradle Plugin：8.11.0&lt;/li&gt;
&lt;li&gt;Gradle：8.13&lt;/li&gt;
&lt;li&gt;Java：17+&lt;/li&gt;
&lt;/ul&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;/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;/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;[下载 Android APK](/downloads/via-mistake-book.apk)&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;Via错题本不是一个复杂的在线平台，而是一个围绕真实学习流程打磨的小工具：把试卷交给程序，把时间留给学习。&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;从网页到 Android App 的过程，也让我重新认识到一个实用工具的价值不只在于“能不能实现”，还在于它是否足够简单、稳定，并且真正贴近用户每天的使用习惯。&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;</content:encoded></item><item><title>Via 作文批改：一个面向学生与教师的英语作文智能批改系统</title><link>https://benjian.xyz/posts/via-essay-correction/</link><guid isPermaLink="true">https://benjian.xyz/posts/via-essay-correction/</guid><description>支持 OCR、AI 结构化批改、教师管理和 Word 报告导出的英语作文批改 Web 与 Android 项目。</description><pubDate>Fri, 04 Sep 2026 10:30:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;Via 作文批改：一个面向学生与教师的英语作文智能批改系统&lt;a href=&quot;#via-作文批改一个面向学生与教师的英语作文智能批改系统&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&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;Via 作文批改是一个面向中学英语写作场景的作文批改系统，提供学生端和教师端两套使用入口。&lt;/p&gt;&lt;p&gt;学生可以直接输入作文，也可以上传作文图片、Word 或 PDF 文件；系统会对图片作文进行 OCR 识别，经过人工校正后，再生成结构化的 AI 批改报告。教师可以查看学生作文、筛选批改状态、阅读详细报告，并补充教师意见。&lt;/p&gt;&lt;p&gt;项目同时提供 Web 网站和 Android APP：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Web 网站：适合电脑浏览器使用，保留完整的后台与报告能力。&lt;/li&gt;
&lt;li&gt;Android APP：使用原生 Android WebView 承载网站，针对手机屏幕重新设计了页面布局和交互。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;项目地址（部署版本）：&lt;code&gt;https://grade.viaenglish.xyz&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;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;&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;图片 OCR 识别 / 文本读取&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;/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;↓&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;AI 生成结构化批改报告&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;/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;/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;网页阅读或导出 Word 报告&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;/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;section&gt;&lt;h3&gt;1. 学生端作文提交&lt;a href=&quot;#1-学生端作文提交&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;学生登录后可以创建作文记录，并填写以下内容：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;作文标题&lt;/li&gt;
&lt;li&gt;作文正文&lt;/li&gt;
&lt;li&gt;作文要求或题干&lt;/li&gt;
&lt;li&gt;作文图片、Word、PDF 等文件&lt;/li&gt;
&lt;li&gt;作文要求图片&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;图片作文会进入 OCR 流程；纯文本作文可以直接进入批改流程。系统会保存作文原文、确认后的文本、文件信息和处理状态，便于后续查看历史记录。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. OCR 识别与人工校正&lt;a href=&quot;#2-ocr-识别与人工校正&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;对于手写或拍照作文，系统会优先调用腾讯云 OCR 进行识别，并保存 OCR 原文和识别质量信息。&lt;/p&gt;&lt;p&gt;OCR 结果不直接作为最终批改文本，而是先进入人工校正页面。教师或学生可以检查段落、单词和标点，确认后再生成 AI 报告。这样可以减少因为图片模糊、手写连笔或版面复杂导致的误判。&lt;/p&gt;&lt;p&gt;当 OCR 服务不可用时，系统会保留可继续编辑的兜底状态，而不是让整个提交流程直接中断。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. AI 结构化批改&lt;a href=&quot;#3-ai-结构化批改&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;报告生成模块优先调用腾讯混元大模型，并对返回结果做 JSON 解析、字段校验和分数归一化。&lt;/p&gt;&lt;p&gt;当前报告主要包含：&lt;/p&gt;&lt;ul&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;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;/li&gt;
&lt;li&gt;原文标注与错误详解&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;提示词中明确要求模型引用学生原句、针对真实作文进行评价，并限制各评分项和输出格式。服务异常时，系统会生成明确的兜底报告，提示用户核对 OCR 文本或稍后重新生成。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 报告分类阅读&lt;a href=&quot;#4-报告分类阅读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;报告页面不是把所有内容一次性堆在长页面中，而是按学习目标拆分成多个入口：&lt;/p&gt;&lt;ol&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;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;/li&gt;
&lt;/ol&gt;&lt;p&gt;在 APP 中，报告首页只展示这些分类菜单；点击菜单后进入对应详情页，再通过返回按钮回到分类首页。这样可以让学生先选择自己想看的维度，降低阅读复杂报告时的信息压力。&lt;/p&gt;&lt;p&gt;评分标准、学生习作图片版和学生习作文字版等原始页面内容没有继续作为独立菜单展示，避免和“错误详解”及评分维度重复。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 教师端管理&lt;a href=&quot;#5-教师端管理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;教师端可以：&lt;/p&gt;&lt;ul&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;li&gt;补充或修改教师确认意见&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;h3&gt;6. Word 报告导出&lt;a href=&quot;#6-word-报告导出&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;网站提供 &lt;code&gt;/api/word/[id]&lt;/code&gt; 接口，根据作文和报告数据生成真实的 &lt;code&gt;.docx&lt;/code&gt; 文件，并设置下载文件名和正确的 MIME 类型。&lt;/p&gt;&lt;p&gt;导出文档使用 &lt;code&gt;docx&lt;/code&gt; 库生成，内容可以继续在 Microsoft Word 或其他兼容软件中编辑、打印和归档。&lt;/p&gt;&lt;p&gt;APP 对导出链接做了专门兼容处理：报告导出请求可以交由系统浏览器接管，使用方式与网站原本的下载行为保持一致。由于网站登录 Cookie 与手机浏览器的登录 Cookie 相互独立，首次在浏览器下载时可能需要先在浏览器中登录网站。&lt;/p&gt;&lt;/section&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;section&gt;&lt;h3&gt;Web 项目&lt;a href=&quot;#web-项目&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;网站使用 Next.js App Router 构建，主要技术包括：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Next.js 15&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Prisma ORM&lt;/li&gt;
&lt;li&gt;SQLite（开发环境）&lt;/li&gt;
&lt;li&gt;bcryptjs（密码哈希校验）&lt;/li&gt;
&lt;li&gt;Zod（表单数据校验）&lt;/li&gt;
&lt;li&gt;docx（Word 文档生成）&lt;/li&gt;
&lt;li&gt;腾讯云 OCR&lt;/li&gt;
&lt;li&gt;腾讯混元大模型&lt;/li&gt;
&lt;/ul&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;grade-web/&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;├─ src/app/                  # 页面、路由和 API Route Handler&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;├─ src/components/           # 报告、布局和交互组件&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;├─ src/lib/actions.ts        # Server Actions：登录、提交、生成报告等&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;├─ src/lib/auth.ts           # Cookie 会话和角色权限&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;├─ src/lib/ocr.ts            # OCR 调用与兜底逻辑&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;├─ src/lib/review.ts         # AI 批改、提示词、JSON 校验和兜底报告&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;├─ src/lib/word-export.ts    # Word 报告生成&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;├─ prisma/schema.prisma      # 数据模型&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;└─ public/                   # 模板、示例图片和字体资源&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;h3&gt;数据模型&lt;a href=&quot;#数据模型&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统围绕以下几类数据组织：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;User&lt;/code&gt;：登录账号和角色&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Student&lt;/code&gt; / &lt;code&gt;Teacher&lt;/code&gt;：角色扩展信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Essay&lt;/code&gt;：作文主记录、正文、题目要求和处理状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EssayFile&lt;/code&gt;：作文图片、Word、PDF 等附件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OcrResult&lt;/code&gt;：OCR 原文、校正文本、识别质量和供应商信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CorrectionReport&lt;/code&gt;：总分、分项评分、分析文本和参考范文&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReportAnnotation&lt;/code&gt;：原文中的错误、修改结果和批注&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;登录状态使用签名 Cookie 保存，密码在数据库中以哈希形式存储。APP 的“记住密码”功能不直接把密码写入网页存储，而是通过 Android 原生桥接，使用 Android Keystore 中的 AES-GCM 密钥加密后保存。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;Android APP 适配&lt;a href=&quot;#android-app-适配&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Android APP 位于 &lt;code&gt;android-app/&lt;/code&gt;，本质上是一个轻量的原生 Android WebView 客户端。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;APP 的设计目标&lt;a href=&quot;#app-的设计目标&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&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;li&gt;通过 Logo、APP 名称和移动端菜单形成独立的 APP 体验&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;APP 专属界面&lt;a href=&quot;#app-专属界面&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;APP 通过 &lt;code&gt;assets/app.js&lt;/code&gt; 和 &lt;code&gt;assets/app.css&lt;/code&gt; 注入移动端样式与交互，不改变网站在普通浏览器中的视觉布局。&lt;/p&gt;&lt;p&gt;主要适配包括：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;首页隐藏冗余描述，只保留学生、教师入口和图标&lt;/li&gt;
&lt;li&gt;开屏显示 &lt;code&gt;logo.jpg&lt;/code&gt;，并限制 Logo 在合理尺寸内，保持原图比例&lt;/li&gt;
&lt;li&gt;登录页增加“记住密码”功能，并避免 WebView 重复注入造成多个复选框&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;li&gt;APP 名称设置为“Via作文批改”&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;原生层职责&lt;a href=&quot;#原生层职责&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;MainActivity.java&lt;/code&gt; 主要负责：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;创建和配置 WebView&lt;/li&gt;
&lt;li&gt;注入 APP CSS 和 JavaScript&lt;/li&gt;
&lt;li&gt;控制页面加载时机，避免原网页开屏界面闪现&lt;/li&gt;
&lt;li&gt;调用 Android 文件选择器&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;网站业务仍由 Next.js 服务端完成，因此 APP 不需要复制数据库、OCR 或 AI 逻辑。&lt;/p&gt;&lt;/section&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;学生打开 Via 作文批改，选择“学生入口”。&lt;/li&gt;
&lt;li&gt;登录后填写作文标题，可上传作文图片和题干图片。&lt;/li&gt;
&lt;li&gt;系统保存作文记录，并把图片送入 OCR。&lt;/li&gt;
&lt;li&gt;学生或教师在 OCR 页面检查识别结果并确认文本。&lt;/li&gt;
&lt;li&gt;点击生成报告，服务端调用 AI 模型生成四项评分和详细建议。&lt;/li&gt;
&lt;li&gt;生成页展示作文图片预览与处理进度。&lt;/li&gt;
&lt;li&gt;报告首页显示九个分类入口。&lt;/li&gt;
&lt;li&gt;学生依次查看语法、词汇、结构、内容和改进建议。&lt;/li&gt;
&lt;li&gt;需要归档时，从“导出报告”进入 Word 导出流程。&lt;/li&gt;
&lt;/ol&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;section&gt;&lt;h3&gt;OCR 不能直接等同于最终文本&lt;a href=&quot;#ocr-不能直接等同于最终文本&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;图片识别存在漏字、错字、断行和标点问题。因此项目把“识别”和“确认”拆开，先让人检查，再让 AI 批改。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;AI 输出需要结构化约束&lt;a href=&quot;#ai-输出需要结构化约束&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;大模型输出可能出现字段缺失、格式不统一或分数不合法。项目通过提示词约束、JSON 解析、字段校验、分数归一化和兜底报告，保证页面始终能渲染。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;移动端不适合直接照搬桌面页面&lt;a href=&quot;#移动端不适合直接照搬桌面页面&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;原始报告内容较多，如果在手机上连续展示，会形成很长的滚动页面。因此 APP 把报告拆成多个分类入口，让用户按需查看。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;WebView 下载与浏览器下载存在会话差异&lt;a href=&quot;#webview-下载与浏览器下载存在会话差异&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;网站报告接口依赖登录 Cookie。WebView、Android 系统下载服务和外部浏览器之间的 Cookie 并不天然共享，因此导出功能需要根据实际设备环境选择接管方式，并在真机上验证登录态和文件保存行为。&lt;/p&gt;&lt;/section&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;section&gt;&lt;h3&gt;启动网站&lt;a href=&quot;#启动网站&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;&lt;span&gt;cd grade&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;web&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;npm.cmd&lt;/span&gt;&lt;span&gt; install&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;npm.cmd&lt;/span&gt;&lt;span&gt; run prisma:generate&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;npm.cmd&lt;/span&gt;&lt;span&gt; run db:reset&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;npm.cmd&lt;/span&gt;&lt;span&gt; run dev&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;http://localhost:3000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;生产环境需要配置数据库、会话密钥、上传目录、腾讯云 OCR 凭据和混元模型凭据。真实密钥不应写入仓库或博客文章。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;构建 Android APP&lt;a href=&quot;#构建-android-app&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;&lt;span&gt;cd android&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;app&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;$env:JAVA_HOME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;E:\Program Files\Android\Android Studio\jbr&apos;&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;$env:ANDROID_HOME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;E:\Android\Sdk&apos;&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;$env:ANDROID_SDK_ROOT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;E:\Android\Sdk&apos;&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;$env:GRADLE_USER_HOME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;E:\Android\Gradle&apos;&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;gradlew.bat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt;&lt;span&gt; :app:assembleDebug &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt;offline &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;plain&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;生成的 Debug APK 位于：&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;android-app/app/build/outputs/apk/debug/app-debug.apk&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;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;增加 PDF 报告导出&lt;/li&gt;
&lt;li&gt;支持更多 OCR 供应商和本地 OCR&lt;/li&gt;
&lt;li&gt;增加作文版本对比和修改前后 diff&lt;/li&gt;
&lt;li&gt;增加班级统计、分数趋势和错误类型统计&lt;/li&gt;
&lt;li&gt;增加报告分享和教师批量导出&lt;/li&gt;
&lt;li&gt;对 APP 下载流程做更多 Android 版本和浏览器兼容性测试&lt;/li&gt;
&lt;li&gt;将开发环境 SQLite 平滑迁移到生产级数据库&lt;/li&gt;
&lt;li&gt;增加自动化测试、接口监控和报告生成耗时统计&lt;/li&gt;
&lt;/ul&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;/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;/downloads/via-essay-correction.apk&quot;&gt;下载 Android APK&lt;/a&gt;&lt;/p&gt;&lt;p&gt;Via 作文批改并不是简单地把一个大模型接口包装成“打分工具”，而是围绕作文批改建立了一条完整流程：从文件上传、OCR 识别、人工确认，到 AI 分项评分、教师复核和 Word 归档。&lt;/p&gt;&lt;p&gt;这个项目的核心价值在于把复杂的批改结果拆解成学生能理解、教师能复核、系统能持续扩展的结构化数据，同时通过 Web + Android WebView 的方式，用较低成本兼顾电脑端和移动端使用体验。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>秋招督学：把求职准备拆成每天可执行的动作</title><link>https://benjian.xyz/posts/autumn-study-coach/</link><guid isPermaLink="true">https://benjian.xyz/posts/autumn-study-coach/</guid><description>面向嵌入式 C/C++ 秋招的个人学习与求职管理 App，覆盖目标、计划、专注、复习和复盘。</description><pubDate>Fri, 04 Sep 2026 10:20: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;blockquote&gt;&lt;p&gt;一个面向嵌入式 C/C++ 秋招的个人学习与求职管理 App。它把目标倒计时、每日任务、专注计时、能力地图、复习记录和求职进度放在同一条执行链路里，帮助我回答一个很具体的问题：&lt;strong&gt;今天最应该做什么，以及做完之后留下了什么证据？&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&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;准备嵌入式岗位时，学习内容往往同时横跨 C/C++、数据结构、Linux、MCU、驱动、RTOS、网络、项目和面试表达。传统待办应用只能记录“要做什么”，却很难持续回答下面几件事：&lt;/p&gt;&lt;ul&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;/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;section&gt;&lt;h3&gt;1. 首次设置与目标倒计时&lt;a href=&quot;#1-首次设置与目标倒计时&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;首次打开 App 时，可以设置称呼、目标岗位、目标准备完成日、每天可投入时间和当前阶段。保存后，系统会生成当天的起步计划，并在首页展示距离目标日期的剩余天数。&lt;/p&gt;&lt;p&gt;目标岗位支持多选，内置嵌入式软件、嵌入式 Linux、单片机 / RTOS 和 C++ 开发等方向。计划中的任务数量由用户自己决定，不强制固定为“三件事”，更强调可兑现的承诺。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 今日作战台&lt;a href=&quot;#2-今日作战台&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;首页只保留最重要的信息：&lt;/p&gt;&lt;ul&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;li&gt;待复习知识点数量；&lt;/li&gt;
&lt;li&gt;开始专注、今日复盘和历史复盘入口。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;任务的创建、编辑和删除统一放在“计划”页，首页负责快速确认状态，减少打开 App 后的决策成本。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 计划与逾期补救&lt;a href=&quot;#3-计划与逾期补救&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;任务包含标题、日期、预计时长、类型、关联能力域、完成标准、优先级和“当日必做”标记。支持学习、算法、项目、复习、简历、投递、面试准备和复盘等类型。&lt;/p&gt;&lt;p&gt;历史未完成任务不会被自动挪到今天，而是按原计划日期保留在“历史未完成”视图中。这样既能看到真实的延期记录，也可以继续编辑、完成或删除，避免因为漏做一天而丢失上下文。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 专注计时&lt;a href=&quot;#4-专注计时&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;专注模块支持正计时和倒计时两种模式：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;正计时：从 &lt;code&gt;00:00&lt;/code&gt; 开始累计，适合自由投入；&lt;/li&gt;
&lt;li&gt;倒计时：支持 25、45、60、90 分钟预设；&lt;/li&gt;
&lt;li&gt;两种模式都支持暂停、继续、最小化和结束；&lt;/li&gt;
&lt;li&gt;App 进入后台或锁屏后，重新回到页面仍能根据时间戳恢复准确状态；&lt;/li&gt;
&lt;li&gt;结束后记录有效专注时长，关联任务时可以继续填写成果和掌握程度。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;进行中的计时状态保存在 &lt;code&gt;localStorage&lt;/code&gt;，历史专注记录写入本地数据库。少于 1 分钟的片段默认不计入有效专注，避免统计被误触或短暂试用污染。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 能力地图与间隔复习&lt;a href=&quot;#5-能力地图与间隔复习&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;能力页内置 10 个能力域，包括：&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;C 语言与底层语义&lt;/td&gt;&lt;td&gt;指针、内存、位运算、未定义行为、编译链接&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;现代 C++&lt;/td&gt;&lt;td&gt;RAII、移动语义、智能指针、STL、并发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;数据结构与算法&lt;/td&gt;&lt;td&gt;链表、树、排序、二分、动态规划&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;操作系统与 Linux&lt;/td&gt;&lt;td&gt;进程线程、同步、虚拟内存、GDB、strace&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MCU 与计算机体系&lt;/td&gt;&lt;td&gt;Cortex-M、中断、DMA、启动文件、链接脚本&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;驱动、接口与硬件调试&lt;/td&gt;&lt;td&gt;GPIO、UART、I2C、SPI、CAN、设备树&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RTOS 与实时系统&lt;/td&gt;&lt;td&gt;调度、队列、信号量、优先级反转、FreeRTOS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;网络与通信&lt;/td&gt;&lt;td&gt;TCP/IP、Socket、MQTT、抓包与故障定位&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;工程、构建与质量&lt;/td&gt;&lt;td&gt;Git、CMake、交叉编译、测试、Sanitizer&lt;/td&gt;&lt;/tr&gt;&lt;tr&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;每个能力域包含具体知识点。用户可以逐项点亮自己确认掌握的内容，等级按“已点亮知识点 / 总知识点”自动计算为 0～5 级，不依赖一个看似精确但缺少依据的主观分数。&lt;/p&gt;&lt;p&gt;完成学习任务后，可以填写 1～5 级掌握程度，并将任务加入复习队列。复习结果分为“忘记、模糊、基本掌握、熟练”，系统按照 1、3、7、14、30 天间隔计算下一次复习日期。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6. AI 每日计划建议&lt;a href=&quot;#6-ai-每日计划建议&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;AI 计划不是自动写入待办的黑盒功能，而是一个“候选生成器”：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;前端收集目标岗位、当前阶段、剩余天数、能力掌握度、已点亮 / 未点亮知识点、今日已排任务和剩余时间；&lt;/li&gt;
&lt;li&gt;Kotlin 原生层通过 WebView Bridge 将请求转发到 CloudBase 云函数；&lt;/li&gt;
&lt;li&gt;云函数调用腾讯混元 MaaS，要求模型返回 4～8 个具体、可验收的任务候选；&lt;/li&gt;
&lt;li&gt;客户端再次校验任务类型、能力 ID、时长、重复标题和总时长；&lt;/li&gt;
&lt;li&gt;用户勾选后，候选任务才会真正加入今日计划。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;云函数不会使用本地规则伪装成模型结果。当网络、云函数或模型异常时，界面会显示真实失败原因，而不是返回一组看起来像 AI 生成的假数据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7. 求职看板与复盘&lt;a href=&quot;#7-求职看板与复盘&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;求职页用阶段看板管理岗位记录，阶段包括“意向、已投递、笔试、一面、二面 / 终面、Offer、结束”。每条记录可以保存公司、岗位、下一步日期和备注，并一键创建准备任务。&lt;/p&gt;&lt;p&gt;每日复盘记录当天收获、未完成原因、明天最重要的一件事以及明天计划做几件事。历史复盘按日期倒序保存，避免复盘变成一次性弹窗。&lt;/p&gt;&lt;p&gt;“我的”页面提供最近 7 天的执行周报，包括有效专注时长、任务完成率、学习天数、新增求职记录和每日柱状图，并支持调用 Android 分享能力输出纯文本周报。&lt;/p&gt;&lt;/section&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;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地 HTML/CSS/JavaScript&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;IndexedDB&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;localStorage&lt;br /&gt;&lt;br /&gt;进行中计时与内容缓存&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Kotlin WebView Bridge&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;通知、闹钟、分享、文件导入导出、RSS&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;CloudBase HTTP 云函数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Tencent MaaS / 混元&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;本地 HTML/CSS/JavaScript&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;IndexedDB&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;localStorage&lt;br /&gt;&lt;br /&gt;进行中计时与内容缓存&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Kotlin WebView Bridge&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;通知、闹钟、分享、文件导入导出、RSS&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;CloudBase HTTP 云函数&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Tencent MaaS / 混元&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;Android 原生壳&lt;a href=&quot;#android-原生壳&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Kotlin + Android SDK；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;minSdk 26&lt;/code&gt;，&lt;code&gt;targetSdk 35&lt;/code&gt;，Java / Kotlin JVM 17；&lt;/li&gt;
&lt;li&gt;单 Activity 承载本地 WebView 页面；&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;AndroidBridge&lt;/code&gt; 暴露通知、闹钟、分享、文件选择、行业资讯和 AI 请求等能力；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;AlarmManager&lt;/code&gt; 实现专注结束提醒和每日计划 / 复盘提醒；&lt;/li&gt;
&lt;li&gt;外部 HTTP(S) 链接交给系统浏览器打开，应用内只加载受信任的本地资源。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;本地 Web 应用&lt;a href=&quot;#本地-web-应用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;HTML + CSS + 原生 JavaScript，无运行时 CDN 依赖；&lt;/li&gt;
&lt;li&gt;页面资源随 APK 打包在 &lt;code&gt;app/src/main/assets/www&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;IndexedDB&lt;/code&gt; 保存 profile、tasks、skills、sessions、applications、reviews、settings 七类数据；&lt;/li&gt;
&lt;li&gt;IndexedDB 不可用时回退到 &lt;code&gt;localStorage&lt;/code&gt;，保证浏览器预览和部分受限环境仍可工作；&lt;/li&gt;
&lt;li&gt;使用响应式布局、深色主题、语义化按钮和 &lt;code&gt;aria-*&lt;/code&gt; 属性，适配移动端屏幕。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;云端 AI 服务&lt;a href=&quot;#云端-ai-服务&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;CloudBase 函数 &lt;code&gt;cloudbase/daily-advice/index.js&lt;/code&gt; 同时提供两类能力：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;daily-plan&lt;/code&gt;：根据能力短板和剩余时间生成今日任务候选；&lt;/li&gt;
&lt;li&gt;行业资讯学习建议：基于真实 RSS 文章生成摘要、关联能力、练习和面试自测问题。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;模型密钥只配置在云函数环境变量 &lt;code&gt;TENCENT_MAAS_API_KEY&lt;/code&gt; 中，不写入 Android 工程、前端资源或 Git。&lt;/p&gt;&lt;/section&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;section&gt;&lt;h3&gt;本地优先，而不是先做账号系统&lt;a href=&quot;#本地优先而不是先做账号系统&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这是一个个人使用工具，核心数据不依赖登录和云同步。离线时仍然可以创建任务、计时、复盘和复习；只有行业资讯和 AI 建议需要网络。这样既降低了首版复杂度，也减少了学习记录被上传的隐私风险。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;时间戳驱动计时，而不是依赖定时器累计&lt;a href=&quot;#时间戳驱动计时而不是依赖定时器累计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;JavaScript 的 &lt;code&gt;setInterval&lt;/code&gt; 只能负责刷新界面，真正的计时依据是 &lt;code&gt;startedAt&lt;/code&gt;、&lt;code&gt;runStartedAt&lt;/code&gt;、&lt;code&gt;elapsedBeforePauseMs&lt;/code&gt; 和 &lt;code&gt;endAt&lt;/code&gt;。因此页面进入后台、被系统挂起或重新打开后，都可以重新计算实际经过的时间。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;AI 负责建议，人负责承诺&lt;a href=&quot;#ai-负责建议人负责承诺&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;AI 只生成候选任务，用户必须明确勾选后才会写入计划。这一设计保留了 AI 的拆解能力，同时避免模型在不了解真实精力和临时安排时替用户做决定。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;真实数据优先于“看起来完整”&lt;a href=&quot;#真实数据优先于看起来完整&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;行业资讯只接受真实 RSS 内容；AI 失败时不返回本地假候选；能力等级由知识点点亮比例计算。项目更关注数据口径可解释，而不是让页面始终显示一个漂亮但不可信的数字。&lt;/p&gt;&lt;/section&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;code&gt;tools/webview-smoke-test.mjs&lt;/code&gt;，通过 WebView 调试协议覆盖主要交互链路，包括：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;底部导航和页面切换；&lt;/li&gt;
&lt;li&gt;目标设置、任务新增 / 编辑 / 删除；&lt;/li&gt;
&lt;li&gt;AI 候选生成与仅添加已勾选任务；&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;li&gt;每日复盘与历史复盘；&lt;/li&gt;
&lt;li&gt;Android 原生 Bridge 是否可用。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;本地构建 Debug APK：&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;./gradlew&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;assembleDebug&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;Windows PowerShell：&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;.\&lt;/span&gt;&lt;span&gt;gradlew.bat&lt;/span&gt;&lt;span&gt; assembleDebug&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;生成的 APK 默认位于：&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;app/build/outputs/apk/debug/app-debug.apk&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;项目结构&lt;a href=&quot;#项目结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;├─ app/&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;│  └─ src/main/&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;│     ├─ java/com/qiuzhao/supervisor/&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;│     │  ├─ MainActivity.kt              # WebView 容器与原生桥接&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;│     │  ├─ FocusAlarmReceiver.kt        # 专注结束通知&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;│     │  └─ DailyReminderReceiver.kt     # 每日提醒通知&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;│     ├─ assets/www/&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;│     │  ├─ index.html                   # 页面结构与弹窗&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;│     │  ├─ css/app.css                  # 主题与响应式样式&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;│     │  └─ js/app.js                    # 状态、数据和交互逻辑&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;│     └─ AndroidManifest.xml&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;├─ cloudbase/daily-advice/&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;│  ├─ index.js                           # AI 计划 / 行业建议云函数&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;│  └─ DEPLOY.md                          # 部署说明&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;├─ tools/webview-smoke-test.mjs          # WebView 端到端冒烟测试&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;└─ REQUIREMENTS.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;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&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;当前版本已经完成个人使用场景下的 MVP 主链路：本地数据、任务计划、专注计时、能力地图、间隔复习、求职记录、每日 / 历史复盘、原生通知、RSS 行业资讯和 AI 任务建议。&lt;/p&gt;&lt;p&gt;后续可以继续完善：&lt;/p&gt;&lt;ul&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;li&gt;更细粒度的岗位能力模板和计划模板。&lt;/li&gt;
&lt;/ul&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;/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;/downloads/autumn-study-coach.apk&quot;&gt;下载 Android APK&lt;/a&gt;&lt;/p&gt;&lt;p&gt;这个项目对我来说，重点不只是“做了一个待办 App”，而是尝试把自己的求职方法固化成一套可执行、可追踪、可复盘的工具：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;把目标拆成任务，把任务变成专注记录，把记录沉淀成能力变化，再把复盘结果带回下一次计划。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;它仍然是一个个人项目，但也正因为使用者就是我自己，很多交互取舍都来自真实使用中的摩擦，而不是先假设一个理想用户。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>基于 ZigBee 与 4G 的无线温湿度监测及远程控制系统</title><link>https://benjian.xyz/posts/zigbee-4g-environment-monitor/</link><guid isPermaLink="true">https://benjian.xyz/posts/zigbee-4g-environment-monitor/</guid><description>基于 CC2530、STM32、ML307R 与 OneNET 实现的无线温湿度监测、告警和远程控制系统。</description><pubDate>Fri, 04 Sep 2026 10:10:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;基于 ZigBee 与 4G 的无线温湿度监测及远程控制系统&lt;a href=&quot;#基于-zigbee-与-4g-的无线温湿度监测及远程控制系统&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&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;这是一个面向物联网环境监测场景设计的嵌入式项目。系统使用 CC2530 和 DHT11 组成无线采集终端，通过 ZigBee 将温湿度数据发送给协调器；协调器中的 CC2530 负责接收无线数据，STM32F103 负责数据解析、状态显示与告警控制，最后通过 ML307R 4G 模块将数据上传到 OneNET 云平台。&lt;/p&gt;&lt;p&gt;除了基本的数据采集和云端展示，系统还实现了温湿度超限告警、LED 与蜂鸣器控制、云端属性下发以及网络异常后的自动重连，形成了一条完整的“端—网关—云”物联网链路。&lt;/p&gt;&lt;p&gt;项目最终实现的主要功能包括：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;DHT11 温湿度周期采集；&lt;/li&gt;
&lt;li&gt;终端 OLED 本地数据显示；&lt;/li&gt;
&lt;li&gt;CC2530 ZigBee 自动组网与无线传输；&lt;/li&gt;
&lt;li&gt;STM32 串口接收、帧校验和异常重同步；&lt;/li&gt;
&lt;li&gt;网关 OLED 显示节点地址、温湿度、网络状态和上传次数；&lt;/li&gt;
&lt;li&gt;温湿度超限时通过 RGB LED 和蜂鸣器报警；&lt;/li&gt;
&lt;li&gt;ML307R 通过 4G 网络接入 OneNET；&lt;/li&gt;
&lt;li&gt;云端查看温湿度及执行 LED、蜂鸣器远程控制；&lt;/li&gt;
&lt;li&gt;4G 掉线或连续发布失败后的自动重连。&lt;/li&gt;
&lt;/ul&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;DHT11&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;终端 CC2530&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;↓ ZigBee&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;协调器 CC2530&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;↓ UART&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;STM32F103&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;↓ USART3 / AT 指令&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;ML307R 4G 模块&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;OneNET 云平台&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;终端 OLED 用于显示当前 ZigBee 入网状态、节点短地址、温度和湿度；网关 OLED 用于显示接收到的节点数据、4G 网络状态、云端发布状态以及串口诊断计数。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;将本文迁移到博客时，需要同时上传上图，或者将图片地址替换为博客图床地址。&lt;/p&gt;&lt;/blockquote&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;系统可以分为采集终端、ZigBee 协调器、4G 网关和云平台四个部分。&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;采集终端&lt;/td&gt;&lt;td&gt;CC2530、DHT11、OLED&lt;/td&gt;&lt;td&gt;采集温湿度、显示数据、加入 ZigBee 网络并周期发送数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ZigBee 协调器&lt;/td&gt;&lt;td&gt;CC2530&lt;/td&gt;&lt;td&gt;建立 ZigBee 网络、接收终端数据、提取节点短地址并通过串口转发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;4G 网关&lt;/td&gt;&lt;td&gt;STM32F103RCT6、ML307R、OLED、RGB LED、蜂鸣器&lt;/td&gt;&lt;td&gt;解析数据、显示状态、执行告警、连接 4G 并与 OneNET 通信&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;云平台&lt;/td&gt;&lt;td&gt;OneNET&lt;/td&gt;&lt;td&gt;保存和展示设备属性，并向网关下发控制命令&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这样的分层方式将局域无线采集与广域网络通信分离：CC2530 专注于 ZigBee 组网，STM32 专注于网关业务，ML307R 专注于蜂窝联网。各模块之间通过明确的数据协议连接，便于单独调试和后续扩展。&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;section&gt;&lt;h3&gt;采集终端&lt;a href=&quot;#采集终端&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;终端以 CC2530 为主控，主要外设连接如下：&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DHT11 DATA&lt;/td&gt;&lt;td&gt;P2.0，外接 10 kΩ 上拉电阻&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OLED SCL&lt;/td&gt;&lt;td&gt;P1.2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OLED SDA&lt;/td&gt;&lt;td&gt;P1.3&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;蜂鸣器&lt;/td&gt;&lt;td&gt;P1.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LED1&lt;/td&gt;&lt;td&gt;P1.4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LED2&lt;/td&gt;&lt;td&gt;P0.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LED3&lt;/td&gt;&lt;td&gt;P1.0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LED4&lt;/td&gt;&lt;td&gt;P1.1&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;终端完成 ZigBee 入网后，大约每 2.000～2.255 秒采集并发送一次数据。增加少量随机抖动，可以降低多个终端同时发送造成碰撞的概率。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;协调器与 4G 网关&lt;a href=&quot;#协调器与-4g-网关&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;协调器板上同时包含 CC2530 和 STM32F103RCT6。两颗 MCU 分工明确：CC2530 处理 ZigBee 协议栈，STM32 负责网关业务。&lt;/p&gt;












































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;功能&lt;/th&gt;&lt;th&gt;STM32 引脚&lt;/th&gt;&lt;th&gt;连接对象&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ZigBee 数据输入&lt;/td&gt;&lt;td&gt;PC11 / UART4_RX&lt;/td&gt;&lt;td&gt;CC2530 P0.4 / USART1_TX&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;调试串口&lt;/td&gt;&lt;td&gt;PA9、PA10 / USART1&lt;/td&gt;&lt;td&gt;CH340N / USB1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ML307R 通信&lt;/td&gt;&lt;td&gt;PB10、PB11 / USART3&lt;/td&gt;&lt;td&gt;ML307R&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OLED SCL&lt;/td&gt;&lt;td&gt;PA12&lt;/td&gt;&lt;td&gt;板载 OLED&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OLED SDA&lt;/td&gt;&lt;td&gt;PA11&lt;/td&gt;&lt;td&gt;板载 OLED&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RGB LED&lt;/td&gt;&lt;td&gt;PC13、PC14、PC15&lt;/td&gt;&lt;td&gt;板载 LED&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;蜂鸣器&lt;/td&gt;&lt;td&gt;PA15&lt;/td&gt;&lt;td&gt;板载有源蜂鸣器驱动电路&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;三路串口均采用 115200、8N1：USART1 输出调试日志，UART4 接收 CC2530 数据，USART3 与 ML307R 通信，从硬件和软件层面避免了串口资源冲突。&lt;/p&gt;&lt;/section&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;CC2530 ZigBee 程序&lt;/td&gt;&lt;td&gt;TI Z-Stack、IAR Embedded Workbench for 8051&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;STM32 网关程序&lt;/td&gt;&lt;td&gt;Keil MDK、STM32F10x 标准外设库&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;蜂窝通信&lt;/td&gt;&lt;td&gt;ML307R RTU 固件及 AT 指令&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;云平台&lt;/td&gt;&lt;td&gt;OneNET 物联网平台&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;CC2530 工程基于 Z-Stack 的 &lt;code&gt;SampleApp&lt;/code&gt; 示例进行二次开发，在 OSAL 事件机制中加入 DHT11 采集、OLED 显示和传感器数据发送任务。STM32 工程采用前后台结构：中断负责快速接收数据，主循环负责协议解析、设备控制和云端通信。&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;section&gt;&lt;h3&gt;终端到协调器：ZigBee 数据帧&lt;a href=&quot;#终端到协调器zigbee-数据帧&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;终端成功读取 DHT11 后发送 5 字节数据：&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;[&apos;T&apos;, &apos;H&apos;, temperature, humidity, checksum]&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;校验值为前 4 个字节相加后取低 8 位：&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;checksum &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;T&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;H&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; temperature &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; humidity;&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;协调器收到数据后会检查帧头、长度、校验值和温湿度范围。数据有效时，它从 Z-Stack 的接收包结构中读取真实的 ZigBee 网络短地址：&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;pkt&lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;srcAddr.addr.shortAddr&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;D1&lt;/code&gt; 确认帧，终端通过 LED 显示本次数据已被接收。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;协调器到 STM32：ASCII 串口帧&lt;a href=&quot;#协调器到-stm32ascii-串口帧&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;为了方便串口调试和跨平台解析，CC2530 将无线数据转换为固定格式的 ASCII 帧：&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;$TH,IIII,TTT,HHH*CC\r\n&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;table&gt;&lt;thead&gt;&lt;tr&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;&lt;code&gt;IIII&lt;/code&gt;&lt;/td&gt;&lt;td&gt;4 位十六进制 ZigBee 网络短地址&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;TTT&lt;/code&gt;&lt;/td&gt;&lt;td&gt;3 位十进制温度&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;HHH&lt;/code&gt;&lt;/td&gt;&lt;td&gt;3 位十进制湿度&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;CC&lt;/code&gt;&lt;/td&gt;&lt;td&gt;从字符 &lt;code&gt;T&lt;/code&gt; 到湿度末位之间所有 ASCII 字节的异或校验&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&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;$TH,E839,025,060*CC\r\n&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;STM32 的 UART4 中断只负责把字节写入 128 字节环形缓冲区，避免在中断中执行耗时操作。主循环再以 &lt;code&gt;$&lt;/code&gt; 为同步字符，以换行符为帧结束标志，完成格式检查、异或校验、数值转换和量程判断。&lt;/p&gt;&lt;p&gt;这种设计即使遇到丢字节、粘包或无效数据，也能在下一次出现 &lt;code&gt;$&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;UART B:&amp;lt;接收字节数&amp;gt; E:&amp;lt;无效帧数&amp;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;/section&gt;&lt;section&gt;&lt;h3&gt;STM32 到 OneNET：4G 属性上报&lt;a href=&quot;#stm32-到-onenet4g-属性上报&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;STM32 通过 USART3 向 ML307R 发送 AT 指令，主要初始化过程包括：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;连续发送 &lt;code&gt;AT&lt;/code&gt;，确认模块已经完成启动且串口通信稳定；&lt;/li&gt;
&lt;li&gt;选择 OneNET RTU 任务；&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;li&gt;发布一次设备属性，并以平台返回的 &lt;code&gt;code:200&lt;/code&gt; 作为真正成功的依据。&lt;/li&gt;
&lt;/ol&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;&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;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;1&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;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;version&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;1.0&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;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;params&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;&quot;temp&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;25&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;&quot;humi&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;60&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;&quot;LED1&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;value&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;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;LED2&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;value&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;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;buzzer&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;value&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;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;程序只有在收到 OneNET 的成功回复后才增加 OLED 上的上传计数，避免将“AT 指令已经发出”误判为“数据已经被云平台接受”。&lt;/p&gt;&lt;/section&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;温度大于 28 ℃时点亮红灯；&lt;/li&gt;
&lt;li&gt;湿度大于 80% 时点亮蓝灯；&lt;/li&gt;
&lt;li&gt;红灯或蓝灯任意一个处于告警状态时，蜂鸣器鸣响。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;阈值集中定义在 &lt;code&gt;协调器4G上云/app_config.h&lt;/code&gt; 中，便于根据应用场景调整。&lt;/p&gt;&lt;p&gt;OneNET 还可以向设备下发 &lt;code&gt;LED1&lt;/code&gt;、&lt;code&gt;LED2&lt;/code&gt; 和 &lt;code&gt;buzzer&lt;/code&gt; 布尔属性。STM32 使用一个按长度接收的状态机解析 ML307R 主动上报的属性设置消息，执行本地控制后，再向平台返回对应的 &lt;code&gt;set_reply&lt;/code&gt;。&lt;/p&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;section&gt;&lt;h3&gt;终端流程&lt;a href=&quot;#终端流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;初始化 CC2530、OLED 和 DHT11；&lt;/li&gt;
&lt;li&gt;启动 Z-Stack 并搜索 ZigBee 网络；&lt;/li&gt;
&lt;li&gt;入网成功后启动周期任务；&lt;/li&gt;
&lt;li&gt;读取 DHT11 的 40 位数据并校验；&lt;/li&gt;
&lt;li&gt;在 OLED 显示节点地址、温度和湿度；&lt;/li&gt;
&lt;li&gt;通过 ZigBee 发送传感器帧；&lt;/li&gt;
&lt;li&gt;等待协调器确认，并进入下一次采集周期。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;DHT11 读取失败时，OLED 会显示 &lt;code&gt;DHT11 ERROR&lt;/code&gt;，程序不会发送无效数据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;网关流程&lt;a href=&quot;#网关流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;初始化调试串口、UART4、OLED、LED、蜂鸣器和 ML307R；&lt;/li&gt;
&lt;li&gt;配置 ML307R 并连接 OneNET；&lt;/li&gt;
&lt;li&gt;持续从 UART4 环形缓冲区取出 CC2530 数据；&lt;/li&gt;
&lt;li&gt;对串口帧执行重新同步、格式检查和校验；&lt;/li&gt;
&lt;li&gt;更新 OLED、LED 和蜂鸣器状态；&lt;/li&gt;
&lt;li&gt;将最新属性上传到 OneNET；&lt;/li&gt;
&lt;li&gt;处理云端属性下发；&lt;/li&gt;
&lt;li&gt;连续发布失败达到上限后，将网络标记为离线并定期重连。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;即使 4G 网络暂时不可用，ZigBee 数据接收、OLED 显示和现场告警仍然可以继续运行，避免云端故障影响本地监测功能。&lt;/p&gt;&lt;/section&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;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;zuwang/&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;├─ Projects/                              # Z-Stack 与 CC2530 主工程&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;│  └─ zstack/Samples/SampleApp/&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;├─ Components/                            # Z-Stack HAL 与公共组件&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;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├─ 协调器4G上云/                           # STM32 + ML307R 网关工程&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;│  ├─ main.c                              # 网关主流程&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;│  ├─ zigbee.c/.h                         # UART4 接收及协议解析&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;│  ├─ ml307.c/.h                          # ML307R AT 指令通信&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;│  ├─ onenet.c/.h                         # OneNET 接入、上报与属性下发&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;│  ├─ led.c/.h                            # RGB LED 控制&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;│  ├─ buzzer.c/.h                         # 蜂鸣器控制&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;│  └─ app_config.h                        # 应用参数与云平台配置&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;/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;└─ 烧录文件/                               # 最终可烧录 HEX 文件&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;CC2530 的核心业务代码位于：&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;Projects/zstack/Samples/SampleApp/Source/&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;├─ SampleApp.c&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;├─ SampleAppDht11.c/.h&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;├─ SampleAppTerminal.c/.h&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;└─ SampleAppUart.c/.h&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;编译与烧录&lt;a href=&quot;#编译与烧录&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;CC2530&lt;a href=&quot;#cc2530&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用 IAR Embedded Workbench for 8051 打开：&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;Projects/zstack/Samples/SampleApp/CC2530DB/SampleApp.ewp&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;工程包含终端与协调器 Target，需要分别选择对应目标进行编译和下载。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;STM32&lt;a href=&quot;#stm32&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用 Keil MDK 打开：&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;协调器4G上云/Project.uvprojx&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;根据自己的 OneNET 产品配置修改 &lt;code&gt;app_config.h&lt;/code&gt;，编译后下载到 STM32F103RCT6。&lt;/p&gt;&lt;p&gt;仓库的 &lt;code&gt;烧录文件&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;EndDevice_CC2530.hex&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;Coordinator_CC2530.hex&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;Gateway_STM32.hex&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;code&gt;Gateway_STM32.hex&lt;/code&gt; 烧录到协调器板上的 STM32；&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;Coordinator_CC2530.hex&lt;/code&gt; 烧录到协调器 CC2530；&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;EndDevice_CC2530.hex&lt;/code&gt; 烧录到终端 CC2530；&lt;/li&gt;
&lt;li&gt;先启动协调器，再启动终端。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&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;观察终端 OLED 是否经历 &lt;code&gt;SEARCHING&lt;/code&gt;、&lt;code&gt;JOINING&lt;/code&gt; 并最终显示 &lt;code&gt;ZB:ONLINE&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;检查终端是否显示合理的温湿度；&lt;/li&gt;
&lt;li&gt;查看网关 OLED 的 &lt;code&gt;UART B/E&lt;/code&gt; 计数；&lt;/li&gt;
&lt;li&gt;确认网关能够显示真实节点地址和传感器数据；&lt;/li&gt;
&lt;li&gt;通过 USB1 打开 115200、8N1 串口日志；&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;[CC2530 RX]&lt;/code&gt; 和 &lt;code&gt;[ZIGBEE]&lt;/code&gt; 日志；&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;[ML307 TX]&lt;/code&gt;、&lt;code&gt;[ML307 RX]&lt;/code&gt; 和 OneNET 的 &lt;code&gt;code:200&lt;/code&gt; 回复；&lt;/li&gt;
&lt;li&gt;最后在 OneNET 查看设备在线状态、属性更新时间及远程控制结果。&lt;/li&gt;
&lt;/ol&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;B=0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;UART4 未收到数据，应检查固件、PC11/RXD4 信号和 CC2530 运行状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;B&amp;gt;0&lt;/code&gt;、&lt;code&gt;E=0&lt;/code&gt;、节点仍为 &lt;code&gt;----&lt;/code&gt;&lt;/td&gt;&lt;td&gt;串口有数据，但尚未收到完整有效的 &lt;code&gt;$TH&lt;/code&gt; 帧&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;B&amp;gt;0&lt;/code&gt;、&lt;code&gt;E&amp;gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;帧格式、波特率、接线或校验值不匹配&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ZigBee 日志正常但云端不更新&lt;/td&gt;&lt;td&gt;检查 SIM 卡、天线、ML307R 注册状态及 OneNET 鉴权参数&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&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;section&gt;&lt;h3&gt;1. STM32 串口引脚判断错误&lt;a href=&quot;#1-stm32-串口引脚判断错误&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最初将 CC2530 数据接收配置为 PA3/USART2_RX，但开发板原理图中的板内网络 &lt;code&gt;RXD4&lt;/code&gt; 实际连接到 PC11/UART4_RX，导致网关一直显示 &lt;code&gt;UART B:0&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;修正 UART4 引脚与中断入口后，STM32 才能正确接收协调器数据。这次问题说明，在使用集成开发板时，不能只根据 MCU 的常用串口引脚猜测连接关系，必须以原理图上的网络名和实际走线为准。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 中断中执行过多业务导致时序风险&lt;a href=&quot;#2-中断中执行过多业务导致时序风险&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;早期实现如果在串口中断内直接解析协议或处理业务，很容易延长中断占用时间。最终方案让中断仅负责写环形缓冲区，将协议解析放到主循环中，既减少丢字节风险，也便于实现错误恢复。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. DHT11 时序与 ZigBee 任务冲突&lt;a href=&quot;#3-dht11-时序与-zigbee-任务冲突&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;DHT11 对微秒级时序较敏感，但长时间关闭中断会影响 Z-Stack 的系统节拍和无线任务。最终只在约 4 ms 的关键 40 位读取窗口中进行必要的临界区保护，而 18～20 ms 的启动阶段保持中断开启，在传感器时序与协议栈实时性之间取得平衡。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 发送成功不等于云端接收成功&lt;a href=&quot;#4-发送成功不等于云端接收成功&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;串口发送完 AT 指令只能说明命令已经交给 4G 模块，不能证明 OneNET 已经接受数据。因此程序将平台返回的 &lt;code&gt;code:200&lt;/code&gt; 作为成功依据，并且只在收到成功回复后增加上传计数。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 本地告警与云端控制的状态冲突&lt;a href=&quot;#5-本地告警与云端控制的状态冲突&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;如果云端控制直接覆盖本地状态，可能出现云端关闭蜂鸣器后现场高温告警失效的问题。本项目将本地告警与云端请求分别保存，最后合并输出，使远程控制不会破坏基本的安全告警逻辑。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6. 旧版工程与新工具链的兼容问题&lt;a href=&quot;#6-旧版工程与新工具链的兼容问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;项目还处理了 IAR 10 启动文件虚拟寄存器、Z-Stack Target 内存空间不足、HAL LCD 与终端 P1.7 蜂鸣器引脚冲突，以及自定义 UART 与 Z-Stack HAL UART 对象冲突等问题。这些问题大多不是业务代码错误，而是旧协议栈、编译器版本和硬件复用共同产生的工程兼容问题。&lt;/p&gt;&lt;/section&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;将 DHT11 的单总线时序与 Z-Stack 的事件调度结合；&lt;/li&gt;
&lt;li&gt;使用 ZigBee 网络短地址区分终端，为多节点扩展保留基础；&lt;/li&gt;
&lt;li&gt;设计带校验、可读且能重新同步的 MCU 间串口协议；&lt;/li&gt;
&lt;li&gt;使用环形缓冲区实现中断接收和主循环解析解耦；&lt;/li&gt;
&lt;li&gt;将本地采集、现场告警、4G 上云和远程控制组合为闭环；&lt;/li&gt;
&lt;li&gt;网络异常时保留本地功能，并实现发布失败后的重连机制；&lt;/li&gt;
&lt;li&gt;通过 OLED、串口日志和计数器建立了较完整的可观测性。&lt;/li&gt;
&lt;/ul&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;将 DHT11 更换为 SHT30、AHT20 等精度更高的传感器；&lt;/li&gt;
&lt;li&gt;支持多个终端节点，并在云端按节点地址分别展示数据；&lt;/li&gt;
&lt;li&gt;增加终端离线判断、心跳包和数据序号，识别丢包与重复包；&lt;/li&gt;
&lt;li&gt;将阈值、采样周期等参数改为云端可配置；&lt;/li&gt;
&lt;li&gt;增加 Flash 参数持久化，设备重启后保留云端设置；&lt;/li&gt;
&lt;li&gt;对 ZigBee 应用数据增加更强的 CRC 或消息认证；&lt;/li&gt;
&lt;li&gt;将同步等待式 AT 通信改为完全非阻塞状态机；&lt;/li&gt;
&lt;li&gt;增加看门狗、断网缓存和网络恢复后的历史数据补传；&lt;/li&gt;
&lt;li&gt;对云平台密钥进行安全存储，避免将真实凭据写入公开源码。&lt;/li&gt;
&lt;/ol&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;本项目搭建了一套完整的 ZigBee—4G 无线温湿度监测系统。终端负责采集，ZigBee 负责局域无线传输，STM32 负责网关控制，ML307R 负责广域联网，OneNET 负责云端展示和远程控制。&lt;/p&gt;&lt;p&gt;在实现过程中，我不仅完成了 DHT11、OLED、串口和 4G 模块等外设驱动，更重要的是实践了事件驱动、通信协议设计、中断与缓冲区、网络状态管理、故障诊断以及多模块联调。相比单一的传感器实验，这个项目更接近一个小型但完整的物联网产品原型。&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;code&gt;app_config.h&lt;/code&gt; 中真实的 OneNET 产品 ID、设备 ID、Access Key 和 Token；&lt;/li&gt;
&lt;li&gt;不要在博客截图、串口日志或示例命令中暴露设备鉴权信息；&lt;/li&gt;
&lt;li&gt;建议提供一个 &lt;code&gt;app_config.example.h&lt;/code&gt; 示例文件，并通过 &lt;code&gt;.gitignore&lt;/code&gt; 忽略真实配置；&lt;/li&gt;
&lt;li&gt;检查历史提交中是否曾经保存密钥；若已经公开，应立即在平台侧轮换密钥；&lt;/li&gt;
&lt;li&gt;博客中的项目效果图可以使用设备实拍、终端 OLED、网关 OLED、串口日志和 OneNET 属性页面组成。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>基于 Qt+OpenCV 人脸识别考勤系统</title><link>https://benjian.xyz/posts/qt-opencv-face-attendance/</link><guid isPermaLink="true">https://benjian.xyz/posts/qt-opencv-face-attendance/</guid><description>基于 Qt Widgets、OpenCV、SeetaFace、TCP 与 SQLite 实现的客户端-服务端人脸识别考勤系统。</description><pubDate>Fri, 04 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;OpenCV-Qt-FaceAttendance 工程阅读与代码详解&lt;a href=&quot;#opencv-qt-faceattendance-工程阅读与代码详解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;1. 项目定位&lt;a href=&quot;#1-项目定位&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这是一个“摄像头客户端 + 人脸识别服务端”的 Qt Widgets 桌面考勤系统。客户端负责采集视频、检测人脸、压缩图片和展示结果；服务端负责接收图片、调用 SeetaFace 识别、查询 SQLite 员工资料、写入考勤记录并返回 JSON。&lt;/p&gt;&lt;p&gt;本文不讨论简历包装，而是沿着真实代码说明工程如何启动、一帧图像如何流转、各模块为何这样划分，以及当前实现有哪些边界。项目不是前后端 HTTP 系统，而是基于 TCP 的本地客户端/服务端程序。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;核心概念：OpenCV 负责摄像头、图像处理和 JPEG 编解码，不负责网络通信；Qt Network 负责 TCP 收发；&lt;code&gt;QDataStream&lt;/code&gt; 负责把长度字段和字节数组序列化到 TCP 字节流。JPEG 是标准格式，“长度 + JPEG 数据”是本工程约定的应用层封装。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2. 技术栈&lt;a href=&quot;#2-技术栈&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;C++11&lt;/li&gt;
&lt;li&gt;Qt Widgets：&lt;code&gt;.ui&lt;/code&gt; 界面、&lt;code&gt;QMainWindow&lt;/code&gt;、信号槽、&lt;code&gt;QTimer/QDataStream&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OpenCV 4.x：&lt;code&gt;VideoCapture&lt;/code&gt;、灰度转换、Haar Cascade、JPEG 编解码&lt;/li&gt;
&lt;li&gt;SeetaFace 6：人脸检测、关键点定位、人脸注册和特征库查询&lt;/li&gt;
&lt;li&gt;Qt Network：&lt;code&gt;QTcpSocket&lt;/code&gt;、&lt;code&gt;QTcpServer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Qt SQL + SQLite：员工表、考勤表和人脸 ID 映射&lt;/li&gt;
&lt;li&gt;qmake：两个工程分别由 &lt;code&gt;.pro&lt;/code&gt; 文件构建&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3. 目录与职责&lt;a href=&quot;#3-目录与职责&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;OpenCV-Qt-FaceAttendance/&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;├─ FaceAttendanceClient/       # 客户端：摄像头和结果展示&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;│  ├─ faceattendance.cpp/.h&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;│  ├─ faceattendance.ui&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;│  └─ FaceAttendence.pro&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;├─ FaceAttendanceServer/       # 服务端：识别、数据库和管理窗口&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;│  ├─ attendancewin.cpp/.h     # TCP 服务和考勤主流程&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;│  ├─ qfaceobject.cpp/.h       # SeetaFace 封装&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;│  ├─ registerwin.cpp/.h       # 人员注册&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;│  ├─ selectwin.cpp/.h         # 员工/考勤查询&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;│  └─ FaceAttendaceServer.pro&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;├─ SeetaFace/                  # 仓库内附带的头文件、库和模型目录&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;└─ opencv452/                  # 仓库内附带的 OpenCV 开发文件&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;FaceAttendanceClient/faceattendance.cpp&lt;/code&gt; -&amp;gt; &lt;code&gt;FaceAttendanceServer/attendancewin.cpp&lt;/code&gt; -&amp;gt; &lt;code&gt;qfaceobject.cpp&lt;/code&gt; -&amp;gt; &lt;code&gt;registerwin.cpp&lt;/code&gt; -&amp;gt; &lt;code&gt;main.cpp&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4. 总体架构&lt;a href=&quot;#4-总体架构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;客户端 FaceAttendance::timerEvent()&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;├─ OpenCV 采集帧&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;├─ Haar Cascade 检测第一张人脸&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;├─ Mat 人脸区域编码为 JPEG&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;└─ QDataStream 写入 [长度][JPEG]，TCP 发给 9999 端口&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;/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;服务端 AttendanceWin::read_data()&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;├─ 按长度处理 TCP 半包&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;├─ JPEG 解码为 cv::Mat&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;└─ queued signal 投递到 QFaceObject 工作线程&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;QFaceObject::face_query()&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;└─ SeetaFace Query -&amp;gt; faceId/相似度&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;AttendanceWin::recv_faceid()&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;├─ faceId 查询 employee 表&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;├─ 插入 attendance 表&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;└─ JSON 返回客户端&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;↓&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;客户端 recv_data() 更新姓名、工号、部门、时间&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;/section&gt;&lt;section&gt;&lt;h2&gt;5. 一次完整打卡的数据流&lt;a href=&quot;#5-一次完整打卡的数据流&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&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;│ BGR 视频帧&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;FaceAttendance::timerEvent()&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;│ 灰度化、Haar 检测、截取第一张人脸 ROI&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;│ OpenCV imencode() 生成 JPEG 字节&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;QDataStream + QTcpSocket&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;│ [quint64 长度][QByteArray/JPEG]&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;AttendanceWin::read_data()&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;│ 等待完整数据、QPixmap 显示、OpenCV imdecode()&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;query(cv::Mat&amp;amp;) 信号&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;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;QFaceObject::face_query()&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;│ SeetaFace Query -&amp;gt; faceId + similarity&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;▼&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;send_faceid(int64_t) 信号&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;│ 回到 AttendanceWin 所在线程&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;▼&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;AttendanceWin::recv_faceid()&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;/span&gt;&lt;/span&gt;&lt;span&gt;│ employee 查询 -&amp;gt; attendance 插入 -&amp;gt; JSON 回包&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&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;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;FaceAttendance::recv_data()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&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;/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;JPEG 字节&lt;/td&gt;&lt;td&gt;某一时刻采集的人脸图片&lt;/td&gt;&lt;td&gt;只在网络传输过程中短暂存在&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;faceId&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SeetaFace 特征库中某个人脸的编号&lt;/td&gt;&lt;td&gt;&lt;code&gt;face.db&lt;/code&gt; 与 &lt;code&gt;employee.faceId&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;employeeId&lt;/code&gt;&lt;/td&gt;&lt;td&gt;业务系统中的员工编号&lt;/td&gt;&lt;td&gt;SQLite &lt;code&gt;server.db&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;code&gt;faceId&lt;/code&gt; 不是员工号。识别算法只返回 &lt;code&gt;faceId&lt;/code&gt;，服务端必须再查询 &lt;code&gt;employee&lt;/code&gt; 表，才能得到业务身份。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6. 客户端代码深读&lt;a href=&quot;#6-客户端代码深读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;6.1 对象和事件来源&lt;a href=&quot;#61-对象和事件来源&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;cv::&lt;/span&gt;&lt;span&gt;VideoCapture&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cap&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;cv::&lt;/span&gt;&lt;span&gt;CascadeClassifier&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cascade&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; // Haar 人脸检测器&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;QTcpSocket&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;msocket&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;QTimer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mtimer&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;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;send_flag&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;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;code&gt;main.cpp&lt;/code&gt; 创建 &lt;code&gt;QApplication&lt;/code&gt; 和 &lt;code&gt;FaceAttendance&lt;/code&gt; 后进入 &lt;code&gt;a.exec()&lt;/code&gt;。此后程序不再靠手写循环运行，而由 Qt 事件循环分发定时器、Socket 和界面事件。&lt;/p&gt;&lt;p&gt;构造函数中的 &lt;code&gt;startTimer(100)&lt;/code&gt; 使用的是 &lt;code&gt;QObject&lt;/code&gt; 的基础定时器，所以超时后进入重写的 &lt;code&gt;timerEvent()&lt;/code&gt;；&lt;code&gt;mtimer&lt;/code&gt; 则是一个独立的 &lt;code&gt;QTimer&lt;/code&gt;，通过 &lt;code&gt;timeout&lt;/code&gt; 信号发起连接。两者用途不同。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.2 摄像头采集与 Haar 检测&lt;a href=&quot;#62-摄像头采集与-haar-检测&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每次 &lt;code&gt;timerEvent()&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;cv::&lt;/span&gt;&lt;span&gt;Mat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;srcImage&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;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;cap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;grab&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;cap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;read&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;srcImage&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;/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;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;srcImage&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;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nullptr&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&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;code&gt;grab()&lt;/code&gt; 推进视频流，&lt;code&gt;read()&lt;/code&gt; 取出并解码当前帧。随后将 BGR 图像转为灰度图，再使用 &lt;code&gt;detectMultiScale()&lt;/code&gt; 得到人脸矩形列表。灰度化可以减少输入通道数，符合 Haar 分类器的工作方式。&lt;/p&gt;&lt;p&gt;当前实现只处理 &lt;code&gt;faceRects.at(0)&lt;/code&gt;，所以画面中有多张脸时只识别检测列表中的第一张。列表顺序不保证等同于“离摄像头最近”或“面积最大”；若需要明确策略，应按矩形面积或中心距离排序。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.3 ROI 的内存关系&lt;a href=&quot;#63-roi-的内存关系&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;cv::&lt;/span&gt;&lt;span&gt;Mat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;faceImage&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;srcImage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rect&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;这行通常创建的是 ROI 视图，&lt;code&gt;faceImage&lt;/code&gt; 与 &lt;code&gt;srcImage&lt;/code&gt; 共享底层像素内存，而不是立即复制整块数据。当前函数紧接着调用 &lt;code&gt;imencode()&lt;/code&gt;，在 &lt;code&gt;srcImage&lt;/code&gt; 仍有效时完成编码，所以可以工作。如果要把 ROI 保存到异步队列，应使用 &lt;code&gt;faceImage.clone()&lt;/code&gt; 获得独立数据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.4 JPEG 编码与 TCP 封装&lt;a href=&quot;#64-jpeg-编码与-tcp-封装&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;std::&lt;/span&gt;&lt;span&gt;vector&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;uchar&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buf&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;cv&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;imencode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.jpg&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;faceImage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;buf&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;QByteArray&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;reinterpret_cast&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&amp;gt;(&lt;/span&gt;&lt;span&gt;buf&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;buf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;size&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;QByteArray&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sendData&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;QDataStream&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;&amp;amp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;sendData&lt;/span&gt;&lt;span&gt;, QIODevice::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;WriteOnly&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;stream&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setVersion&lt;/span&gt;&lt;span&gt;(QDataStream::&lt;/span&gt;&lt;span&gt;Qt_5_14&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;quint64&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bytesize&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;size&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;stream&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bytesize&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;byte&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;msocket&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sendData&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;code&gt;imencode()&lt;/code&gt; 把像素矩阵编码为标准 JPEG。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QDataStream&lt;/code&gt; 把 &lt;code&gt;quint64&lt;/code&gt; 和 &lt;code&gt;QByteArray&lt;/code&gt; 按 Qt 的序列化规则写入 &lt;code&gt;sendData&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QTcpSocket::write()&lt;/code&gt; 把 &lt;code&gt;sendData&lt;/code&gt; 交给 Socket 发送缓冲区。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;code&gt;write()&lt;/code&gt; 成功只代表数据被接受进 Qt/操作系统缓冲区，不代表服务端已经完成接收或识别。若要确认发送进度，可以监听 &lt;code&gt;bytesWritten&lt;/code&gt;；若要确认业务完成，则必须等待服务端回包。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.5 &lt;code&gt;send_flag&lt;/code&gt; 状态变化&lt;a href=&quot;#65-send_flag-状态变化&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;send_flag&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;没有检测到人脸 -&amp;gt; send_flag = 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;连续检测到人脸 -&amp;gt; 0, 1, 2, 3...&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;send_flag &amp;gt; 2  -&amp;gt; 发送一次，然后设置为 -2&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;人脸继续存在   -&amp;gt; 保持 -2，不再发送&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;人脸离开       -&amp;gt; 重置为 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;发送后设置为 &lt;code&gt;-2&lt;/code&gt;，后续帧不会再进入 &lt;code&gt;send_flag &amp;gt;= 0&lt;/code&gt; 分支，因此同一张脸会保持“不再发送”状态，直到检测不到人脸时重置为 &lt;code&gt;0&lt;/code&gt;。它是一种简单的抗抖和防重复打卡策略，不是带时间窗口的通用限流器。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.6 BGR/RGB 与图像生命周期&lt;a href=&quot;#66-bgrrgb-与图像生命周期&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;OpenCV 彩色图默认是 BGR，Qt 的 &lt;code&gt;QImage::Format_RGB888&lt;/code&gt; 需要 RGB，因此显示前必须交换通道。&lt;code&gt;QImage&lt;/code&gt; 构造函数此处引用 &lt;code&gt;cv::Mat&lt;/code&gt; 的现有内存，没有深拷贝；当前代码马上用 &lt;code&gt;QPixmap::fromImage()&lt;/code&gt; 生成用于控件显示的对象，没有把临时 &lt;code&gt;QImage&lt;/code&gt; 保存到函数外。&lt;/p&gt;&lt;p&gt;如果以后将 &lt;code&gt;QImage&lt;/code&gt; 缓存为成员变量，应调用 &lt;code&gt;copy()&lt;/code&gt; 或确保对应 &lt;code&gt;cv::Mat&lt;/code&gt; 一直有效，否则可能访问已经释放或被下一帧覆盖的内存。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.7 连接和断线重连&lt;a href=&quot;#67-连接和断线重连&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;客户端启动后每 5 秒调用一次：&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;msocket&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;connectToHost&lt;/span&gt;&lt;span&gt;(QHostAddress::&lt;/span&gt;&lt;span&gt;LocalHost&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;9999&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;连接成功后停止重连定时器；断开时以 3 秒间隔重新启动。&lt;code&gt;QHostAddress::LocalHost&lt;/code&gt; 对应本机回环地址，因此当前配置只能连接本机服务端。若服务端在另一台设备上，需要把地址改为配置项。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.8 JSON 结果接收&lt;a href=&quot;#68-json-结果接收&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;recv_data()&lt;/code&gt; 使用 &lt;code&gt;readAll()&lt;/code&gt; 读取当前已经到达的字节，并调用 &lt;code&gt;QJsonDocument::fromJson()&lt;/code&gt;。识别成功时更新控件，姓名为空时隐藏成功提示。&lt;/p&gt;&lt;p&gt;这里没有为 JSON 设置消息边界。TCP 可能把一条 JSON 分成多次到达，也可能把多条 JSON 合并到一次 &lt;code&gt;readyRead&lt;/code&gt; 中。因此正式实现应该保留接收缓冲区，并给 JSON 增加长度字段或换行分隔符。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;7. 服务端网络代码深读&lt;a href=&quot;#7-服务端网络代码深读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;7.1 监听和连接对象&lt;a href=&quot;#71-监听和连接对象&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;AttendanceWin&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;mserver&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;listen&lt;/span&gt;&lt;span&gt;(QHostAddress::&lt;/span&gt;&lt;span&gt;Any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;9999&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;code&gt;Any&lt;/code&gt; 表示监听本机所有网卡地址。收到 &lt;code&gt;newConnection&lt;/code&gt; 后，&lt;code&gt;nextPendingConnection()&lt;/code&gt; 返回代表该客户端的 &lt;code&gt;QTcpSocket&lt;/code&gt;，其 &lt;code&gt;readyRead&lt;/code&gt; 信号连接到 &lt;code&gt;read_data()&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;当前窗口只有一个 &lt;code&gt;QTcpSocket *msocket&lt;/code&gt; 和一个 &lt;code&gt;quint64 bsize&lt;/code&gt;。后连接的客户端会覆盖 &lt;code&gt;msocket&lt;/code&gt;，不同连接也会共享 &lt;code&gt;bsize&lt;/code&gt;，因此代码实际适用于一个考勤客户端，并不具备完整的多客户端并发管理能力。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.2 为什么要有长度字段&lt;a href=&quot;#72-为什么要有长度字段&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;TCP 是有序字节流，不保留应用层的消息边界。客户端一次 &lt;code&gt;write()&lt;/code&gt; 的内容，服务端可能分多次收到；客户端连续多次 &lt;code&gt;write()&lt;/code&gt;，服务端也可能一次收到多条消息。&lt;/p&gt;&lt;p&gt;长度字段的作用是告诉接收端：“下一段有效载荷应有多少字节”。服务端先读取 &lt;code&gt;bsize&lt;/code&gt;，如果剩余字节不足就返回，等待下一次 &lt;code&gt;readyRead&lt;/code&gt;。这处理了单个图片分段到达的情况。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.3 &lt;code&gt;QByteArray&lt;/code&gt; 序列化细节&lt;a href=&quot;#73-qbytearray-序列化细节&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;代码使用 &lt;code&gt;stream &amp;lt;&amp;lt; bytesize &amp;lt;&amp;lt; byte&lt;/code&gt;，接收端使用 &lt;code&gt;stream &amp;gt;&amp;gt; bsize&lt;/code&gt; 和 &lt;code&gt;stream &amp;gt;&amp;gt; data&lt;/code&gt;。&lt;code&gt;QDataStream&lt;/code&gt; 在序列化 &lt;code&gt;QByteArray&lt;/code&gt; 时本身也会写入数组长度，因此流中并非简单的“8 字节长度后紧跟裸 JPEG”，而是 Qt 序列化格式：外层手写的 &lt;code&gt;quint64&lt;/code&gt; 后还有 &lt;code&gt;QByteArray&lt;/code&gt; 自己的长度信息。&lt;/p&gt;&lt;p&gt;由于两端都设置 &lt;code&gt;QDataStream::Qt_5_14&lt;/code&gt; 并使用对称的 &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;/&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;，它们能够正确解析。但若以后使用非 Qt 客户端，就必须明确字节序和 &lt;code&gt;QByteArray&lt;/code&gt; 序列化规则，或者改成手工写入固定头部与裸 payload。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.4 当前半包处理的边界&lt;a href=&quot;#74-当前半包处理的边界&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;现有逻辑表达了正确方向，但还不是通用解析器：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sizeof(bsize)&lt;/code&gt; 只检查外层长度字段，不代表后面的 &lt;code&gt;QByteArray&lt;/code&gt; 序列化头已经完整；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bytesAvailable() &amp;gt;= bsize&lt;/code&gt; 没有把 &lt;code&gt;QByteArray&lt;/code&gt; 自身的长度前缀计算在内；&lt;/li&gt;
&lt;li&gt;一次到达多个完整包时没有 &lt;code&gt;while&lt;/code&gt; 循环继续解析；&lt;/li&gt;
&lt;li&gt;没有限制 &lt;code&gt;bsize&lt;/code&gt; 最大值，错误长度可能造成异常资源消耗；&lt;/li&gt;
&lt;li&gt;Socket 断开后没有清理残留接收状态。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;更稳妥的实现是把所有新字节追加到 &lt;code&gt;receiveBuffer&lt;/code&gt;，用显式固定长度包头解析，只有当包头和 payload 都完整时才取出一包，并循环处理缓冲区中的后续完整包。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.5 JPEG 解码&lt;a href=&quot;#75-jpeg-解码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;收到完整 &lt;code&gt;QByteArray&lt;/code&gt; 后，代码一方面使用 &lt;code&gt;QPixmap::loadFromData()&lt;/code&gt; 在服务端界面显示图片，另一方面转换为 &lt;code&gt;std::vector&amp;lt;uchar&amp;gt;&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;cv&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;imdecode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;buf&lt;/span&gt;&lt;span&gt;, cv::&lt;/span&gt;&lt;span&gt;IMREAD_COLOR&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;输出是 BGR &lt;code&gt;cv::Mat&lt;/code&gt;。若 JPEG 损坏，&lt;code&gt;faceImage.empty()&lt;/code&gt; 为真；当前代码只打印错误，仍然发出识别信号，合理的异常路径应该在此直接返回并向客户端发送错误结果。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;8. SeetaFace 模块深读&lt;a href=&quot;#8-seetaface-模块深读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;8.1 三个模型的作用&lt;a href=&quot;#81-三个模型的作用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&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;&lt;code&gt;fd_2_00.dat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;找出图像中的人脸区域&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;pd_2_00_pts5.dat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;定位眼睛、鼻子、嘴角等 5 个关键点&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;fr_2_10.dat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;提取人脸特征并进行相似度检索&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;code&gt;QFaceObject&lt;/code&gt; 用静态 &lt;code&gt;FaceEngine *fengineptr&lt;/code&gt; 保证进程中共享同一引擎实例。首次构造时加载模型，并调用 &lt;code&gt;initializeDatabase()&lt;/code&gt; 加载或创建 &lt;code&gt;face.db&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.2 &lt;code&gt;face.db&lt;/code&gt; 和 &lt;code&gt;server.db&lt;/code&gt; 的区别&lt;a href=&quot;#82-facedb-和-serverdb-的区别&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;face.db&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;SeetaFace 自己保存的人脸特征库&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;保存特征和算法生成的 faceId&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;server.db&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;SQLite 业务数据库&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;保存员工资料、faceId 映射和考勤记录&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;face.db&lt;/code&gt; 会丢失人脸特征；只保留 &lt;code&gt;server.db&lt;/code&gt; 时，员工资料还在，但识别引擎无法找到对应特征。反过来，只保留 &lt;code&gt;face.db&lt;/code&gt; 时算法可能返回 &lt;code&gt;faceId&lt;/code&gt;，但服务端查不到员工姓名。两个数据库必须保持一致。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.3 注册流程&lt;a href=&quot;#83-注册流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;face_register()&lt;/code&gt; 依次完成：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;检查输入图像非空。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;cv::Mat&lt;/code&gt; 的数据指针、宽、高、通道数填入 &lt;code&gt;SeetaImageData&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;DetectFaces()&lt;/code&gt;，没有人脸就返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;先调用 &lt;code&gt;Query()&lt;/code&gt;，相似度大于 &lt;code&gt;0.7&lt;/code&gt; 时认为已经注册。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;Register()&lt;/code&gt; 获取新 &lt;code&gt;faceId&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;Save(&quot;./face.db&quot;)&lt;/code&gt; 持久化特征库。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;之后 &lt;code&gt;RegisterWin&lt;/code&gt; 才将个人资料和这个 &lt;code&gt;faceId&lt;/code&gt; 写入 SQLite。由于这两步不在同一个事务中，存在特征注册成功但业务数据插入失败的可能，此时 &lt;code&gt;face.db&lt;/code&gt; 中会出现没有对应员工记录的孤立特征。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.4 查询流程和阈值&lt;a href=&quot;#84-查询流程和阈值&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;face_query()&lt;/code&gt; 调用 &lt;code&gt;Query()&lt;/code&gt; 得到候选 &lt;code&gt;faceId&lt;/code&gt; 与 &lt;code&gt;similarity&lt;/code&gt;。信号中使用 &lt;code&gt;similarity &amp;gt; 0.4 ? faceid : -1&lt;/code&gt;，低于阈值按陌生人处理。&lt;/p&gt;&lt;p&gt;注册去重阈值 &lt;code&gt;0.7&lt;/code&gt; 和识别阈值 &lt;code&gt;0.4&lt;/code&gt; 用途不同：前者避免重复录入，后者决定一次识别是否可信。它们是工程中的经验值，不应理解为通用标准。部署时需要根据摄像头、光照、人员规模以及误识率/拒识率要求重新测试。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9. Qt 多线程模型&lt;a href=&quot;#9-qt-多线程模型&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;9.1 当前线程关系&lt;a href=&quot;#91-当前线程关系&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;服务端 GUI 线程&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;QTcpServer/QTcpSocket&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;JPEG 解码与界面更新&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;SQLite 查询和写入&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;│ query(cv::Mat&amp;amp;) 信号&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;▼&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;/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;QFaceObject::face_query()&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;SeetaFace::Query()&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;│ send_faceid(int64_t) 信号&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;服务端 GUI 线程&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;AttendanceWin::recv_faceid()&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;fobject.moveToThread(thread)&lt;/code&gt; 改变 &lt;code&gt;QFaceObject&lt;/code&gt; 的线程归属。发送者和接收者位于不同线程时，Qt 的自动连接会使用队列方式：信号参数进入目标线程事件队列，槽函数稍后在目标线程执行。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.2 元类型注册&lt;a href=&quot;#92-元类型注册&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;main.cpp&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;qRegisterMetaType&lt;/span&gt;&lt;span&gt;&amp;lt;cv::&lt;/span&gt;&lt;span&gt;Mat&lt;/span&gt;&lt;span&gt;&amp;gt;(&lt;/span&gt;&lt;span&gt;&quot;cv::Mat&amp;amp;&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;qRegisterMetaType&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;int64_t&lt;/span&gt;&lt;span&gt;&amp;gt;(&lt;/span&gt;&lt;span&gt;&quot;int64_t&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;p&gt;Qt 的队列连接需要知道如何复制信号参数，所以自定义或非 Qt 内建类型需要注册。&lt;code&gt;cv::Mat&lt;/code&gt; 本身采用引用计数，普通复制通常是浅拷贝；如果发送后源数据可能被改写，跨线程前应 &lt;code&gt;clone()&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.3 为什么识别放到工作线程&lt;a href=&quot;#93-为什么识别放到工作线程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;SeetaFace 推理可能耗时。如果直接在 &lt;code&gt;readyRead&lt;/code&gt; 槽中识别，GUI 线程会停止处理绘制、输入和后续网络事件。移入工作线程后，界面仍可响应，Socket 事件也能继续分发。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.4 生命周期问题&lt;a href=&quot;#94-生命周期问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当前 &lt;code&gt;QThread *thread&lt;/code&gt; 是构造函数中的局部指针，没有保存为成员，也没有在窗口退出时执行 &lt;code&gt;quit()&lt;/code&gt; 和 &lt;code&gt;wait()&lt;/code&gt;；&lt;code&gt;QFaceObject&lt;/code&gt; 又是 &lt;code&gt;AttendanceWin&lt;/code&gt; 的成员对象。关闭程序时可能出现“线程仍在运行，对象已经析构”的问题。&lt;/p&gt;&lt;p&gt;更标准的写法是让窗口保存线程指针，连接 &lt;code&gt;thread-&amp;gt;finished&lt;/code&gt; 与清理逻辑，并在析构前按顺序停止接收新任务、&lt;code&gt;quit()&lt;/code&gt;、&lt;code&gt;wait()&lt;/code&gt;。共享的静态 &lt;code&gt;FaceEngine&lt;/code&gt; 也需要明确释放者并确认注册和查询不会跨线程并发访问。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;10. 数据库和管理功能&lt;a href=&quot;#10-数据库和管理功能&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;10.1 数据表&lt;a href=&quot;#101-数据表&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&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;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&lt;/span&gt;&lt;span&gt; employee(&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;employeeId &lt;/span&gt;&lt;span&gt;INTEGER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PRIMARY KEY&lt;/span&gt;&lt;span&gt; AUTOINCREMENT,&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;name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VARCHAR&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;256&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;sex &lt;/span&gt;&lt;span&gt;VARCHAR&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;32&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;birthday &lt;/span&gt;&lt;span&gt;TEXT&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;address&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TEXT&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;phone &lt;/span&gt;&lt;span&gt;TEXT&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;faceId &lt;/span&gt;&lt;span&gt;INTEGER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UNIQUE&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;headfile &lt;/span&gt;&lt;span&gt;TEXT&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;/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;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&lt;/span&gt;&lt;span&gt; attendance(&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;attendanceId &lt;/span&gt;&lt;span&gt;INTEGER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PRIMARY KEY&lt;/span&gt;&lt;span&gt; AUTOINCREMENT,&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;employeeId &lt;/span&gt;&lt;span&gt;INTEGER&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;attendanceTime &lt;/span&gt;&lt;span&gt;TIMESTAMP&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DEFAULT&lt;/span&gt;&lt;span&gt; CURRENT_TIMESTAMP&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;/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;employee.faceId&lt;/code&gt; 唯一，防止一个算法 ID 关联多名员工。&lt;code&gt;attendance.employeeId&lt;/code&gt; 当前没有外键，数据库不会自动校验员工是否存在，也不会级联处理员工删除。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.2 注册窗口&lt;a href=&quot;#102-注册窗口&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;注册窗口支持选择图片和摄像头拍照。提交时先完成人脸特征注册，再用 &lt;code&gt;QSqlTableModel&lt;/code&gt; 构造员工记录，调用 &lt;code&gt;insertRecord()&lt;/code&gt; 和 &lt;code&gt;submitAll()&lt;/code&gt; 写入数据库。&lt;/p&gt;&lt;p&gt;头像保存到 &lt;code&gt;./data/&lt;/code&gt;，文件名使用姓名的 Base64 字符串。它能降低中文文件名带来的路径兼容问题，但同名员工可能覆盖同一文件；更可靠的方式是使用员工编号或 UUID。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.3 识别、回包和考勤落库&lt;a href=&quot;#103-识别回包和考勤落库&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;recv_faceid()&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;SELECT&lt;/span&gt;&lt;span&gt; employeeId, &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; employee &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; faceId &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; :faceId;&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;找到员工后，服务端构造包含员工号、姓名、部门和时间的 JSON，写回客户端，再向 &lt;code&gt;attendance&lt;/code&gt; 插入员工号。当前部门值是硬编码，不来自数据表；时间由服务端本机生成。&lt;/p&gt;&lt;p&gt;当前实现没有上下班规则、时间窗口或去重约束。因此只要客户端再次发送并识别成功，就会新增考勤记录。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.4 查询窗口&lt;a href=&quot;#104-查询窗口&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;SelectWin&lt;/code&gt; 根据单选按钮选择 &lt;code&gt;employee&lt;/code&gt; 或 &lt;code&gt;attendance&lt;/code&gt; 表，再用 &lt;code&gt;QSqlTableModel::select()&lt;/code&gt; 加载到 &lt;code&gt;QTableView&lt;/code&gt;。这展示了 Qt SQL 的 Model/View 用法，但没有条件筛选、分页、排序规则或关联员工姓名。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11. 构建和运行&lt;a href=&quot;#11-构建和运行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;11.1 两个独立工程&lt;a href=&quot;#111-两个独立工程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;客户端和服务端分别由 &lt;code&gt;FaceAttendence.pro&lt;/code&gt; 与 &lt;code&gt;FaceAttendaceServer.pro&lt;/code&gt; 构建。客户端需要 Qt &lt;code&gt;core/gui/widgets/network&lt;/code&gt;；服务端还需要 &lt;code&gt;sql&lt;/code&gt;，两者都链接 OpenCV 和 SeetaFace。&lt;/p&gt;&lt;p&gt;建议运行顺序：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;检查 Qt SQLite 驱动、OpenCV DLL、SeetaFace DLL 和模型文件。&lt;/li&gt;
&lt;li&gt;启动服务端，确认 &lt;code&gt;server.db&lt;/code&gt; 打开且 9999 端口监听成功。&lt;/li&gt;
&lt;li&gt;在服务端注册窗口录入至少一个人脸与员工资料。&lt;/li&gt;
&lt;li&gt;启动客户端，确认摄像头画面和 TCP 连接日志。&lt;/li&gt;
&lt;li&gt;面向摄像头，依次观察检测、发送、服务端识别、数据库写入和客户端回显。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.2 当前路径依赖&lt;a href=&quot;#112-当前路径依赖&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;源码和 &lt;code&gt;.pro&lt;/code&gt; 文件包含 &lt;code&gt;D:\FaceRecognitionAttendanceSystem\...&lt;/code&gt; 形式的绝对路径。若工程位于其他目录，需要同步修改 OpenCV、SeetaFace 和模型路径。更好的做法是使用 qmake 变量、相对目录或配置文件，并用 &lt;code&gt;QCoreApplication::applicationDirPath()&lt;/code&gt; 定位运行资源。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.3 Linux 移植不是直接重新编译&lt;a href=&quot;#113-linux-移植不是直接重新编译&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当前依赖目录含 MinGW 库和 Windows DLL，不能直接用于 Linux。移植时需要 Linux 版 OpenCV/SeetaFace 动态库，修改 &lt;code&gt;.pro&lt;/code&gt; 的头文件与链接目录，处理 &lt;code&gt;.so&lt;/code&gt; 搜索路径、摄像头设备权限、Qt 平台插件以及模型资源路径。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;12. 代码中的关键限制与改造思路&lt;a href=&quot;#12-代码中的关键限制与改造思路&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;12.1 &lt;code&gt;server.db&lt;/code&gt; 的创建判断&lt;a href=&quot;#121-serverdb-的创建判断&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;服务端 &lt;code&gt;main.cpp&lt;/code&gt; 用 &lt;code&gt;QDir::exists(&quot;./server.db&quot;)&lt;/code&gt; 和 &lt;code&gt;mkdir(&quot;./server.db&quot;)&lt;/code&gt; 处理数据库路径，但 &lt;code&gt;server.db&lt;/code&gt; 应是文件而不是目录。SQLite 在文件不存在时可以自动创建；这里应直接 &lt;code&gt;setDatabaseName()&lt;/code&gt; 后 &lt;code&gt;open()&lt;/code&gt;，或用 &lt;code&gt;QFile::exists()&lt;/code&gt; 判断。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.2 多客户端会话&lt;a href=&quot;#122-多客户端会话&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;QTcpSocket *socket&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;QByteArray receiveBuffer&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;quint64 expectedPayloadSize&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;ParseState state&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;Socket 断开时销毁对应会话，不让不同客户端共享 &lt;code&gt;bsize&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.3 统一消息格式&lt;a href=&quot;#123-统一消息格式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;图片请求和 JSON 响应应使用同一种包头，例如：&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;| magic | version | messageType | payloadLength | payload |&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;messageType&lt;/code&gt; 区分图像、识别结果和错误；&lt;code&gt;payloadLength&lt;/code&gt; 必须设置合理上限；解析器将新字节追加到缓冲区，并循环取出所有完整消息。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.4 业务一致性&lt;a href=&quot;#124-业务一致性&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;人脸特征库与 SQLite 是两个存储系统，无法直接使用同一个数据库事务。注册时需要补偿逻辑：如果 SQLite 插入失败，应删除刚注册的特征或记录待清理状态。删除员工时也应同步删除特征和头像。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.5 错误处理&lt;a href=&quot;#125-错误处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当前多个操作只打印日志，生产代码应向上返回明确错误，包括摄像头打不开、Haar/SeetaFace 模型加载失败、JPEG 编解码失败、Socket 写入失败、数据库提交失败和特征库保存失败。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;13. 建议的调试路径&lt;a href=&quot;#13-建议的调试路径&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;cap.isOpened()&lt;/code&gt; 是否为真，帧尺寸和 &lt;code&gt;empty()&lt;/code&gt; 是否正常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测层：&lt;/strong&gt; Haar 文件是否加载成功，&lt;code&gt;faceRects.size()&lt;/code&gt; 是否大于 0。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编码层：&lt;/strong&gt; &lt;code&gt;imencode()&lt;/code&gt; 是否成功，JPEG 字节数是否合理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接层：&lt;/strong&gt; Socket 是否进入 &lt;code&gt;ConnectedState&lt;/code&gt;，服务端是否收到 &lt;code&gt;newConnection&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拆包层：&lt;/strong&gt; &lt;code&gt;bsize&lt;/code&gt; 与 &lt;code&gt;bytesAvailable()&lt;/code&gt; 是否符合预期。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解码层：&lt;/strong&gt; &lt;code&gt;imdecode()&lt;/code&gt; 后的 &lt;code&gt;cv::Mat&lt;/code&gt; 是否为空。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;识别层：&lt;/strong&gt; 模型和 &lt;code&gt;face.db&lt;/code&gt; 是否加载成功，相似度是多少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射层：&lt;/strong&gt; 返回的 &lt;code&gt;faceId&lt;/code&gt; 是否存在于 &lt;code&gt;employee&lt;/code&gt; 表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层：&lt;/strong&gt; SQL 是否执行成功，&lt;code&gt;lastError()&lt;/code&gt; 输出什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回包层：&lt;/strong&gt; JSON 是否完整到达客户端并成功解析。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14. 工程理解总结&lt;a href=&quot;#14-工程理解总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这个工程的核心不是单独某个人脸识别 API，而是把多个子系统串成一条完整链路：&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;Qt 事件循环&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;+ OpenCV 摄像头与图像处理&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;+ Qt TCP 字节流通信&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;+ SeetaFace 特征库检索&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;+ Qt 跨线程信号槽&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;+ SQLite 业务数据持久化&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;阅读时应始终区分四个边界：OpenCV 与 Qt 图像对象的内存边界、JPEG 编码与 TCP 封包的协议边界、GUI 线程与识别线程的执行边界，以及 SeetaFace &lt;code&gt;faceId&lt;/code&gt; 与业务 &lt;code&gt;employeeId&lt;/code&gt; 的数据边界。把这四处理解清楚，工程的主要结构就清楚了。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item><item><title>平衡球 Linux Qt 上位机：实现与架构</title><link>https://benjian.xyz/posts/balance-ball-linux-qt/</link><guid isPermaLink="true">https://benjian.xyz/posts/balance-ball-linux-qt/</guid><description>串口协议、实时曲线、PID 参数下发以及 CSV 与 MySQL 数据记录。</description><pubDate>Thu, 13 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;本文档的目标不是只告诉你“如何运行”，而是带你从入口、协议、
Linux I/O、线程通信一直读到 Qt 界面。读完后，你应该能自己说清数据
从 GD32 到曲线的完整路径，也能够继续修改代码。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;/media/projects/balance-console.png&quot; alt=&quot;上位机连接 PTY 模拟器后的运行界面&quot; /&gt;&lt;figcaption&gt;上位机连接 PTY 模拟器后的运行界面&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;section&gt;&lt;h2&gt;1. 这个升级项目解决了什么问题&lt;a href=&quot;#1-这个升级项目解决了什么问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;原工程的 GD32F407 负责采集小球位置、运行 FreeRTOS 任务、执行 PID 计算并
控制舵机。Linux 端不代替 MCU 的实时控制环，而是承担上位机职责：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;从串口持续接收 B0 位置帧和 B1 PID 帧。&lt;/li&gt;
&lt;li&gt;以曲线和数值展示当前位置、目标位置和 PID 参数。&lt;/li&gt;
&lt;li&gt;下发 F0 目标位置命令和 E0 PID 参数命令。&lt;/li&gt;
&lt;li&gt;将运行数据同时记录为 CSV 并上传到虚拟机 &lt;code&gt;192.168.28.128&lt;/code&gt; 上的 MySQL。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;QSqlTableModel&lt;/code&gt; 在界面中查询和刷新历史数据。&lt;/li&gt;
&lt;li&gt;在没有开发板时，通过 PTY 伪串口模拟真实设备。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这个边界很重要：&lt;strong&gt;PID 仍然在 MCU 上执行&lt;/strong&gt;。Linux 调度延迟不确定，串口也有
传输延迟，如果把 10 ms 或 50 ms 控制环移到普通 Linux 用户态，控制周期抖动会
明显变大。上位机适合做人机交互、数据管理和参数配置，MCU 适合做硬实时控制。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;2. 开发和运行环境&lt;a href=&quot;#2-开发和运行环境&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Linux 虚拟机&lt;/td&gt;&lt;td&gt;Ubuntu 24.04.3 x86_64&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;虚拟机地址&lt;/td&gt;&lt;td&gt;&lt;code&gt;bob@192.168.28.128&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;远端工程&lt;/td&gt;&lt;td&gt;&lt;code&gt;/home/bob/balance-ball-linux-host/linux-host&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Qt&lt;/td&gt;&lt;td&gt;Qt 6.4.2 Widgets&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Qt 用户安装目录&lt;/td&gt;&lt;td&gt;&lt;code&gt;/home/bob/.local/balance-qt6&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;构建工具&lt;/td&gt;&lt;td&gt;CMake + GNU g++ 13&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Linux I/O&lt;/td&gt;&lt;td&gt;termios + epoll + eventfd + timerfd&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;界面线程模型&lt;/td&gt;&lt;td&gt;Qt 主线程 + 一个串口 QThread&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Qt 被解包到用户目录，不需要 &lt;code&gt;sudo&lt;/code&gt;。&lt;code&gt;tools/install_user_qt6.sh&lt;/code&gt; 会下载 Qt Base
的开发文件、运行库和 QPA 图形插件，然后生成 &lt;code&gt;env.sh&lt;/code&gt;。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;3. 目录结构与每个目录的职责&lt;a href=&quot;#3-目录结构与每个目录的职责&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;linux-host/&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;├── CMakeLists.txt                       # 构建目标和测试入口&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;├── balance-console-screenshot.png      # 真实 PTY 联调截图&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;├── 项目实现与架构阅读指南.md       # 本文档&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;├── src/&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;│   ├── main.cpp                         # Qt 程序入口和命令行参数&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;│   ├── protocol/&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;│   │   ├── protocol_codec.h             # 协议数据结构和公共接口&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;│   │   └── protocol_codec.cpp           # 组帧、拆帧、大小端转换&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;│   ├── io/&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;│   │   ├── serial_worker.h              # 串口工作线程声明&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;│   │   └── serial_worker.cpp            # termios/epoll/eventfd/timerfd&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;│   └── app/&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;│       ├── database_config.h            # MySQL 连接参数与建表 SQL&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;│       ├── main_window.h                # 主窗口状态和接口&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;│       ├── main_window.cpp              # 界面、信号槽、CSV/MySQL 记录&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;│       ├── position_plot.h              # 实时曲线控件声明&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;│       └── position_plot.cpp            # QPainter 自绘曲线&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;├── tests/&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;│   ├── protocol_codec_test.cpp          # 协议单元测试&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;│   ├── database_integration_test.cpp    # 真实 MySQL 端到端测试&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;│   ├── main_window_test.cpp             # 目标/PID 界面语义测试&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;│   └── serial_worker_test.cpp           # 真实 PTY 串口集成测试&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;└── tools/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── device_simulator.cpp              # 无板卡设备模拟器&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── install_user_qt6.sh               # 无 sudo 安装 Qt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── run_console.sh                    # 构建并启动上位机&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── run_simulator.sh                  # 构建并启动模拟器&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;protocol&lt;/code&gt; 不知道串口，&lt;code&gt;io&lt;/code&gt; 不知道
界面布局，&lt;code&gt;app&lt;/code&gt; 通过已解析数据更新界面。这使协议可以独立测试，也使串口实现
之后能够替换成 TCP 而不重写整个界面。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;4. 整体架构和线程边界&lt;a href=&quot;#4-整体架构和线程边界&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;B0/B1/F0/E0&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;bytesReceived 排队信号&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;enqueueFrame&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;GD32 / PTY 模拟器&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Linux 串口从设备&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;SerialWorker&lt;br /&gt;&lt;br /&gt;termios + epoll&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;StreamParser&lt;br /&gt;&lt;br /&gt;粘包/拆包/校验&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MainWindow&lt;br /&gt;&lt;br /&gt;Qt 主线程&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;PositionPlot&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;CSV 文件&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MySQL&lt;br /&gt;&lt;br /&gt;balance_ball&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;QSqlTableModel&lt;br /&gt;&lt;br /&gt;历史查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;B0/B1/F0/E0&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;bytesReceived 排队信号&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;enqueueFrame&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;GD32 / PTY 模拟器&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;Linux 串口从设备&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;SerialWorker&lt;br /&gt;&lt;br /&gt;termios + epoll&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;StreamParser&lt;br /&gt;&lt;br /&gt;粘包/拆包/校验&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MainWindow&lt;br /&gt;&lt;br /&gt;Qt 主线程&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;PositionPlot&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;CSV 文件&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;MySQL&lt;br /&gt;&lt;br /&gt;balance_ball&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;QSqlTableModel&lt;br /&gt;&lt;br /&gt;历史查询&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;4.1 Qt 主线程负责什么&lt;a href=&quot;#41-qt-主线程负责什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;创建和绘制所有 QWidget。&lt;/li&gt;
&lt;li&gt;响应连接、下发、记录等用户操作。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;StreamParser::feed()&lt;/code&gt; 解析已收到的字节块。&lt;/li&gt;
&lt;li&gt;更新数值和曲线，并将数据写入 CSV/MySQL。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;QSqlTableModel&lt;/code&gt; 在对话框中展示 MySQL 历史记录。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Qt 规定 QWidget 只能在 GUI 主线程操作，所以 &lt;code&gt;SerialWorker&lt;/code&gt; 不会直接调用
任何界面函数。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4.2 串口工作线程负责什么&lt;a href=&quot;#42-串口工作线程负责什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;打开和配置串口。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;epoll_wait()&lt;/code&gt; 中等待串口可读、可写、唤醒和定时事件。&lt;/li&gt;
&lt;li&gt;将收到的字节以 Qt 信号发给主线程。&lt;/li&gt;
&lt;li&gt;将多个下发帧按入队顺序写入串口。&lt;/li&gt;
&lt;li&gt;断线后每秒尝试重连，并每秒上报通信统计。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4.3 跨线程通信为什么是安全的&lt;a href=&quot;#43-跨线程通信为什么是安全的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;bytesReceived&lt;/code&gt; 等信号以 &lt;code&gt;Qt::QueuedConnection&lt;/code&gt; 连接到主窗口。工作线程发出信号
时，Qt 只把参数复制到主线程事件队列；真正的界面槽函数会在主线程执行。&lt;/p&gt;&lt;p&gt;主线程调用 &lt;code&gt;enqueueFrame()&lt;/code&gt; 时，发送队列受 &lt;code&gt;QMutex&lt;/code&gt; 保护。入队后通过
&lt;code&gt;eventfd&lt;/code&gt; 唤醒 &lt;code&gt;epoll_wait()&lt;/code&gt;。这不依赖 Qt 工作线程的事件循环，因为
&lt;code&gt;SerialWorker::run()&lt;/code&gt; 本身就是一个持续运行的 Linux 事件循环。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;5. 协议格式：先把“12 字节和 16 字节”说清楚&lt;a href=&quot;#5-协议格式先把12-字节和-16-字节说清楚&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;5.1 通用帧结构&lt;a href=&quot;#51-通用帧结构&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;AA AA CMD LEN PAYLOAD... CHECKSUM BB&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;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;帧头&lt;/td&gt;&lt;td&gt;2 字节&lt;/td&gt;&lt;td&gt;固定为 &lt;code&gt;AA AA&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CMD&lt;/td&gt;&lt;td&gt;1 字节&lt;/td&gt;&lt;td&gt;B0/B1/F0/E0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LEN&lt;/td&gt;&lt;td&gt;1 字节&lt;/td&gt;&lt;td&gt;&lt;strong&gt;只表示 PAYLOAD 的字节数&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PAYLOAD&lt;/td&gt;&lt;td&gt;LEN 字节&lt;/td&gt;&lt;td&gt;命令数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CHECKSUM&lt;/td&gt;&lt;td&gt;1 字节&lt;/td&gt;&lt;td&gt;CMD + LEN + PAYLOAD 的 8 位累加和&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;帧尾&lt;/td&gt;&lt;td&gt;1 字节&lt;/td&gt;&lt;td&gt;固定为 &lt;code&gt;BB&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;MCU 的上报帧之后可能还有 &lt;code&gt;0A&lt;/code&gt; 换行字节。它是传输时附加的分隔字节，
不在 LEN 中，也不参与校验。解析器会把它当作帧间噪声跳过。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5.2 B0 位置帧逐字节解释&lt;a href=&quot;#52-b0-位置帧逐字节解释&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;假设位置是 120 mm，即 &lt;code&gt;0x0078&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;索引       0   1   2   3   4   5   6        7&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;字节      AA  AA  B0  02  00  78  CHECKSUM BB&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;/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;所以原 MCU 代码中：&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;frame&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cur_pos &lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;8&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;frame&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;=&lt;/span&gt;&lt;span&gt; cur_pos &lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;0x&lt;/span&gt;&lt;span&gt;FF&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;code&gt;4&lt;/code&gt; 和 &lt;code&gt;5&lt;/code&gt; 是&lt;strong&gt;数组索引&lt;/strong&gt;，不是“第 4 个 PID 字节”。
B0 的 payload 只有 2 字节，完整协议帧是 8 字节；如果再附加 &lt;code&gt;0A&lt;/code&gt;，串口
总共发送 9 字节。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5.3 B1 PID 帧的 12 字节在哪里&lt;a href=&quot;#53-b1-pid-帧的-12-字节在哪里&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;kp&lt;/code&gt;、&lt;code&gt;ki&lt;/code&gt;、&lt;code&gt;kd&lt;/code&gt; 都是 32 位 &lt;code&gt;float&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;3 个 float × 4 字节 = 12 字节 payload&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;B1 完整帧的索引如下：&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;索引       0   1   2   3   4..7   8..11  12..15  16       17&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;含义      AA  AA  B1  0C   kp      ki      kd    CHECKSUM  BB&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;长度       2       1   1     4       4       4       1       1&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;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;完整 B1 协议帧 = 2 + 1 + 1 + 12 + 1 + 1 = 18 字节&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;如果帧后还发 0A = 19 个串口字节&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;PID 数据本身仍然只有 12 字节。&lt;/strong&gt; 你看到索引最大到 &lt;code&gt;15&lt;/code&gt;，是因为前面
已经有 4 个字节的帧头、命令和长度，不是 PID 变成了 16 字节。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5.4 为什么 B0 是高字节在前，B1 float 却是低字节在前&lt;a href=&quot;#54-为什么-b0-是高字节在前b1-float-却是低字节在前&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这是原嵌入式协议的字段约定，不是 C/C++ 强制规定：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;B0/F0 的 16 位位置通过移位显式拆分，协议约定为大端。&lt;/li&gt;
&lt;li&gt;B1 是 MCU 内存中的 &lt;code&gt;float&lt;/code&gt; 按字节上传，GD32 是小端，所以为小端。&lt;/li&gt;
&lt;li&gt;E0 下发 PID 又按原接收协议使用大端 float。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;理想的新协议应该统一端序，但上位机首先要兼容已有 MCU 实现。所以代码
对每个字段显式使用 &lt;code&gt;qFromLittleEndian&lt;/code&gt; 或 &lt;code&gt;qToBigEndian&lt;/code&gt;，不直接用结构体
&lt;code&gt;reinterpret_cast&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5.5 本项目的命令表&lt;a href=&quot;#55-本项目的命令表&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;


































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;CMD&lt;/th&gt;&lt;th&gt;方向&lt;/th&gt;&lt;th&gt;LEN&lt;/th&gt;&lt;th&gt;payload&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;B0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;MCU → Linux&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;当前位置，16 位大端&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;B1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;MCU → Linux&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;kp/ki/kd，3 个 32 位小端 float&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;F0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Linux → MCU&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;目标位置，16 位大端&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;E0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Linux → MCU&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;1 字节选择器 + 4 字节大端 float&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;6. 建议的代码阅读顺序&lt;a href=&quot;#6-建议的代码阅读顺序&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;不要一开始就从 &lt;code&gt;main_window.cpp&lt;/code&gt; 第一行读到最后一行。界面代码很长，会掩盖
真正的主线。建议分七轮：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;读 &lt;code&gt;protocol_codec.h&lt;/code&gt;，只记住帧结构、命令字和公共接口。&lt;/li&gt;
&lt;li&gt;读 &lt;code&gt;protocol_codec_test.cpp&lt;/code&gt;，用具体字节理解协议预期行为。&lt;/li&gt;
&lt;li&gt;读 &lt;code&gt;protocol_codec.cpp&lt;/code&gt;，重点看 &lt;code&gt;StreamParser::feed()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;读 &lt;code&gt;serial_worker.h&lt;/code&gt; 中的信号、队列和原子变量，再读 &lt;code&gt;run()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;读 &lt;code&gt;serial_worker_test.cpp&lt;/code&gt;，看测试怎样在 PTY 两端扮演 Linux 和 MCU。&lt;/li&gt;
&lt;li&gt;读 &lt;code&gt;MainWindow::onWorkerBytes()&lt;/code&gt; 和两个 &lt;code&gt;send...()&lt;/code&gt;，先跳过 &lt;code&gt;setupUi()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;最后读 &lt;code&gt;setupUi()&lt;/code&gt;、&lt;code&gt;PositionPlot::paintEvent()&lt;/code&gt; 和模拟器。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;每读完一轮，都尝试回答：“这个模块接收什么，输出什么，它在哪个线程
运行？”如果这三点能答清楚，就不会在细节中迷路。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;7. &lt;code&gt;protocol_codec&lt;/code&gt; 逐函数讲解&lt;a href=&quot;#7-protocol_codec-逐函数讲解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;7.1 &lt;code&gt;byteAt()&lt;/code&gt;&lt;a href=&quot;#71-byteat&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;QByteArray::at()&lt;/code&gt; 返回 &lt;code&gt;char&lt;/code&gt;，而 &lt;code&gt;char&lt;/code&gt; 在某些编译器上是有符号的。&lt;code&gt;0xAA&lt;/code&gt;
可能被解释为负数。&lt;code&gt;byteAt()&lt;/code&gt; 统一转换为 &lt;code&gt;quint8&lt;/code&gt;，避免协议比较时出现
符号扩展。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.2 &lt;code&gt;floatFromLittleEndian()&lt;/code&gt;&lt;a href=&quot;#72-floatfromlittleendian&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;先把 4 字节按小端解码为 &lt;code&gt;quint32&lt;/code&gt; 位模式。&lt;/li&gt;
&lt;li&gt;再用 &lt;code&gt;std::memcpy()&lt;/code&gt; 把相同位模式复制到 &lt;code&gt;float&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;不用 &lt;code&gt;reinterpret_cast&amp;lt;float *&amp;gt;&lt;/code&gt; 的原因是它可能同时触发未对齐访问和 C++ 严格别名
问题。&lt;code&gt;memcpy&lt;/code&gt; 是这种位级转换的稳妥写法。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.3 &lt;code&gt;floatToBigEndian()&lt;/code&gt;&lt;a href=&quot;#73-floattobigendian&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;过程与上面相反：先拿到 float 的 32 位模式，再用 &lt;code&gt;qToBigEndian()&lt;/code&gt; 存入
4 字节数组。它用于 E0 PID 参数下发。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.4 &lt;code&gt;StreamParser::feed()&lt;/code&gt;&lt;a href=&quot;#74-streamparserfeed&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这是协议层最重要的函数。串口和 TCP 都是&lt;strong&gt;字节流&lt;/strong&gt;，一次 &lt;code&gt;read()&lt;/code&gt; 不会
保证恰好返回一帧。可能发生：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;半包：一帧被分成多次接收。&lt;/li&gt;
&lt;li&gt;粘包：一次接收多帧。&lt;/li&gt;
&lt;li&gt;粘包 + 半包：前面有完整帧，最后只有下一帧的一部分。&lt;/li&gt;
&lt;li&gt;噪声：帧头之前有 &lt;code&gt;0A&lt;/code&gt;、无效字节或损坏帧。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;code&gt;feed()&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;/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;查找 AA AA&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;不足 4 字节？ 保留，等下次&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;↓&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;读 LEN，如果 &amp;gt; 64 则拒绝并重新寻找帧头&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;/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;/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;检查 CHECKSUM 和 BB&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;生成 Frame，从缓冲区删除该帧，继续解析后续数据&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;AA AA&lt;/code&gt;，但缓冲区最后一字节是 &lt;code&gt;AA&lt;/code&gt;，它会保留这一
字节。因为它可能是下一次数据中帧头的第一个 &lt;code&gt;AA&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;LEN 上限 64 是内存和异常流量保护。如果上位机收到 &lt;code&gt;LEN=100&lt;/code&gt;，解析器
不会等待 100 字节，而是计一次拒绝、移除一个字节，然后继续搜索下一个
&lt;code&gt;AA AA&lt;/code&gt;。这避免故意或损坏的长度字节让缓冲区无限增长。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.5 &lt;code&gt;checksum8()&lt;/code&gt;&lt;a href=&quot;#75-checksum8&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;将 &lt;code&gt;command&lt;/code&gt;、&lt;code&gt;payload.size()&lt;/code&gt; 和所有 payload 字节累加，只保留低 8 位。
它能发现常见传输错误，但不是密码学完整性保护。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.6 &lt;code&gt;encodeFrame()&lt;/code&gt;&lt;a href=&quot;#76-encodeframe&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;按帧头、命令、长度、payload、校验、帧尾的顺序构造通用帧。Linux 下发
不额外加 &lt;code&gt;0A&lt;/code&gt;，因为帧长已经由 LEN 唯一确定。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.7 &lt;code&gt;encodeTargetPosition()&lt;/code&gt;&lt;a href=&quot;#77-encodetargetposition&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把 16 位目标位置拆为高字节和低字节，再构造 F0 帧。测试中
&lt;code&gt;0x1234&lt;/code&gt; 的完整帧必须是 &lt;code&gt;AA AA F0 02 12 34 38 BB&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.8 &lt;code&gt;encodePidValue()&lt;/code&gt;&lt;a href=&quot;#78-encodepidvalue&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;一个 E0 帧只下发一个 PID 参数。payload 是“选择器 + 4 字节大端 float”，
选择器 0/1/2 分别表示 Kp/Ki/Kd。所以界面点击“下发 PID”后会连续入队
3 帧，而不是一个包发三个 float。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.9 &lt;code&gt;decodePosition()&lt;/code&gt; 和 &lt;code&gt;decodePidValues()&lt;/code&gt;&lt;a href=&quot;#79-decodeposition-和-decodepidvalues&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;它们首先检查 CMD 和 payload 长度。只有匹配时才返回 &lt;code&gt;std::optional&lt;/code&gt; 中的数值，
否则返回 &lt;code&gt;std::nullopt&lt;/code&gt;。这让调用者必须显式处理“这不是我要的帧”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.10 &lt;code&gt;toHexText()&lt;/code&gt;&lt;a href=&quot;#710-tohextext&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把字节显示为 &lt;code&gt;AA AA F0 ...&lt;/code&gt; 格式，只用于通信日志和测试诊断，不参与
协议传输。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;8. &lt;code&gt;SerialWorker&lt;/code&gt; 逐函数讲解&lt;a href=&quot;#8-serialworker-逐函数讲解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;8.1 三个被 epoll 监听的 fd&lt;a href=&quot;#81-三个被-epoll-监听的-fd&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;fd&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;&lt;code&gt;serialFd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;EPOLLIN/EPOLLOUT/ERR/HUP&lt;/td&gt;&lt;td&gt;收发串口数据和检测断线&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;localWakeFd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;EPOLLIN&lt;/td&gt;&lt;td&gt;主线程通知“有新发送帧”或“请求退出”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;timerFd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;EPOLLIN&lt;/td&gt;&lt;td&gt;每秒重连和更新统计&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt; 并不只是为了“高并发服务器”。对这个单设备项目，它的价值是把串口、
跨线程唤醒和定时器统一到一个不忙等待的事件循环。不需要为了简历故意宣称
“高并发”，应该准确表述为“基于 epoll 的事件驱动非阻塞 I/O”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.2 &lt;code&gt;baudFlag()&lt;/code&gt;&lt;a href=&quot;#82-baudflag&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把界面中的整数波特率映射为 termios 需要的 &lt;code&gt;B115200&lt;/code&gt; 等常量。不支持的值
返回 0，后续配置会以 &lt;code&gt;EINVAL&lt;/code&gt; 失败。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.3 &lt;code&gt;addEpollFd()&lt;/code&gt;&lt;a href=&quot;#83-addepollfd&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;封装 &lt;code&gt;epoll_ctl(EPOLL_CTL_ADD)&lt;/code&gt;，将 fd 和关心的事件注册到 epoll 实例。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.4 &lt;code&gt;enqueueFrame()&lt;/code&gt;&lt;a href=&quot;#84-enqueueframe&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;空帧直接忽略。&lt;/li&gt;
&lt;li&gt;加锁 &lt;code&gt;txMutex_&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;把整个 &lt;code&gt;QByteArray&lt;/code&gt; 放入 &lt;code&gt;txQueue_&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;解锁后调用 &lt;code&gt;wakeLoop()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;“整帧入队”和“只由一个工作线程写 fd”两个条件，保证多个界面操作
同时发送时不会把两帧的字节交叉。&lt;code&gt;volatile&lt;/code&gt; 不能实现这个保证，因为它
不提供互斥、原子性或线程间 happens-before 关系。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.5 &lt;code&gt;requestStop()&lt;/code&gt;&lt;a href=&quot;#85-requeststop&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;以 release 语义将 &lt;code&gt;stopRequested_&lt;/code&gt; 设为 true，然后写 eventfd。如果只改标志而
不唤醒，工作线程可能永远阻塞在 &lt;code&gt;epoll_wait(-1)&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.6 &lt;code&gt;run()&lt;/code&gt;&lt;a href=&quot;#86-run&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;run()&lt;/code&gt; 是整个 I/O 线程的生命周期：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;创建 epoll、eventfd 和 timerfd。&lt;/li&gt;
&lt;li&gt;设置 timerfd 首次 1 秒后触发，之后每秒触发。&lt;/li&gt;
&lt;li&gt;尝试打开串口。&lt;/li&gt;
&lt;li&gt;进入 &lt;code&gt;epoll_wait()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;按 fd 类型分发事件。&lt;/li&gt;
&lt;li&gt;检查到退出标志后注销串口，关闭所有 fd，发出 &lt;code&gt;finished()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;串口 EPOLLIN 到来后会循环 &lt;code&gt;read()&lt;/code&gt;，直到返回 &lt;code&gt;EAGAIN&lt;/code&gt;。这是边缘式思路中
常见的“一次把已到达数据读干净”，也可减少 epoll 唤醒次数。当前注册使用
默认水平触发，即使未一次读完也不会丢数据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.7 &lt;code&gt;openSerial()&lt;/code&gt;&lt;a href=&quot;#87-openserial&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用以下标志打开设备：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;O_RDWR&lt;/code&gt;：同时读写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O_NOCTTY&lt;/code&gt;：不让该串口成为进程的控制终端。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O_NONBLOCK&lt;/code&gt;：读写不阻塞工作线程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O_CLOEXEC&lt;/code&gt;：以后如果启动子进程，子进程不继承该 fd。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;打开失败不会让线程退出。它会报告“等待设备”，再由 timerfd 每秒重试。
这对 USB 转串口拔插很实用。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.8 &lt;code&gt;configureSerial()&lt;/code&gt;&lt;a href=&quot;#88-configureserial&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;cfmakeraw()&lt;/code&gt; 关闭终端的行缓冲、回显和特殊字符处理，让串口成为原始二进制
通道。之后配置 8 数据位、1 停止位、无校验、无硬件流控，即 8N1。&lt;/p&gt;&lt;p&gt;&lt;code&gt;VMIN=0&lt;/code&gt; 和 &lt;code&gt;VTIME=0&lt;/code&gt; 配合 &lt;code&gt;O_NONBLOCK&lt;/code&gt;，使没有数据时 &lt;code&gt;read()&lt;/code&gt; 立即返回；等待
由 epoll 负责，不由 termios 超时负责。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.9 &lt;code&gt;collectOutgoingFrames()&lt;/code&gt;&lt;a href=&quot;#89-collectoutgoingframes&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在互斥锁保护下把队列中的整帧顺序追加到 &lt;code&gt;pending&lt;/code&gt;。之后所有实际
&lt;code&gt;write()&lt;/code&gt; 都由工作线程完成。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.10 &lt;code&gt;flushPending()&lt;/code&gt;&lt;a href=&quot;#810-flushpending&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Linux &lt;code&gt;write()&lt;/code&gt; 可能只写出部分字节，所以使用 &lt;code&gt;offset&lt;/code&gt; 记住已写位置。
如果遇到 &lt;code&gt;EAGAIN&lt;/code&gt;，它会给串口加上 EPOLLOUT 关注；内核通知可写时再继续。
全部写完后清空 &lt;code&gt;pending&lt;/code&gt;、重置偏移，并取消 EPOLLOUT，避免可写事件造成
空转。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.11 &lt;code&gt;consumeWakeEvent()&lt;/code&gt; 和 &lt;code&gt;wakeLoop()&lt;/code&gt;&lt;a href=&quot;#811-consumewakeevent-和-wakeloop&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;eventfd&lt;/code&gt; 内部是一个 64 位计数器。&lt;code&gt;wakeLoop()&lt;/code&gt; 写入 1，&lt;code&gt;consumeWakeEvent()&lt;/code&gt;
把已累积的唤醒值读空。多次快速入队可以合并成一次 epoll 处理，不会需要
一帧对应一个线程事件。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;9. &lt;code&gt;MainWindow&lt;/code&gt; 逐函数讲解&lt;a href=&quot;#9-mainwindow-逐函数讲解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;9.1 &lt;code&gt;setupStyle()&lt;/code&gt;&lt;a href=&quot;#91-setupstyle&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;设置全局 Qt Style Sheet。界面使用浅灰工作区、白色图表、青绿色实时信号、
黄色目标线和红色故障状态。风格的目标是高频使用的仪器控制台，不是宣传页。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.2 &lt;code&gt;setupUi()&lt;/code&gt;&lt;a href=&quot;#92-setupui&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;界面主体是一个水平 &lt;code&gt;QSplitter&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;/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;│ 左侧：全尺寸位置曲线       │ 右侧：数值/目标/PID/统计 │&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;/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;setupUi()&lt;/code&gt; 最后才连接按钮信号和槽。数值控件的变化不会自动下发，必须点击
明确的“下发”按钮，避免用户输入过程中反复改变 MCU 参数。
通信日志区已删除，左侧空间全部用于位置轨迹。标题只保留“平衡球控制台”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.3 &lt;code&gt;refreshPorts()&lt;/code&gt;&lt;a href=&quot;#93-refreshports&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;扫描 &lt;code&gt;/dev/ttyUSB*&lt;/code&gt; 和 &lt;code&gt;/dev/ttyACM*&lt;/code&gt;。串口下拉框可编辑，因此 PTY 的
&lt;code&gt;/dev/pts/N&lt;/code&gt; 可直接输入，不需要把短暂存在的 PTY 枚举到普通硬件串口列表。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.4 &lt;code&gt;connectDevice()&lt;/code&gt; 和 &lt;code&gt;startDemoMode()&lt;/code&gt;&lt;a href=&quot;#94-connectdevice-和-startdemomode&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这两个是命令行自动化入口。&lt;code&gt;--device&lt;/code&gt; 会填写串口和波特率后连接；
&lt;code&gt;--demo&lt;/code&gt; 会立即启动内置数学模型。如果已经有连接，会先停止旧连接。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.5 &lt;code&gt;startSerial()&lt;/code&gt;&lt;a href=&quot;#95-startserial&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;从界面取设备路径和波特率。&lt;/li&gt;
&lt;li&gt;创建 &lt;code&gt;QThread&lt;/code&gt; 和 &lt;code&gt;SerialWorker&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;把 worker 移到工作线程。&lt;/li&gt;
&lt;li&gt;连接 started/run、数据、状态、错误、统计和 finished 信号。&lt;/li&gt;
&lt;li&gt;清空协议解析器的历史半包。&lt;/li&gt;
&lt;li&gt;启动线程。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.6 &lt;code&gt;stopSerial()&lt;/code&gt;&lt;a href=&quot;#96-stopserial&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;先调用 &lt;code&gt;requestStop()&lt;/code&gt; 写 eventfd，再请求 QThread 退出并等待最多 3 秒。线程停止后
才销毁 worker 和 thread，避免 fd 仍在使用时对象已经被删除。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.7 &lt;code&gt;onWorkerBytes()&lt;/code&gt;&lt;a href=&quot;#97-onworkerbytes&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这是“串口字节到界面数值”的桥梁：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;累加接收字节数。&lt;/li&gt;
&lt;li&gt;如果开启原始帧日志，显示十六进制字节。&lt;/li&gt;
&lt;li&gt;把数据交给 &lt;code&gt;parser_.feed()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对 B0 调用 &lt;code&gt;updateTelemetry()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对 B1 更新 PID 实时显示；用户正在编辑时不覆盖编辑框。&lt;/li&gt;
&lt;li&gt;更新接受帧、拒绝帧和半包缓冲字节数。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;解析在主线程是可接受的，因为单帧最长受限且运算量很小。如果以后变成
多设备、高帧率或复杂解码，可再把解析移到 worker，对界面只发已解码的结构体。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.8 &lt;code&gt;sendTargetPosition()&lt;/code&gt;&lt;a href=&quot;#98-sendtargetposition&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;从 spin box 取目标，构造 F0 帧并整帧入队。只有这个函数成功执行后，
输入框数值才成为 &lt;code&gt;targetPosition_&lt;/code&gt;，并进入曲线和记录。因此只修改输入框而
没有点击“下发目标”时，CSV 和 MySQL 仍记录上一个有效目标。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.9 &lt;code&gt;sendPidParameters()&lt;/code&gt;&lt;a href=&quot;#99-sendpidparameters&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;读取三个 spin box，对 Kp/Ki/Kd 分别构造一个 E0 帧。三帧顺序进入同一
发送队列，并由同一 I/O 线程写出。原先 PID 难以修改的原因是 B1 每 100 ms
将 MCU 旧值重新写回 spin box。现在使用 &lt;code&gt;pidEditorDirty_&lt;/code&gt; 区分“用户待下发值”和
“设备实时回报值”，只在用户没有编辑，或 B1 已与下发值一致时同步编辑器。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.10 &lt;code&gt;toggleRecording()&lt;/code&gt; 和 &lt;code&gt;appendRecordLine()&lt;/code&gt;&lt;a href=&quot;#910-togglerecording-和-appendrecordline&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;开始记录时打开 CSV 文件并写表头：&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;timestamp,&lt;/span&gt;&lt;span&gt;current_mm,&lt;/span&gt;&lt;span&gt;target_mm,&lt;/span&gt;&lt;span&gt;kp,&lt;/span&gt;&lt;span&gt;ki,&lt;/span&gt;&lt;span&gt;kd&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;ensureDatabaseConnection()&lt;/code&gt;。MySQL 可用时，每次 B0 更新会
向 CSV 和 &lt;code&gt;position_records&lt;/code&gt; 各写一行；MySQL 不可用时降级为“仅 CSV”，不中断监控。
演示模式中每 25 个 20 ms 周期刷新一次文件缓冲，
在性能和异常退出时的数据丢失量之间折中。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.11 &lt;code&gt;ensureDatabaseConnection()&lt;/code&gt;&lt;a href=&quot;#911-ensuredatabaseconnection&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用 QMYSQL 连接虚拟机 &lt;code&gt;192.168.28.128:3306&lt;/code&gt;，用户名 &lt;code&gt;bob&lt;/code&gt;、密码按当前
学习环境配置为 &lt;code&gt;已隐藏&lt;/code&gt;。
连接成功后自动创建 &lt;code&gt;balance_ball&lt;/code&gt; 数据库和 &lt;code&gt;position_records&lt;/code&gt; 表。函数具有懒连接
语义：启动上位机时不强制连 MySQL，只在开始记录或点击查询时连接。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.12 &lt;code&gt;insertDatabaseRecord()&lt;/code&gt;&lt;a href=&quot;#912-insertdatabaserecord&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用预处理 SQL 和命名绑定参数插入时间、当前位置、有效目标和 PID。不使用
字符串拼接数值，避免格式和 SQL 转义问题。写入失败后将数据库标记为不可用，
后续仍继续 CSV 记录。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.13 &lt;code&gt;showDatabaseRecords()&lt;/code&gt;&lt;a href=&quot;#913-showdatabaserecords&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;创建历史记录对话框，用 &lt;code&gt;QSqlTableModel&lt;/code&gt; 直接绑定 &lt;code&gt;position_records&lt;/code&gt;。表格按 ID
倒序显示、禁止界面编辑，并提供刷新按钮重新执行 &lt;code&gt;select()&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.14 &lt;code&gt;startDemo()&lt;/code&gt; 和 &lt;code&gt;updateDemoData()&lt;/code&gt;&lt;a href=&quot;#914-startdemo-和-updatedemodata&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;内置演示模式每 20 ms 用 QTimer 更新一次简化二阶模型，可以快速检查界面和
CSV/MySQL，但它不经过串口。需要验证 Linux I/O 时，应该使用 PTY 模拟器。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.15 &lt;code&gt;updateTelemetry()&lt;/code&gt;&lt;a href=&quot;#915-updatetelemetry&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;统一更新当前位置、有效目标、曲线和 CSV/MySQL。将所有入口汇总到一个函数，
可以避免演示数据和串口数据对界面的更新行为不一致。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;10. &lt;code&gt;PositionPlot&lt;/code&gt; 怎样画实时曲线&lt;a href=&quot;#10-positionplot-怎样画实时曲线&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;10.1 为什么没有使用 Qt Charts&lt;a href=&quot;#101-为什么没有使用-qt-charts&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这个项目只需要一条位置曲线和一条目标线。用 QPainter 自绘可以减少依赖，
也能精确控制网格、游标、颜色和时间窗。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.2 &lt;code&gt;appendSample()&lt;/code&gt;&lt;a href=&quot;#102-appendsample&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用 &lt;code&gt;QElapsedTimer&lt;/code&gt; 记录单调时间，不受系统时钟调整影响。每个样本保存为
&lt;code&gt;QPointF(秒, 毫米)&lt;/code&gt;。只保留可见 12 秒再多 1 秒的数据，防止长时间运行后
内存无限增长。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.3 &lt;code&gt;paintEvent()&lt;/code&gt;&lt;a href=&quot;#103-paintevent&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每次重绘执行：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;清空背景。&lt;/li&gt;
&lt;li&gt;预留坐标轴文字边距。&lt;/li&gt;
&lt;li&gt;绘制 0~300 mm 水平网格。&lt;/li&gt;
&lt;li&gt;绘制最近 12 秒时间网格。&lt;/li&gt;
&lt;li&gt;绘制黄色目标虚线。&lt;/li&gt;
&lt;li&gt;把样本映射到图表坐标，用 &lt;code&gt;QPainterPath&lt;/code&gt; 连成青绿曲线。&lt;/li&gt;
&lt;li&gt;在最新位置绘制游标和圆点。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;code&gt;mapPoint()&lt;/code&gt; 完成“时间/毫米坐标”到“像素坐标”的线性映射。因为屏幕 y 轴
向下增大，而数学图表 y 轴向上增大，所以 y 计算从 &lt;code&gt;plotArea.bottom()&lt;/code&gt; 往上减。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;11. PTY 设备模拟器的实现&lt;a href=&quot;#11-pty-设备模拟器的实现&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;11.1 PTY 是什么&lt;a href=&quot;#111-pty-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;PTY（pseudo terminal）是 Linux 内核提供的主从设备对。模拟器持有主端 fd，
Qt 上位机像普通串口一样打开 &lt;code&gt;/dev/pts/N&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;模拟器 write(master)  →  上位机 read(/dev/pts/N)&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;模拟器 read(master)   ←  上位机 write(/dev/pts/N)&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;因为上位机打开的真是一个 Linux 字符设备，所以 termios、非阻塞 read/write 和 epoll
全部会真实执行。这比把随机数直接塞给界面更接近真实硬件联调。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.2 &lt;code&gt;DeviceState&lt;/code&gt;&lt;a href=&quot;#112-devicestate&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;保存位置、速度、目标、Kp/Ki/Kd 和模拟时间。它是模拟设备的全部可变状态。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.3 &lt;code&gt;encodeFrame()&lt;/code&gt;、&lt;code&gt;sendPosition()&lt;/code&gt; 和 &lt;code&gt;sendPid()&lt;/code&gt;&lt;a href=&quot;#113-encodeframesendposition-和-sendpid&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;模拟器故意不链接 Qt 协议库，而是用标准 C++ 独立实现编码。这避免测试两端
共享同一个错误实现后仍然看起来“互相匹配”。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;B0 每 20 ms 发送一次。&lt;/li&gt;
&lt;li&gt;B1 每 100 ms 发送一次。&lt;/li&gt;
&lt;li&gt;上报帧后附加换行，模拟 MCU 当前行为。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.4 &lt;code&gt;updatePhysics()&lt;/code&gt;&lt;a href=&quot;#114-updatephysics&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;使用一个带阻尼和正弦扰动的简化二阶模型。它不是精确的球板动力学模型，
但可以产生连续、有惯性、能够趋近目标的位置数据，适合验证曲线和命令下发。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.5 &lt;code&gt;parseIncoming()&lt;/code&gt; 和 &lt;code&gt;executeFrame()&lt;/code&gt;&lt;a href=&quot;#115-parseincoming-和-executeframe&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;模拟器也会处理半包、粘包和错误帧。收到 F0 后改变目标；收到 E0 后按
选择器改变对应 PID 值，并在终端打印更新结果。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;12. 构建、测试和运行&lt;a href=&quot;#12-构建测试和运行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;12.1 登录虚拟机&lt;a href=&quot;#121-登录虚拟机&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;ssh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;192.168.28.128&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;h3&gt;12.2 首次安装用户目录 Qt&lt;a href=&quot;#122-首次安装用户目录-qt&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/home/bob/balance-ball-linux-host/linux-host&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;bash&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tools/install_user_qt6.sh&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;source&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/home/bob/.local/balance-qt6/env.sh&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;sudo dpkg -i&lt;/code&gt;，而是使用 &lt;code&gt;apt-get download&lt;/code&gt; 下载 deb，再用 &lt;code&gt;dpkg-deb -x&lt;/code&gt;
解包到 &lt;code&gt;~/.local/balance-qt6&lt;/code&gt;。它会强制下载 Core/Gui/Sql/Widgets 和
&lt;code&gt;libqt6sql6-mysql&lt;/code&gt; 驱动，即使这些包
在系统 dpkg 中已显示安装。原因是用户目录中的 Qt CMake 配置会按自己的
前缀寻找运行库。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.3 手动构建和测试&lt;a href=&quot;#123-手动构建和测试&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;source&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/home/bob/.local/balance-qt6/env.sh&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;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/home/bob/balance-ball-linux-host/linux-host&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;cmake&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-S&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-B&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-DCMAKE_BUILD_TYPE=Debug&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;cmake&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-j2&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;ctest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--test-dir&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--output-on-failure&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;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;protocol_codec_test  Passed&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;sql_driver_test      Passed&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;database_integration_test Passed&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;main_window_test     Passed&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;serial_worker_test   Passed&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;100% tests passed, 0 tests failed&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;h3&gt;12.4 配置虚拟机本机 MySQL&lt;a href=&quot;#124-配置虚拟机本机-mysql&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;host:     192.168.28.128&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;port:     3306&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;user:     bob&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;password: 已隐藏&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;database: balance_ball&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;table:    position_records&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;这里的主机地址是 Ubuntu 虚拟机的网卡地址。已实际验证 TCP 登录：&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;mysql&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-h192.168.28.128&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-ubob&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&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;SELECT 1;&quot;&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;服务器返回 MySQL 8.0.46 和 &lt;code&gt;bob@%&lt;/code&gt;，该账号具有 &lt;code&gt;CREATE&lt;/code&gt;、&lt;code&gt;INSERT&lt;/code&gt;、&lt;code&gt;SELECT&lt;/code&gt;
等所需权限。程序会自动建库建表，无需手工创建 &lt;code&gt;balance_ball&lt;/code&gt;。
生产环境不应在源码中保存密码，本项目按当前本地学习环境的明确要求配置。&lt;/p&gt;&lt;p&gt;&lt;code&gt;position_records&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;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;&lt;td&gt;BIGINT AUTO_INCREMENT&lt;/td&gt;&lt;td&gt;主键&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;recorded_at&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DATETIME(3)&lt;/td&gt;&lt;td&gt;毫秒级记录时间&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;current_mm&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DOUBLE&lt;/td&gt;&lt;td&gt;当前位置&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;target_mm&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DOUBLE&lt;/td&gt;&lt;td&gt;已下发的有效目标&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;kp&lt;/code&gt; / &lt;code&gt;ki&lt;/code&gt; / &lt;code&gt;kd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DOUBLE&lt;/td&gt;&lt;td&gt;设备当前回报 PID&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.5 无串口的内置演示模式&lt;a href=&quot;#125-无串口的内置演示模式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;./tools/run_console.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--demo&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;这适合检查界面、曲线和 CSV/MySQL，但不检查 termios/epoll。
也可以使用命令行直接开始记录：&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;./tools/run_console.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--demo&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;/tmp/balance-demo.csv&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;--record&lt;/code&gt; 调用的是和界面“开始记录”按钮相同的函数，因此可以用于无人值守
验收，不是另外伪造的数据库通道。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.6 使用 PTY 模拟真实串口&lt;a href=&quot;#126-使用-pty-模拟真实串口&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;终端 1：&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;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/home/bob/balance-ball-linux-host/linux-host&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;./tools/run_simulator.sh&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;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;Qt 上位机串口请选择：/dev/pts/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;终端 2：&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;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/home/bob/balance-ball-linux-host/linux-host&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;./tools/run_console.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--device&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/dev/pts/0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--baud&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;115200&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;/dev/pts/0&lt;/code&gt; 每次运行都可能变化，必须使用模拟器当次打印的路径。&lt;/p&gt;&lt;p&gt;在界面中改变目标和 PID 并点击下发后，终端 1 会打印：&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;目标位置更新为 180 mm&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;Kp 更新为 0.2&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;Ki 更新为 0.01&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;Kd 更新为 0.6&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;h3&gt;12.7 以后连接真实板卡&lt;a href=&quot;#127-以后连接真实板卡&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;把 USB 转串口插入虚拟机，确保不是只映射给 Windows 主机。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;dmesg&lt;/code&gt; 或 &lt;code&gt;ls /dev/ttyUSB* /dev/ttyACM*&lt;/code&gt; 确认设备名。&lt;/li&gt;
&lt;li&gt;确保用户有权限。常见做法是将用户加入 &lt;code&gt;dialout&lt;/code&gt; 组后重新登录。&lt;/li&gt;
&lt;li&gt;运行：&lt;/li&gt;
&lt;/ol&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;./tools/run_console.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--device&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/dev/ttyUSB0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--baud&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;115200&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;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;13. CMake 构建目标&lt;a href=&quot;#13-cmake-构建目标&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;&lt;code&gt;balance_protocol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;静态库&lt;/td&gt;&lt;td&gt;协议编解码，供程序和测试共用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;balance_console&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Qt 可执行程序&lt;/td&gt;&lt;td&gt;主上位机&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;protocol_codec_test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Qt Test&lt;/td&gt;&lt;td&gt;协议单元测试&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sql_driver_test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Qt Test&lt;/td&gt;&lt;td&gt;验证 QMYSQL 运行时驱动可加载&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;database_integration_test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Qt Test + MySQL&lt;/td&gt;&lt;td&gt;真实建表、写入、查询与清理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;main_window_test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Qt Test&lt;/td&gt;&lt;td&gt;有效目标和 PID 编辑/下发语义&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;serial_worker_test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Qt Test + PTY&lt;/td&gt;&lt;td&gt;Linux I/O 集成测试，只在 Linux 构建&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;balance_device_simulator&lt;/code&gt;&lt;/td&gt;&lt;td&gt;标准 C++ 程序&lt;/td&gt;&lt;td&gt;独立 PTY 设备模拟器&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;code&gt;CMAKE_AUTOMOC&lt;/code&gt; 会对带 &lt;code&gt;Q_OBJECT&lt;/code&gt; 的类自动运行 Qt moc，生成信号槽需要的元对象代码。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;14. 测试在验证什么&lt;a href=&quot;#14-测试在验证什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;14.1 &lt;code&gt;protocol_codec_test&lt;/code&gt;&lt;a href=&quot;#141-protocol_codec_test&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&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;&lt;code&gt;encodesTargetPosition&lt;/code&gt;&lt;/td&gt;&lt;td&gt;F0 大端位置和校验和&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;parsesFragmentedPositionFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;一帧分三次到达仍只输出一帧&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;parsesStickyFramesAndPidValues&lt;/code&gt;&lt;/td&gt;&lt;td&gt;B0+B1 粘包、换行和小端 float&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;rejectsCorruptFrameAndResynchronizes&lt;/code&gt;&lt;/td&gt;&lt;td&gt;损坏帧后能找回下一帧&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;boundsInvalidPayloadLength&lt;/code&gt;&lt;/td&gt;&lt;td&gt;LEN=255 不会让缓冲无限等待&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;14.2 &lt;code&gt;serial_worker_test&lt;/code&gt;&lt;a href=&quot;#142-serial_worker_test&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;测试创建真实 PTY 主从对：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SerialWorker&lt;/code&gt; 在 QThread 中打开 PTY 从端。&lt;/li&gt;
&lt;li&gt;测试从主端写 B0，检查 &lt;code&gt;bytesReceived&lt;/code&gt; 收到数据并解出 120 mm。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;enqueueFrame()&lt;/code&gt; 发 F0，从 PTY 主端读回并逐字节比较。&lt;/li&gt;
&lt;li&gt;连续入队 Kp/Ki/Kd 三个 E0 帧，验证字节顺序不交叉。&lt;/li&gt;
&lt;li&gt;请求线程退出，验证 eventfd 能唤醒 epoll 且线程在 2 秒内停止。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;这个测试覆盖了“能编译”无法覆盖的真实 Linux fd 行为。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;14.3 &lt;code&gt;main_window_test&lt;/code&gt;&lt;a href=&quot;#143-main_window_test&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;将目标输入从 160 改为 210，不点击下发时有效目标仍是 160。&lt;/li&gt;
&lt;li&gt;点击“下发目标”后，有效目标才变为 210。&lt;/li&gt;
&lt;li&gt;编辑 Kp/Ki/Kd 后数值保持可写，点击下发后演示控制器采用新参数。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;14.4 &lt;code&gt;database_integration_test&lt;/code&gt;&lt;a href=&quot;#144-database_integration_test&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;使用工程配置的地址和账号真实连接 MySQL。&lt;/li&gt;
&lt;li&gt;自动创建 &lt;code&gt;balance_ball&lt;/code&gt; 和 &lt;code&gt;position_records&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;插入一条 120/160 mm 的测试数据并用 SQL 读回逐字段比较。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;QSqlTableModel&lt;/code&gt; 过滤并读取该行，覆盖查询对话框的核心路径。&lt;/li&gt;
&lt;li&gt;验证完成后删除自己的测试行，不影响正式记录。&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;15. 常见问题和面试表达&lt;a href=&quot;#15-常见问题和面试表达&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;15.1 既然只有一个平衡球，为什么还用 epoll&lt;a href=&quot;#151-既然只有一个平衡球为什么还用-epoll&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;不要回答“因为高并发”。这个项目当前不是高并发。准确回答是：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;epoll 用于将串口可读/可写、跨线程 eventfd 唤醒和 timerfd 定时重连统一到
一个事件循环，避免忙轮询。如果以后接入 TCP 或多设备，该模型也可扩展，
但当前简历不应夸大为高并发系统。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;15.2 为什么不在 Qt 主线程里直接 read&lt;a href=&quot;#152-为什么不在-qt-主线程里直接-read&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;即使 fd 是非阻塞的，持续读写、断线处理和重连仍会占用 GUI 事件循环。
独立 I/O 线程可以让绘制和用户操作保持响应，再用排队信号穿过线程边界。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;15.3 为什么协议解析不依赖“一次 read 一帧”&lt;a href=&quot;#153-为什么协议解析不依赖一次-read-一帧&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;因为串口驱动只保证字节顺序，不保证应用层帧边界。正确解析器必须保留
半包、循环处理粘包，并在错误后重新同步帧头。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;15.4 多个任务同时发送时怎样防止帧交叉&lt;a href=&quot;#154-多个任务同时发送时怎样防止帧交叉&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;上层以完整 &lt;code&gt;QByteArray&lt;/code&gt; 入队，队列受互斥锁保护，只有 &lt;code&gt;SerialWorker&lt;/code&gt;
一个线程执行底层 &lt;code&gt;write()&lt;/code&gt;。部分写使用 offset 继续，完成当前 pending
数据后才清空。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;15.5 &lt;code&gt;volatile&lt;/code&gt; 能不能替代互斥锁&lt;a href=&quot;#155-volatile-能不能替代互斥锁&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;不能。&lt;code&gt;volatile&lt;/code&gt; 主要告诉编译器每次进行实际内存访问，常用于寄存器或
中断可见变量。它不保证复合操作原子性，不保护容器内部结构，也不建立 C++
线程间内存顺序。本项目的停止标志用 &lt;code&gt;std::atomic_bool&lt;/code&gt;，队列用 &lt;code&gt;QMutex&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;15.6 为什么 PID 周期改变后要重新整定&lt;a href=&quot;#156-为什么-pid-周期改变后要重新整定&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;离散 PID 中积分项累加频率和微分项的时间尺度都取决于采样周期 &lt;code&gt;dt&lt;/code&gt;。
如果代码的积分和微分公式没有显式带 &lt;code&gt;dt&lt;/code&gt;，把 50 ms 改成 10 ms 后，每秒积分
累加次数变成 5 倍，而相邻误差变化量也对应变小。即使公式正确引入 &lt;code&gt;dt&lt;/code&gt;，
采样、滤波、舍机和被控对象的离散特性也已变化，仍需复核稳定性和响应。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;15.7 本项目和 FreeRTOS 上的队列怎样对应&lt;a href=&quot;#157-本项目和-freertos-上的队列怎样对应&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;FreeRTOS Queue 适合在 MCU 任务或 ISR 之间传递固定大小消息，具有阻塞和唤醒语义。
Linux Qt 上位机中使用 &lt;code&gt;QQueue&amp;lt;QByteArray&amp;gt; + QMutex + eventfd&lt;/code&gt;，是因为帧长可变，
而工作线程已经以 epoll 为唤醒中心。两者解决的都是“消息传递 + 同步”，
但应根据平台和事件模型选择，不需强行使用同一实现。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;16. 如何把它写进嵌入式 Linux 简历&lt;a href=&quot;#16-如何把它写进嵌入式-linux-简历&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;项目名称可以写成：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;基于 FreeRTOS 与 Linux Qt 的平衡球控制与监控系统&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;推荐描述：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;在 GD32F407 + FreeRTOS 平衡球控制项目上开发 Linux Qt6 上位机，完成
位置/PID 实时监控、目标和参数下发、曲线展示与 CSV/MySQL 数据记录，
使用 QSqlTableModel 实现历史数据查询。基于
termios 配置 115200 8N1 二进制串口，使用 epoll + eventfd + timerfd
实现非阻塞收发、跨线程唤醒、统计和断线重连；设计支持半包、粘包、
错帧重同步和长度上限保护的流式协议解析器。搭建 PTY 伪串口设备模拟器和
Qt Test 自动化测试，在无开发板环境下完成协议与 Linux I/O 双向联调。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;面试时应主动说明：当前是单设备，epoll 是事件驱动 I/O 设计，不是为了虚构
高并发指标。这种边界意识比简单堆叠“高并发”关键词更有说服力。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;17. 后续升级路线&lt;a href=&quot;#17-后续升级路线&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;阶段 1：真实板卡回归&lt;a href=&quot;#阶段-1真实板卡回归&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;确认 B0/B1 的实际帧长、换行和 float 端序。&lt;/li&gt;
&lt;li&gt;对比 Qt 与原上位机显示值。&lt;/li&gt;
&lt;li&gt;测试 USB 串口拔插和自动重连。&lt;/li&gt;
&lt;li&gt;确认 MCU 对 F0/E0 的校验和范围保护。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;阶段 2：协议健壮性&lt;a href=&quot;#阶段-2协议健壮性&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;增加协议版本、序号和应答帧。&lt;/li&gt;
&lt;li&gt;下发命令在超时时重试，但必须设计幂等语义。&lt;/li&gt;
&lt;li&gt;将 8 位累加和升级为 CRC16。&lt;/li&gt;
&lt;li&gt;统一所有多字节整数和 float 端序。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;阶段 3：数据与可观测性&lt;a href=&quot;#阶段-3数据与可观测性&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;记录帧序号、延迟、丢帧和重连次数。&lt;/li&gt;
&lt;li&gt;增加记录文件分卷和元数据。&lt;/li&gt;
&lt;li&gt;对 CSV 增加离线回放功能，用同一曲线控件复现运行过程。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;阶段 4：网络化（真有需求时再做）&lt;a href=&quot;#阶段-4网络化真有需求时再做&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;在嵌入式 Linux 板上将串口转换为 TCP/WebSocket 服务。&lt;/li&gt;
&lt;li&gt;Qt 客户端通过网络连接远端设备。&lt;/li&gt;
&lt;li&gt;将运输层接口抽象为 Serial/TCP 两种实现，复用同一协议层。&lt;/li&gt;
&lt;li&gt;只在多设备、多用户或远程监控确实存在时，再设计认证、并发和流控。&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;18. 阅读完成后的自检问题&lt;a href=&quot;#18-阅读完成后的自检问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;B1 中 PID 是 12 字节，为什么完整帧是 18 字节？&lt;/li&gt;
&lt;li&gt;如果一次 &lt;code&gt;read()&lt;/code&gt; 只返回 &lt;code&gt;AA AA B0&lt;/code&gt;，解析器怎样处理？&lt;/li&gt;
&lt;li&gt;如果 LEN 是 100，为什么解析器不会继续等待 100 字节？&lt;/li&gt;
&lt;li&gt;主线程入队一帧后，正阻塞在 &lt;code&gt;epoll_wait()&lt;/code&gt; 的工作线程怎样被唤醒？&lt;/li&gt;
&lt;li&gt;为什么串口只在一个线程中执行 &lt;code&gt;write()&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;为什么 EPOLLOUT 不能一直注册？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bytesReceived&lt;/code&gt; 是在哪个线程发出，&lt;code&gt;onWorkerBytes()&lt;/code&gt; 又在哪个线程执行？&lt;/li&gt;
&lt;li&gt;内置演示模式和 PTY 模拟器分别能验证什么？&lt;/li&gt;
&lt;li&gt;为什么 PID 运算仍然留在 GD32/FreeRTOS 端？&lt;/li&gt;
&lt;li&gt;如果以后加 TCP，哪些模块应该复用，哪个模块需要替换？&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;如果这 10 个问题可以不看代码说清楚，你对这个 Linux 上位机的主体架构就
已经真正理解了。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>tiny-httpd：从零实现 Linux 静态 HTTP 服务器</title><link>https://benjian.xyz/posts/tiny-httpd/</link><guid isPermaLink="true">https://benjian.xyz/posts/tiny-httpd/</guid><description>使用 C、epoll 和线程池实现的 Linux 静态 HTTP 服务器。</description><pubDate>Wed, 12 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h2&gt;1. 项目定位&lt;a href=&quot;#1-项目定位&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;tiny-httpd&lt;/code&gt; 是一个使用 C 语言独立实现的 Linux 静态 HTTP 服务器。项目面向嵌入式 Linux 用户态开发岗位，重点展示 Linux 系统编程、网络编程、并发控制、守护进程和模块化 C 工程能力。&lt;/p&gt;&lt;p&gt;项目只把 Zaver 等开源服务器作为设计参考，没有复用其源文件。功能范围被有意控制在静态 GET 服务：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;支持 HTTP/1.0、HTTP/1.1 和 GET；&lt;/li&gt;
&lt;li&gt;解析 &lt;code&gt;Host&lt;/code&gt;、&lt;code&gt;Connection&lt;/code&gt;、&lt;code&gt;If-Modified-Since&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;支持 keep-alive 和空闲超时；&lt;/li&gt;
&lt;li&gt;返回 200、304、400、403、404、405、414 等状态；&lt;/li&gt;
&lt;li&gt;目录 URI 自动追加 &lt;code&gt;index.html&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;根据扩展名设置 MIME 类型；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;epoll + EPOLLONESHOT + 固定线程池&lt;/code&gt; 处理并发；&lt;/li&gt;
&lt;li&gt;使用最小堆组织超时任务；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;sendfile&lt;/code&gt; 发送静态文件；&lt;/li&gt;
&lt;li&gt;阻止 URI 路径逃逸；&lt;/li&gt;
&lt;li&gt;默认以双 fork 守护进程运行，并使用 PID 文件锁防止重复启动。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;项目不实现 POST、CGI、FastCGI、反向代理、HTTPS、Range、HTTP/2 和动态业务。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;2. 工程目录&lt;a href=&quot;#2-工程目录&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&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;http_server/&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;├── CMakeLists.txt&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;├── README.md&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;├── 需求.md&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;├── 工程代码详解.md&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;├── config/&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;│   └── server.conf&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;├── include/&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;│   ├── build_features.h&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;│   ├── common.h&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;│   ├── config.h&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;│   ├── daemon.h&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;│   ├── http_protocol.h&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;│   ├── io.h&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;│   ├── list.h&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;│   ├── priority_queue.h&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;│   ├── server.h&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;│   ├── static_file.h&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;│   ├── thread_pool.h&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;│   └── timer.h&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;├── src/&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;│   ├── config.c&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;│   ├── daemon.c&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;│   ├── http_protocol.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── io.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── list.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── main.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── priority_queue.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── server.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── static_file.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── thread_pool.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   └── timer.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── tests/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── CMakeLists.txt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── test_http_protocol.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── test_list.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── test_priority_queue.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   └── test_thread_pool.c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── www/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── index.html&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;&lt;td&gt;参数解析和启动流程编排&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;config&lt;/code&gt;&lt;/td&gt;&lt;td&gt;配置解析、范围校验、路径绝对化&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;daemon&lt;/code&gt;&lt;/td&gt;&lt;td&gt;双 fork、setsid、PID 文件锁和描述符重定向&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;server&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Socket、epoll、任务分发和连接生命周期&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;http_protocol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;请求行、请求头、HTTP 日期和 MIME 解析&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;static_file&lt;/code&gt;&lt;/td&gt;&lt;td&gt;URI 映射、安全校验、响应构造和文件发送&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;io&lt;/code&gt;&lt;/td&gt;&lt;td&gt;非阻塞完整写和 sendfile&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;thread_pool&lt;/code&gt;&lt;/td&gt;&lt;td&gt;固定工作线程和 FIFO 任务队列&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;timer&lt;/code&gt;&lt;/td&gt;&lt;td&gt;线程安全的超时任务管理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;priority_queue&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按到期时间排序的二叉最小堆&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;list&lt;/code&gt;&lt;/td&gt;&lt;td&gt;线程池使用的单向链表&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;3. 总体架构&lt;a href=&quot;#3-总体架构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;服务器由一个事件线程和若干工作线程组成。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;默认&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;-f&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;监听 fd&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户端 fd&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;是&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;否&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;读取配置&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;运行模式&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;双 fork 守护化&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;前台运行&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;初始化服务器&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;监听 Socket&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;epoll&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;线程池&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;定时器最小堆&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;epoll_wait&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;accept 新连接&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;提交线程池任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;read 请求&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;解析 GET&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;URI 映射&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;响应头和 sendfile&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;keep-alive&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;重设超时并 rearm&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;关闭连接&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;默认&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;-f&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;监听 fd&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;客户端 fd&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;是&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;否&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;读取配置&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;运行模式&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;双 fork 守护化&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;前台运行&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;初始化服务器&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;监听 Socket&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;epoll&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;线程池&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;定时器最小堆&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;epoll_wait&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;accept 新连接&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;提交线程池任务&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;read 请求&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;解析 GET&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;URI 映射&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;响应头和 sendfile&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;keep-alive&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;重设超时并 rearm&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;p&gt;关闭连接&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&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;main&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;├── http_config_load&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;├── http_daemonize                  # -f 前台模式跳过&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;└── http_server_run&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;├── install_signal_handlers&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;├── http_timer_manager_init&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;├── http_thread_pool_init&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;├── create_listening_socket&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;├── epoll_create1&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;└── epoll_wait&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;├── accept_connections&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;│   └── register_connection&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;│       └── connection_schedule_timeout&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;└── dispatch_connection&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;└── http_thread_pool_submit&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;└── handle_connection_task&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;├── read&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;├── http_request_parse&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;├── process_request&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;│   ├── http_resolve_static_path&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;│   └── http_send_static_response&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;│       ├── http_write_all&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;&lt;/span&gt;&lt;span&gt;│       └── http_send_file&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;/span&gt;&lt;/span&gt;&lt;span&gt;└── connection_rearm 或 connection_close_locked&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;主线程不解析 HTTP，也不发送文件，只管理事件和超时。工作线程承担可能耗时的请求处理，避免慢客户端阻塞整个事件循环。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;4. 启动流程与配置&lt;a href=&quot;#4-启动流程与配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;4.1 main 函数&lt;a href=&quot;#41-main-函数&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;入口位于 &lt;code&gt;src/main.c&lt;/code&gt;。程序使用 &lt;code&gt;getopt_long&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;-c FILE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;指定配置文件&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;&lt;td&gt;前台运行，方便调试&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;-h&lt;/code&gt;&lt;/td&gt;&lt;td&gt;输出帮助&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;启动顺序是：解析参数、读取配置、按需守护化、运行服务器、退出时清理 PID 文件。&lt;/p&gt;&lt;p&gt;&lt;code&gt;main&lt;/code&gt; 只负责流程编排，不放网络业务逻辑，使入口函数保持短小。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4.2 配置结构&lt;a href=&quot;#42-配置结构&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;typedef&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; http_server_config {&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;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;document_root&lt;/span&gt;&lt;span&gt;[PATH_MAX];&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;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pid_file&lt;/span&gt;&lt;span&gt;[PATH_MAX];&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;int&lt;/span&gt;&lt;span&gt; port;&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;int&lt;/span&gt;&lt;span&gt; thread_count;&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;int&lt;/span&gt;&lt;span&gt; idle_timeout_ms;&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;int&lt;/span&gt;&lt;span&gt; listen_backlog;&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;http_server_config_t&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;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;root&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;../www&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;port&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;8080&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;threads&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;4&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;idle_timeout_ms&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5000&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;backlog&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;128&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;pid_file&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;../tiny_httpd.pid&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;http_config_load&lt;/code&gt; 会：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;跳过空行和注释；&lt;/li&gt;
&lt;li&gt;分割 &lt;code&gt;key=value&lt;/code&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;验证 root 是有效目录；&lt;/li&gt;
&lt;li&gt;将 root 和 PID 文件路径转换成绝对路径。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;路径绝对化很重要，因为守护进程会执行 &lt;code&gt;chdir(&quot;/&quot;)&lt;/code&gt;。如果保留相对路径，守护化后就无法找到站点目录。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;5. 守护进程实现&lt;a href=&quot;#5-守护进程实现&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;src/daemon.c&lt;/code&gt; 使用经典双 fork：&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;└── fork&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;&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;├── setsid&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;└── fork&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;/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;第一次 fork 和 &lt;code&gt;setsid&lt;/code&gt; 使进程脱离原会话、进程组和控制终端。第二次 fork 使最终进程不再是会话首进程，从而不能重新获得控制终端。&lt;/p&gt;&lt;p&gt;其他守护化操作：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;umask(027)&lt;/code&gt; 限制新文件权限；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chdir(&quot;/&quot;)&lt;/code&gt; 避免占用启动目录；&lt;/li&gt;
&lt;li&gt;标准输入、输出和错误重定向到 &lt;code&gt;/dev/null&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建 PID 文件并使用 &lt;code&gt;flock(LOCK_EX | LOCK_NB)&lt;/code&gt; 加排他锁。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;PID 文件描述符在服务运行期间保持打开，否则文件锁会被释放。第二个实例尝试启动时无法获得锁，从而避免重复启动。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;6. Socket 和 epoll&lt;a href=&quot;#6-socket-和-epoll&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;6.1 创建监听 Socket&lt;a href=&quot;#61-创建监听-socket&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;create_listening_socket&lt;/code&gt; 依次执行：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;socket(AF_INET, SOCK_STREAM, 0)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;设置 &lt;code&gt;SO_REUSEADDR&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;fcntl&lt;/code&gt; 添加 &lt;code&gt;O_NONBLOCK&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;fcntl&lt;/code&gt; 添加 &lt;code&gt;FD_CLOEXEC&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bind&lt;/code&gt; 到配置端口和 &lt;code&gt;INADDR_ANY&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listen&lt;/code&gt; 开始监听。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;code&gt;O_NONBLOCK&lt;/code&gt; 使系统调用在暂时无法完成时返回 &lt;code&gt;EAGAIN&lt;/code&gt;，不会无限阻塞线程。&lt;code&gt;FD_CLOEXEC&lt;/code&gt; 防止描述符被以后执行的其他程序意外继承。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.2 epoll 注册&lt;a href=&quot;#62-epoll-注册&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;监听 fd 使用：&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;listen_event.events &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; EPOLLIN;&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;listen_event.data.ptr &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;/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;客户端 fd 使用：&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;event.events &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; EPOLLIN &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; EPOLLONESHOT &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; EPOLLRDHUP;&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;event.data.ptr &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; connection;&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;data.ptr == NULL&lt;/code&gt; 表示监听 fd，其他值直接保存连接对象指针，省去额外的 fd 映射表。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.3 LT 与 ET&lt;a href=&quot;#63-lt-与-et&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;当前客户端事件没有设置 &lt;code&gt;EPOLLET&lt;/code&gt;，因此使用水平触发 LT，而不是边缘触发 ET。&lt;/p&gt;&lt;p&gt;选择 LT 的原因：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;行为直观，代码容易验证；&lt;/li&gt;
&lt;li&gt;数据没有读完时，重新激活后仍可收到通知；&lt;/li&gt;
&lt;li&gt;避免 ET 下未读到 EAGAIN 导致通知丢失。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;工作线程仍循环读到 &lt;code&gt;EAGAIN&lt;/code&gt;，目的是尽量一次取完当前数据、减少事件循环往返，并不代表使用了 ET。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.4 EPOLLONESHOT&lt;a href=&quot;#64-epolloneshot&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;一次事件触发后，&lt;code&gt;EPOLLONESHOT&lt;/code&gt; 会暂时禁用这个 fd，直到调用 &lt;code&gt;EPOLL_CTL_MOD&lt;/code&gt; 重新激活。&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;&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;-&amp;gt; epoll 只报告一次&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;-&amp;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;-&amp;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;-&amp;gt; keep-alive 时重新激活&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;/section&gt;&lt;section&gt;&lt;h3&gt;6.5 epoll 与定时器结合&lt;a href=&quot;#65-epoll-与定时器结合&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;事件循环先调用 &lt;code&gt;http_timer_next_timeout&lt;/code&gt; 获取最近定时任务剩余时间，再把它作为 &lt;code&gt;epoll_wait&lt;/code&gt; 的 timeout。&lt;/p&gt;&lt;p&gt;这样没有网络事件时，epoll 会在最近连接超时时自动醒来；没有定时任务时 timeout 为 &lt;code&gt;-1&lt;/code&gt;，可以无限等待。不需要额外的定时器线程，也不需要固定间隔轮询。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;7. 线程池&lt;a href=&quot;#7-线程池&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;typedef&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; http_thread_pool {&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;pthread_t&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;threads;&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;int&lt;/span&gt;&lt;span&gt; thread_count;&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;int&lt;/span&gt;&lt;span&gt; stopping;&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;pthread_mutex_t&lt;/span&gt;&lt;span&gt; mutex;&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;pthread_cond_t&lt;/span&gt;&lt;span&gt; condition;&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;http_list_t&lt;/span&gt;&lt;span&gt; tasks;&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;http_thread_pool_t&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;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;typedef&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; http_task {&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;void&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;function)(&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;argument);&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;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;argument;&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;http_task_t&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;code&gt;http_thread_pool_submit&lt;/code&gt; 在锁内把任务追加到 FIFO 链表，然后通过 &lt;code&gt;pthread_cond_signal&lt;/code&gt; 唤醒一个线程。&lt;/p&gt;&lt;p&gt;工作线程等待条件必须写成 &lt;code&gt;while&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;while&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;http_list_size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;tasks&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;0&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;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;stopping) {&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;pthread_cond_wait&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;condition&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;pool&lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;mutex&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;/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;p&gt;任务出队后先解锁，再执行回调。否则一个线程处理网络请求期间会一直占用队列锁，其他线程无法取任务，线程池将失去并发意义。&lt;/p&gt;&lt;p&gt;销毁时设置 &lt;code&gt;stopping&lt;/code&gt;、广播唤醒线程，再 &lt;code&gt;pthread_join&lt;/code&gt; 等待已入队任务完成。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;8. 连接生命周期&lt;a href=&quot;#8-连接生命周期&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;typedef&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; http_connection {&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;int&lt;/span&gt;&lt;span&gt; fd;&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;int&lt;/span&gt;&lt;span&gt; closed;&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;uint64_t&lt;/span&gt;&lt;span&gt; timer_generation;&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;atomic_int&lt;/span&gt;&lt;span&gt; reference_count;&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;pthread_mutex_t&lt;/span&gt;&lt;span&gt; mutex;&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;http_server_t&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;server;&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;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;receive_buffer&lt;/span&gt;&lt;span&gt;[HTTP_REQUEST_BUFFER_SIZE];&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;size_t&lt;/span&gt;&lt;span&gt; receive_length;&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;http_connection_t&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;table&gt;&lt;thead&gt;&lt;tr&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;&lt;code&gt;fd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;客户端 Socket&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;closed&lt;/code&gt;&lt;/td&gt;&lt;td&gt;防止重复关闭&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;timer_generation&lt;/code&gt;&lt;/td&gt;&lt;td&gt;使旧超时任务失效&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;reference_count&lt;/code&gt;&lt;/td&gt;&lt;td&gt;保证对象仍被使用时不释放&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mutex&lt;/code&gt;&lt;/td&gt;&lt;td&gt;保护连接状态和接收缓冲区&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;server&lt;/code&gt;&lt;/td&gt;&lt;td&gt;访问共享服务器对象&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;receive_buffer&lt;/code&gt;&lt;/td&gt;&lt;td&gt;保存未完全解析的数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;receive_length&lt;/code&gt;&lt;/td&gt;&lt;td&gt;有效数据长度&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;section&gt;&lt;h3&gt;8.1 锁与引用计数的区别&lt;a href=&quot;#81-锁与引用计数的区别&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;互斥锁解决并发读写状态的问题；&lt;/li&gt;
&lt;li&gt;引用计数解决对象何时可以释放的问题。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;连接可能同时被基础连接、线程池任务和定时任务引用。仅使用互斥锁不能阻止对象在另一个线程使用时被释放。&lt;/p&gt;&lt;p&gt;&lt;code&gt;connection_retain&lt;/code&gt; 原子增加引用，&lt;code&gt;connection_release&lt;/code&gt; 原子减少引用。引用数归零时才销毁 mutex 和连接对象。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.2 关闭连接&lt;a href=&quot;#82-关闭连接&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;connection_close_locked&lt;/code&gt; 在持锁状态下：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;检查是否已经关闭；&lt;/li&gt;
&lt;li&gt;设置 &lt;code&gt;closed&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;增加 generation，使定时任务失效；&lt;/li&gt;
&lt;li&gt;从 epoll 删除 fd；&lt;/li&gt;
&lt;li&gt;关闭 Socket。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;它不会直接释放连接对象。调用者先解锁，再释放基础引用，否则可能释放 mutex 后又执行 unlock，造成 use-after-free。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.3 工作线程处理连接&lt;a href=&quot;#83-工作线程处理连接&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;handle_connection_task&lt;/code&gt; 的流程：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;锁定连接；&lt;/li&gt;
&lt;li&gt;循环 &lt;code&gt;read&lt;/code&gt; 到 &lt;code&gt;EAGAIN&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;http_request_parse&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;数据不完整则保留缓冲区；&lt;/li&gt;
&lt;li&gt;完整请求调用 &lt;code&gt;process_request&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;memmove&lt;/code&gt; 删除已经消费的请求；&lt;/li&gt;
&lt;li&gt;缓冲区还有完整请求则继续处理；&lt;/li&gt;
&lt;li&gt;短连接或错误时关闭；&lt;/li&gt;
&lt;li&gt;长连接创建新超时任务，并 rearm &lt;code&gt;EPOLLONESHOT&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;释放任务持有的连接引用。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;同一次 read 中存在多个顺序请求时，该循环也能逐个处理。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;9. HTTP 解析&lt;a href=&quot;#9-http-解析&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;HTTP_PARSE_COMPLETE&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;HTTP_PARSE_INCOMPLETE&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;HTTP_PARSE_BAD_REQUEST&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;HTTP_PARSE_METHOD_NOT_ALLOWED&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;HTTP_PARSE_URI_TOO_LONG&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;HTTP_PARSE_INCOMPLETE&lt;/code&gt; 不是协议错误，表示请求被 TCP 分段，只到达了一部分。服务器保留数据并等待下一次可读事件。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;9.1 请求行&lt;a href=&quot;#91-请求行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;GET&lt;/span&gt;&lt;span&gt; /index.html &lt;/span&gt;&lt;span&gt;HTTP&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;1.1&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;解析器查找两个空格，把请求行分成方法、URI 和版本：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;方法必须是 GET，否则返回 405；&lt;/li&gt;
&lt;li&gt;URI 不能为空且不能超过固定容量；&lt;/li&gt;
&lt;li&gt;版本只接受 HTTP/1.0 或 HTTP/1.1；&lt;/li&gt;
&lt;li&gt;HTTP/1.1 默认 keep-alive；&lt;/li&gt;
&lt;li&gt;HTTP/1.0 默认关闭。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.2 请求头&lt;a href=&quot;#92-请求头&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;解析器查找 CRLF，逐行处理请求头，直到遇到空行。&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Host&lt;/code&gt;&lt;/td&gt;&lt;td&gt;保存值；HTTP/1.1 缺少 Host 返回 400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Connection&lt;/code&gt;&lt;/td&gt;&lt;td&gt;close 或 keep-alive 覆盖默认值&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;If-Modified-Since&lt;/code&gt;&lt;/td&gt;&lt;td&gt;解析 GMT 日期，用于 304&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;请求头名称不区分大小写。未知请求头被忽略，以兼容浏览器自动携带的其他字段。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.3 304 缓存&lt;a href=&quot;#93-304-缓存&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;如果请求携带 &lt;code&gt;If-Modified-Since&lt;/code&gt;，且文件修改时间 &lt;code&gt;st_mtime&lt;/code&gt; 不晚于请求时间，服务器返回 304，&lt;code&gt;Content-Length&lt;/code&gt; 为 0，不发送文件实体。&lt;/p&gt;&lt;p&gt;解析 HTTP 日期使用 &lt;code&gt;strptime&lt;/code&gt;，转换 UTC 时间使用 &lt;code&gt;timegm&lt;/code&gt;，响应日期通过 &lt;code&gt;gmtime_r + strftime&lt;/code&gt; 生成。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;10. URI 映射和安全&lt;a href=&quot;#10-uri-映射和安全&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;http_resolve_static_path&lt;/code&gt; 按以下顺序执行：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;移除查询串；&lt;/li&gt;
&lt;li&gt;解码 &lt;code&gt;%xx&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;拒绝控制字符、NUL、反斜杠和编码斜杠；&lt;/li&gt;
&lt;li&gt;与站点根目录拼接；&lt;/li&gt;
&lt;li&gt;目录目标追加 &lt;code&gt;index.html&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;realpath&lt;/code&gt; 解析点目录和符号链接；&lt;/li&gt;
&lt;li&gt;验证最终路径仍在站点根目录；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;stat&lt;/code&gt; 确认目标是普通文件。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;不能只检查 URI 是否包含 &lt;code&gt;..&lt;/code&gt;，因为还存在百分号编码、符号链接和复杂路径组合。项目以 &lt;code&gt;realpath&lt;/code&gt; 的最终结果为准。&lt;/p&gt;&lt;p&gt;根目录若是 &lt;code&gt;/srv/www&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;/srv/www&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;/srv/www/index.html&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;/srv/www/assets/app.css&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;/srv/www-secret/file&lt;/code&gt; 虽有相同字符串前缀，但前缀后不是 &lt;code&gt;/&lt;/code&gt; 或字符串结尾，因此不会被误判为根目录内部。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;11. 响应、MIME 和 sendfile&lt;a href=&quot;#11-响应mime-和-sendfile&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;正常响应包含状态行、Date、Server、Connection、Content-Type、Content-Length 和 Last-Modified。&lt;/p&gt;&lt;p&gt;字符串使用有容量限制的 &lt;code&gt;snprintf&lt;/code&gt; 构造，并检查是否发生截断。&lt;/p&gt;&lt;p&gt;MIME 表支持 HTML、CSS、JavaScript、TXT、JSON、PNG、JPEG、GIF、SVG、ICO 和 PDF。未知扩展名使用 &lt;code&gt;application/octet-stream&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;响应头通过 &lt;code&gt;http_write_all&lt;/code&gt; 发送，文件实体通过 &lt;code&gt;http_send_file&lt;/code&gt; 调用 Linux &lt;code&gt;sendfile&lt;/code&gt;。&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;文件 -&amp;gt; 内核页缓存 -&amp;gt; 用户态缓冲区 -&amp;gt; Socket 内核缓冲区&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;sendfile&lt;/code&gt; 避免应用层自行执行 &lt;code&gt;read + write&lt;/code&gt; 和管理文件缓冲区，适合静态文件服务，也有利于资源受限设备减少内存复制。&lt;/p&gt;&lt;p&gt;一次 write 或 sendfile 不保证发完全部数据。两个 I/O 函数都会维护剩余长度，并处理：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EINTR&lt;/code&gt;：重试；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EAGAIN&lt;/code&gt;：使用 &lt;code&gt;poll(POLLOUT)&lt;/code&gt; 等待可写；&lt;/li&gt;
&lt;li&gt;部分写：移动游标后继续；&lt;/li&gt;
&lt;li&gt;超过 30 秒仍不可写：以 &lt;code&gt;ETIMEDOUT&lt;/code&gt; 失败。&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;12. 定时器和最小堆&lt;a href=&quot;#12-定时器和最小堆&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;定时器使用 &lt;code&gt;CLOCK_MONOTONIC&lt;/code&gt; 计算毫秒时间。它不受手工修改系统时间或 NTP 调整影响，适合计算相对超时。&lt;/p&gt;&lt;p&gt;底层优先队列是动态数组实现的二叉最小堆：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;堆顶始终是最早到期任务；&lt;/li&gt;
&lt;li&gt;插入通过向上调整恢复堆性质；&lt;/li&gt;
&lt;li&gt;删除堆顶通过向下调整恢复堆性质；&lt;/li&gt;
&lt;li&gt;查看堆顶为 O(1)；&lt;/li&gt;
&lt;li&gt;插入、删除为 O(log n)。&lt;/li&gt;
&lt;/ul&gt;&lt;section&gt;&lt;h3&gt;12.1 generation 机制&lt;a href=&quot;#121-generation-机制&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每次连接进入新阶段时递增 &lt;code&gt;timer_generation&lt;/code&gt;。定时任务保存创建时的 generation，回调执行时只有两者仍相等才关闭连接。&lt;/p&gt;&lt;p&gt;旧任务无需从堆中间查找删除，到期后发现 generation 不匹配便自然失效。代价是旧任务在原到期时间前仍占用少量内存。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.2 定时任务所有权&lt;a href=&quot;#122-定时任务所有权&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;http_timer_schedule_owned&lt;/code&gt; 接收执行回调和参数清理回调。任务正常执行、取消或管理器销毁时，参数清理回调都会释放上下文。&lt;/p&gt;&lt;p&gt;连接定时任务借此释放自己持有的连接引用，防止引用计数泄漏。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;13. 信号和异常处理&lt;a href=&quot;#13-信号和异常处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;SIGINT&lt;/code&gt; 和 &lt;code&gt;SIGTERM&lt;/code&gt; 的处理器只设置 &lt;code&gt;sig_atomic_t&lt;/code&gt; 标志。清理资源仍在普通事件循环中执行，因为日志、malloc 和 pthread 锁等操作不是异步信号安全的。&lt;/p&gt;&lt;p&gt;服务器忽略 &lt;code&gt;SIGPIPE&lt;/code&gt;。客户端提前断开后，写 Socket 会返回 &lt;code&gt;EPIPE&lt;/code&gt;，而不会终止整个服务进程。&lt;/p&gt;&lt;p&gt;关键 errno：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;errno&lt;/th&gt;&lt;th&gt;含义与处理&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;EINTR&lt;/code&gt;&lt;/td&gt;&lt;td&gt;被信号中断，重试系统调用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;EAGAIN/EWOULDBLOCK&lt;/code&gt;&lt;/td&gt;&lt;td&gt;非阻塞 fd 暂时无法继续，稍后重试&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;EPIPE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;对端已断开，关闭当前连接&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ETIMEDOUT&lt;/code&gt;&lt;/td&gt;&lt;td&gt;等待可写超时，终止当前响应&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;14. 构建与测试&lt;a href=&quot;#14-构建与测试&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;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;cmake&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-S&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-B&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build&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;cmake&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-j4&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;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;-Wall -Wextra -Wpedantic -Werror&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;p&gt;运行测试：&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;ctest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--test-dir&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--output-on-failure&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;table&gt;&lt;thead&gt;&lt;tr&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;&lt;code&gt;test_list&lt;/code&gt;&lt;/td&gt;&lt;td&gt;FIFO 顺序、元素数量和空链表&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_priority_queue&lt;/code&gt;&lt;/td&gt;&lt;td&gt;最小堆按到期时间弹出&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_thread_pool&lt;/code&gt;&lt;/td&gt;&lt;td&gt;4 个线程完整执行 1000 个任务&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_http_protocol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;GET、Host、Connection、不完整请求、POST 拒绝&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;已经完成的集成验证包括 200、304、403、404、405、keep-alive 连接复用、路径逃逸拒绝、守护进程 PID 文件和信号停止清理。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;15. 当前限制&lt;a href=&quot;#15-当前限制&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;HTTP 解析器只覆盖当前需要的语法，不是完整 RFC 实现；&lt;/li&gt;
&lt;li&gt;工作线程在发送遇到 EAGAIN 时使用 poll 等待，不是完全事件驱动写；&lt;/li&gt;
&lt;li&gt;没有活动连接表，退出时部分空闲连接依赖进程结束由内核回收；&lt;/li&gt;
&lt;li&gt;定时任务使用 generation 失效，旧任务到期前仍占内存；&lt;/li&gt;
&lt;li&gt;没有请求速率限制、最大连接数和单 IP 限制；&lt;/li&gt;
&lt;li&gt;没有日志文件及轮转；&lt;/li&gt;
&lt;li&gt;没有 TLS、Range 和动态请求；&lt;/li&gt;
&lt;li&gt;尚未提供 ARM 交叉编译工具链和开发板实测数据；&lt;/li&gt;
&lt;li&gt;尚未使用 wrk/ab 得到可复现的吞吐和延迟结果。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;后续可增加连接注册表、完全事件驱动写、sanitizer 检查、ARM 部署、压力测试和 systemd service。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>停车场车位与车辆调度系统</title><link>https://benjian.xyz/posts/parking-dispatch/</link><guid isPermaLink="true">https://benjian.xyz/posts/parking-dispatch/</guid><description>使用数组、循环队列与顺序表实现车位分配、等待调度和历史记录。</description><pubDate>Tue, 11 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h2&gt;1. 课题名称&lt;a href=&quot;#1-课题名称&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;停车场车位与车辆调度系统的设计与实现&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;2. 课题背景&lt;a href=&quot;#2-课题背景&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;随着城市机动车数量不断增加，停车场管理问题越来越常见。传统人工管理方式效率较低，容易出现车位分配不合理、车辆等待时间过长、信息记录不完整等问题。为了提高停车场管理效率，有必要设计一个能够完成车辆进出管理、车位分配、等待调度和记录查询的简易系统。&lt;/p&gt;&lt;p&gt;本课题以停车场管理为实际应用场景，使用 C 语言和数据结构基础知识实现一个命令行停车场车位与车辆调度系统。该系统不依赖数据库和 Web 技术，重点体现数组、结构体、队列等知识在实际问题中的应用，适合作为数据结构课程设计题目。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;3. 设计目标&lt;a href=&quot;#3-设计目标&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;实现停车场满载情况下的车辆排队等待&lt;/li&gt;
&lt;li&gt;实现车辆离场处理及费用计算&lt;/li&gt;
&lt;li&gt;实现空闲车位释放后的自动调度&lt;/li&gt;
&lt;li&gt;实现停车区、等待区和历史记录的查询&lt;/li&gt;
&lt;li&gt;通过项目实践加深对数据结构与 C 语言程序设计的理解&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;4. 需求分析&lt;a href=&quot;#4-需求分析&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;4.1 功能需求&lt;a href=&quot;#41-功能需求&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统应具备以下功能：&lt;/p&gt;&lt;ol&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;li&gt;查看历史记录&lt;/li&gt;
&lt;li&gt;查看车位统计信息&lt;/li&gt;
&lt;li&gt;退出系统&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4.2 业务规则&lt;a href=&quot;#42-业务规则&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&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;li&gt;停车费用按停车时长计算&lt;/li&gt;
&lt;li&gt;车辆信息和历史记录在程序运行期间保存在内存中&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4.3 输入输出需求&lt;a href=&quot;#43-输入输出需求&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;输入内容主要包括：&lt;/p&gt;&lt;ul&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;/p&gt;&lt;ul&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;li&gt;历史停车记录&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;5. 可行性分析&lt;a href=&quot;#5-可行性分析&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;5.1 技术可行性&lt;a href=&quot;#51-技术可行性&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本系统只使用 C 语言完成，所用知识均为课程阶段已学习内容，包括：&lt;/p&gt;&lt;ul&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;li&gt;模块化编程&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;实现难度适中，技术上完全可行。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5.2 经济可行性&lt;a href=&quot;#52-经济可行性&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本系统作为课程设计项目，不需要额外硬件设备或商业软件支持，仅需普通编译环境即可运行，因此成本低。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5.3 操作可行性&lt;a href=&quot;#53-操作可行性&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统采用菜单方式交互，输入输出直观，操作简单，适合演示与测试。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;6. 总体设计&lt;a href=&quot;#6-总体设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;6.1 系统结构&lt;a href=&quot;#61-系统结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统采用模块化设计，主要由以下模块组成：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main.c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parking.c&lt;/code&gt; / &lt;code&gt;parking.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue.c&lt;/code&gt; / &lt;code&gt;queue.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vehicle.c&lt;/code&gt; / &lt;code&gt;vehicle.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record.c&lt;/code&gt; / &lt;code&gt;record.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils.c&lt;/code&gt; / &lt;code&gt;utils.h&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;各模块功能如下：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 模块负责菜单显示和主流程控制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parking&lt;/code&gt; 模块负责车位管理和停车区操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue&lt;/code&gt; 模块负责等待车辆的排队调度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vehicle&lt;/code&gt; 模块负责车辆信息初始化与显示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record&lt;/code&gt; 模块负责历史记录保存与查看&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils&lt;/code&gt; 模块负责时间和费用等通用处理&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6.2 系统流程&lt;a href=&quot;#62-系统流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统总体流程如下：&lt;/p&gt;&lt;ol&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;li&gt;否则返回主菜单继续执行&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;7. 数据结构设计&lt;a href=&quot;#7-数据结构设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;7.1 车辆信息结构体&lt;a href=&quot;#71-车辆信息结构体&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;车辆信息使用 &lt;code&gt;Vehicle&lt;/code&gt; 结构体表示，主要字段如下：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;plate_number&lt;/code&gt;：车牌号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enter_time&lt;/code&gt;：进场时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exit_time&lt;/code&gt;：离场时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;duration&lt;/code&gt;：停车时长&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space_id&lt;/code&gt;：车位编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fee&lt;/code&gt;：停车费用&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;该结构体是系统中最核心的数据对象，用于停车区、等待队列和历史记录之间的数据传递。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.2 停车区结构&lt;a href=&quot;#72-停车区结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;停车区使用结构体数组实现，每个车位使用 &lt;code&gt;ParkingSpace&lt;/code&gt; 结构体表示，包含：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：车位编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;occupied&lt;/code&gt;：是否被占用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vehicle&lt;/code&gt;：当前停放车辆信息&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;整个停车场使用 &lt;code&gt;ParkingLot&lt;/code&gt; 结构体表示，包含：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;spaces[MAX_SPACES]&lt;/code&gt;：车位数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capacity&lt;/code&gt;：停车场容量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;occupied_count&lt;/code&gt;：已占用车位数&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这种设计本质上属于顺序存储结构，便于通过下标直接访问车位信息。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.3 等待队列结构&lt;a href=&quot;#73-等待队列结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;等待车辆使用循环队列实现，其结构为：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data[MAX_WAITING]&lt;/code&gt;：等待车辆数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;front&lt;/code&gt;：队头下标&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rear&lt;/code&gt;：队尾下标&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;：当前队列长度&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;队列满足先进先出原则，符合“先到先等、先等先入场”的业务需求。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;7.4 历史记录结构&lt;a href=&quot;#74-历史记录结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;历史记录使用顺序表形式实现，即使用数组保存所有离场车辆信息，结构中包含：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;records[MAX_RECORDS]&lt;/code&gt;：历史记录数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;：当前记录数&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这种实现方式简单直观，适合课程设计规模的数据管理。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;8. 详细设计&lt;a href=&quot;#8-详细设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;8.1 主菜单设计&lt;a href=&quot;#81-主菜单设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统主菜单包括以下选项：&lt;/p&gt;&lt;ol&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;li&gt;查看历史记录&lt;/li&gt;
&lt;li&gt;查看车位统计&lt;/li&gt;
&lt;li&gt;退出系统&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;程序在 &lt;code&gt;main&lt;/code&gt; 函数中通过 &lt;code&gt;do...while&lt;/code&gt; 循环持续显示菜单，直到用户输入 &lt;code&gt;0&lt;/code&gt; 退出。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.2 车辆进场设计&lt;a href=&quot;#82-车辆进场设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;车辆进场处理流程如下：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;输入车牌号&lt;/li&gt;
&lt;li&gt;输入进场时间&lt;/li&gt;
&lt;li&gt;初始化车辆结构体&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;park_vehicle&lt;/code&gt; 判断停车区是否有空位&lt;/li&gt;
&lt;li&gt;若有空位则分配车位&lt;/li&gt;
&lt;li&gt;若无空位则调用 &lt;code&gt;enqueue_vehicle&lt;/code&gt; 加入等待队列&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;该功能体现了顺序表查找空闲位置和队列入队操作。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.3 车辆离场设计&lt;a href=&quot;#83-车辆离场设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;车辆离场处理流程如下：&lt;/p&gt;&lt;ol&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;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;/li&gt;
&lt;/ol&gt;&lt;p&gt;该部分是整个系统的核心逻辑，体现了多个数据结构之间的协作。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.4 停车区查询设计&lt;a href=&quot;#84-停车区查询设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;停车区查询功能遍历车位数组：&lt;/p&gt;&lt;ul&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;h3&gt;8.5 等待队列查询设计&lt;a href=&quot;#85-等待队列查询设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;等待队列查询功能从队头开始遍历循环队列，依次输出等待车辆信息，用于显示排队情况。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.6 历史记录查询设计&lt;a href=&quot;#86-历史记录查询设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;历史记录查询功能遍历历史记录数组，输出每一辆已离场车辆的车牌号、车位号、进场时间、停车时长和费用信息。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;8.7 统计功能设计&lt;a href=&quot;#87-统计功能设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统统计功能输出以下信息：&lt;/p&gt;&lt;ul&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;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;9. 核心算法说明&lt;a href=&quot;#9-核心算法说明&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;9.1 车位分配算法&lt;a href=&quot;#91-车位分配算法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统采用顺序扫描方式分配车位：&lt;/p&gt;&lt;ol&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;/ol&gt;&lt;p&gt;该算法实现简单，适合小规模停车场系统。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;9.2 等待调度算法&lt;a href=&quot;#92-等待调度算法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统采用先进先出调度策略：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;当停车场满时，车辆进入等待队列尾部&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;h3&gt;9.3 费用计算算法&lt;a href=&quot;#93-费用计算算法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;系统当前使用简单计费规则：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;停车时长为 &lt;code&gt;exit_time - enter_time&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每小时收费 5 元&lt;/li&gt;
&lt;li&gt;若计算结果小于等于 0，则默认收费 5 元&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;该算法实现简单，便于验证和演示。后续可以继续扩展为更复杂的收费策略。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;10. 主要函数说明&lt;a href=&quot;#10-主要函数说明&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;10.1 &lt;code&gt;init_parking_lot&lt;/code&gt;&lt;a href=&quot;#101-init_parking_lot&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：初始化停车场，包括车位编号、占用状态和容量信息。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.2 &lt;code&gt;park_vehicle&lt;/code&gt;&lt;a href=&quot;#102-park_vehicle&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：将车辆停入停车场，若有空位则返回车位号，否则返回失败标志。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.3 &lt;code&gt;remove_vehicle&lt;/code&gt;&lt;a href=&quot;#103-remove_vehicle&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：根据车牌号将车辆从停车区移除，并返回车辆信息。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.4 &lt;code&gt;enqueue_vehicle&lt;/code&gt;&lt;a href=&quot;#104-enqueue_vehicle&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：将车辆加入等待队列。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.5 &lt;code&gt;dequeue_vehicle&lt;/code&gt;&lt;a href=&quot;#105-dequeue_vehicle&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：从等待队列取出队头车辆。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.6 &lt;code&gt;add_record&lt;/code&gt;&lt;a href=&quot;#106-add_record&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：将离场车辆写入历史记录表。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.7 &lt;code&gt;calculate_duration&lt;/code&gt;&lt;a href=&quot;#107-calculate_duration&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：计算停车时长。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;10.8 &lt;code&gt;calculate_fee&lt;/code&gt;&lt;a href=&quot;#108-calculate_fee&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;功能：根据停车时长计算费用。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;11. 测试设计&lt;a href=&quot;#11-测试设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;11.1 测试目标&lt;a href=&quot;#111-测试目标&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;验证系统各项基础功能是否能够正确运行，重点检查：&lt;/p&gt;&lt;ul&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;li&gt;历史记录是否成功保存&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;11.2 测试用例&lt;a href=&quot;#112-测试用例&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;section&gt;&lt;h4&gt;测试用例 1：车辆正常进场&lt;a href=&quot;#测试用例-1车辆正常进场&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;输入一辆新车牌号和进场时间&lt;/li&gt;
&lt;li&gt;预期结果：车辆进入停车场并获得车位编号&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;测试用例 2：停车场满后进入等待队列&lt;a href=&quot;#测试用例-2停车场满后进入等待队列&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;连续输入超过车位容量的车辆&lt;/li&gt;
&lt;li&gt;预期结果：超出容量的车辆进入等待队列&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;测试用例 3：车辆正常离场&lt;a href=&quot;#测试用例-3车辆正常离场&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;输入已在停车场中的车辆车牌号和离场时间&lt;/li&gt;
&lt;li&gt;预期结果：车辆成功离场，显示停车时长和费用&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;测试用例 4：等待车辆自动调度&lt;a href=&quot;#测试用例-4等待车辆自动调度&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;在停车场满且等待队列非空时，有车辆离场&lt;/li&gt;
&lt;li&gt;预期结果：等待队列中的第一辆车自动进入停车场&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h4&gt;测试用例 5：查询功能测试&lt;a href=&quot;#测试用例-5查询功能测试&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;分别查看停车区、等待队列、历史记录和统计信息&lt;/li&gt;
&lt;li&gt;预期结果：输出内容与当前系统状态一致&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;12. 运行环境与运行方法&lt;a href=&quot;#12-运行环境与运行方法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;12.1 运行环境&lt;a href=&quot;#121-运行环境&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;操作系统：Linux、macOS 或 Windows&lt;/li&gt;
&lt;li&gt;编译器：GCC&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;12.2 编译命令&lt;a href=&quot;#122-编译命令&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;gcc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main.c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;parking.c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;queue.c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;vehicle.c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record.c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;utils.c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;parking&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;h3&gt;12.3 运行命令&lt;a href=&quot;#123-运行命令&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&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;./parking&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;section&gt;&lt;h2&gt;13. 系统特点与不足&lt;a href=&quot;#13-系统特点与不足&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;13.1 系统特点&lt;a href=&quot;#131-系统特点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;采用 C 语言实现，结构清晰&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;/section&gt;&lt;section&gt;&lt;h3&gt;13.2 系统不足&lt;a href=&quot;#132-系统不足&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&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;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;14. 改进方向&lt;a href=&quot;#14-改进方向&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;增加车牌查询和车辆修改功能&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;/section&gt;
&lt;section&gt;&lt;h2&gt;15. 课程设计总结&lt;a href=&quot;#15-课程设计总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;通过本次课程设计，完成了一个基于 C 语言和基础数据结构的停车场车位与车辆调度系统。系统实现了车辆进场、离场、等待调度、历史记录查询和车位统计等主要功能，将抽象的数据结构知识与具体业务问题结合起来。&lt;/p&gt;&lt;p&gt;在实现过程中，加深了对结构体、数组、循环队列、模块化编程等知识的理解，也提高了分析问题、拆分模块和组织程序的能力。该项目具有较强的实践性和教学意义，达到了课程设计的基本目标。&lt;/p&gt;&lt;/section&gt;</content:encoded></item></channel></rss>