<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Dait&apos;s 技术小栈</title><description>热爱生活的人，或许会偶尔心生困顿，但绝对不会彻底失去希望。</description><link>https://www.daitcc.top/</link><language>zh_CN</language><item><title>C语言实现面向对象编程（OOP）补充内容</title><link>https://www.daitcc.top/posts/8b17e87d/</link><guid isPermaLink="true">https://www.daitcc.top/posts/8b17e87d/</guid><description>本补充专题讲解向上转型、向下转型与虚函数防御。向上转型靠基类首成员规则保证地址一致；向下转型引入 Linux 内核的 container_of 宏，利用 offsetof 编译期偏移计算实现零开销恢复；虚函数防御通过修正后的 SAFE_CALL 宏统一检查空指针，杜绝崩溃。三者结合，可在纯 C 中安全运用多态。</description><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C语言实现面向对象编程（OOP）补充内容&lt;/h1&gt;
&lt;p&gt;上篇内容实现了继承与多态，但围绕指针转换和虚函数调用安全还有几个关键问题没有展开。本补充专题将集中讲解&lt;strong&gt;向上转型、向下转型（container_of）和虚函数防御&lt;/strong&gt;，并配以可运行的测试代码，帮助你彻底理清转型机制与防御式编程的配合方式。&lt;/p&gt;
&lt;h3&gt;一、向上转型&lt;/h3&gt;
&lt;p&gt;向上转型是指将&lt;strong&gt;派生类指针&lt;/strong&gt;赋值给&lt;strong&gt;基类指针&lt;/strong&gt;，编译器保证安全，无需任何额外代码。&lt;/p&gt;
&lt;h4&gt;1.1 安全的前提：基类作为首成员&lt;/h4&gt;
&lt;p&gt;我们始终把基类对象放在派生类的第一个位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct Student {
    Person base_;        /* 必须作为第一个成员 */
    char   school_[50];
} Student;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，《C 语言标准》中“结构体首成员地址与结构体地址相同”的规则保证了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.daitcc.top/i/1/1.webp#pic_center&quot; alt=&quot;内存分布&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Student*&lt;/code&gt;、&lt;code&gt;&amp;amp;(s-&amp;gt;base_)&lt;/code&gt; 和 &lt;code&gt;(Person*)s&lt;/code&gt; 这三个指针的值&lt;strong&gt;完全相同&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;向上转型 &lt;code&gt;Person* p = (Person*)s;&lt;/code&gt; 是一次&lt;strong&gt;零开销的类型视角切换&lt;/strong&gt;，编译器保证安全。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;p&lt;/code&gt; 访问 &lt;code&gt;age_&lt;/code&gt;、&lt;code&gt;name_&lt;/code&gt; 的偏移量与访问 &lt;code&gt;base_.age_&lt;/code&gt;、&lt;code&gt;base_.name_&lt;/code&gt; 完全一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以用一段简单的代码验证这三个地址的一致性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student* stu = Student_newWith(&quot;Bob&quot;, 20, &quot;XYZ University&quot;);
Person*  p   = (Person*)stu;

printf(&quot;Student*    = %p\n&quot;, (void*)stu);
printf(&quot;&amp;amp;base_      = %p\n&quot;, (void*)&amp;amp;stu-&amp;gt;base_);
printf(&quot;(Person*)s  = %p\n&quot;, (void*)p);
// 三个输出完全相同，验证首成员规则
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2 如果基类不在首成员会发生什么？&lt;/h4&gt;
&lt;p&gt;故意打乱布局（&lt;strong&gt;切勿模仿&lt;/strong&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct Student {
    int    id;
    Person base_;       /* 不再在开头 */
    char   school_[50];
} Student;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内存布局变为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.daitcc.top/i/1/2.webp#pic_center&quot; alt=&quot;内存布局&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时 &lt;code&gt;(Person*)s&lt;/code&gt; 强制转换仍指向 &lt;code&gt;id&lt;/code&gt;，编译器就会错误地将 &lt;code&gt;id&lt;/code&gt; 的字节当作 &lt;code&gt;name_&lt;/code&gt; 的开始。&lt;strong&gt;向上转型立即失效&lt;/strong&gt;，整个“is‑a”关系崩塌。因此必须死守铁律：&lt;strong&gt;基类永远作为派生类的第一个成员&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;1.3 多个派生类的内存分布示例&lt;/h4&gt;
&lt;p&gt;当有多个不同的派生类（如 &lt;code&gt;Student&lt;/code&gt; 和 &lt;code&gt;Teacher&lt;/code&gt;）都以 &lt;code&gt;Person&lt;/code&gt; 作为首成员时，它们的内存布局均保持“基类前缀”一致，多态统一接口就此成立。&lt;/p&gt;
&lt;p&gt;新增一个 &lt;code&gt;Teacher&lt;/code&gt; 类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    Person base_;         /* 首成员 */
    char   subject_[50];
    int    years_;
} Teacher;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时两种对象在内存中的排布如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.daitcc.top/i/1/3.webp#pic_center&quot; alt=&quot;内存分布&quot; /&gt;&lt;/p&gt;
&lt;p&gt;无论 &lt;code&gt;Student&lt;/code&gt; 还是 &lt;code&gt;Teacher&lt;/code&gt;，其起始部分都是一个完整的 &lt;code&gt;Person&lt;/code&gt;。因此 &lt;code&gt;(Person*)stu&lt;/code&gt; 与 &lt;code&gt;(Person*)tch&lt;/code&gt; 都直接复用对象首地址，可以安全地通过基类指针操作公共数据或调用虚函数，这正是&lt;strong&gt;多态能作用于不同派生类的内存根基&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们可以用以下代码打印地址并演示多态调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student* stu = Student_newWith(&quot;Bob&quot;,   20, &quot;XYZ University&quot;);
Teacher* tch = Teacher_newWith(&quot;Alice&quot;, 35, &quot;Physics&quot;, 10);

printf(&quot;=== Student 布局 ===\n&quot;);
printf(&quot;Student*    = %p\n&quot;, (void*)stu);
printf(&quot;&amp;amp;base_      = %p\n&quot;, (void*)&amp;amp;stu-&amp;gt;base_);
printf(&quot;&amp;amp;school_    = %p\n&quot;, (void*)&amp;amp;stu-&amp;gt;school_);

printf(&quot;\n=== Teacher 布局 ===\n&quot;);
printf(&quot;Teacher*    = %p\n&quot;, (void*)tch);
printf(&quot;&amp;amp;base_      = %p\n&quot;, (void*)&amp;amp;tch-&amp;gt;base_);
printf(&quot;&amp;amp;subject_   = %p\n&quot;, (void*)&amp;amp;tch-&amp;gt;subject_);
printf(&quot;&amp;amp;years_     = %p\n&quot;, (void*)&amp;amp;tch-&amp;gt;years_);

Person* ps = (Person*)stu;
Person* pt = (Person*)tch;
printf(&quot;\n(Person*)Student = %p  ( == Student* )\n&quot;, (void*)ps);
printf(&quot;(Person*)Teacher = %p  ( == Teacher* )\n&quot;, (void*)pt);

/* 多态调用 */
ps-&amp;gt;vtable_-&amp;gt;print_(ps);   // 实际执行 Student_print_impl
pt-&amp;gt;vtable_-&amp;gt;print_(pt);   // 实际执行 Teacher_print_impl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果（地址值因环境而异，但相等关系固定）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== Student 布局 ===
Student*    = 0x55a7c2d4b2a0
&amp;amp;base_      = 0x55a7c2d4b2a0
&amp;amp;school_    = 0x55a7c2d4b2d8

=== Teacher 布局 ===
Teacher*    = 0x55a7c2d4b320
&amp;amp;base_      = 0x55a7c2d4b320
&amp;amp;subject_   = 0x55a7c2d4b358
&amp;amp;years_     = 0x55a7c2d4b38c

(Person*)Student = 0x55a7c2d4b2a0  ( == Student* )
(Person*)Teacher = 0x55a7c2d4b320  ( == Teacher* )
[Student] 姓名：Bob，年龄：20，学校：XYZ University
[Teacher] 姓名：Alice，年龄：35，科目：Physics
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出中可以清楚看到：每个派生类的首地址、基类成员地址、向上转型后的 &lt;code&gt;Person*&lt;/code&gt; 地址三者完全一致。这就是多态统一接口得以成立的内存基石——&lt;strong&gt;只要基类放在首位，任何派生类都可以被安全地视为基类对象来操作&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;二、向下转型：container_of&lt;/h3&gt;
&lt;p&gt;向下转型是从基类指针恢复出派生类指针。由于 C 语言没有运行时类型信息，向下转型的安全性由程序员负责。这里介绍 Linux 内核中经典的 &lt;strong&gt;container_of 宏&lt;/strong&gt;，它利用编译期偏移量计算实现零开销转换。&lt;/p&gt;
&lt;h4&gt;2.1 关键工具：offsetof&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;offsetof&lt;/code&gt; 是 C 标准库（&lt;code&gt;&amp;lt;stddef.h&amp;gt;&lt;/code&gt;）提供的宏，用于计算结构体中某个成员相对于结构体起始地址的字节偏移：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stddef.h&amp;gt;

size_t offset = offsetof(Student, base_);   // 编译期计算出 base_ 在 Student 中的偏移
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;base_&lt;/code&gt; 是 &lt;code&gt;Student&lt;/code&gt; 的第一个成员时，&lt;code&gt;offsetof(Student, base_)&lt;/code&gt; 的值为 0；如果不是首成员，则返回对应的正数偏移。它在编译期求值，运行时零开销。&lt;/p&gt;
&lt;h4&gt;2.2 地址回推原理&lt;/h4&gt;
&lt;p&gt;假设我们已经有一个指向 &lt;code&gt;base_&lt;/code&gt; 的有效指针 &lt;code&gt;Person* p&lt;/code&gt;，并且我们知道这个 &lt;code&gt;base_&lt;/code&gt; 属于某个 &lt;code&gt;Student&lt;/code&gt; 对象。那么要得到 &lt;code&gt;Student*&lt;/code&gt;，只需从 &lt;code&gt;p&lt;/code&gt; 往回&lt;strong&gt;减去&lt;/strong&gt; &lt;code&gt;base_&lt;/code&gt; 在 &lt;code&gt;Student&lt;/code&gt; 中的偏移量即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 &lt;code&gt;p&lt;/code&gt; 转成字节指针 &lt;code&gt;(char*)p&lt;/code&gt;，以便进行精确的字节级地址运算。&lt;/li&gt;
&lt;li&gt;减去 &lt;code&gt;offsetof(Student, base_)&lt;/code&gt;，就得到 &lt;code&gt;Student&lt;/code&gt; 的起始地址。&lt;/li&gt;
&lt;li&gt;最后将结果强制转换为 &lt;code&gt;Student*&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个过程用一行代码表达：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student* s = (Student*)((char*)p - offsetof(Student, base_));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;base_&lt;/code&gt; 是首成员时，偏移量为 0，这行代码等价于 &lt;code&gt;(Student*)p&lt;/code&gt;；当 &lt;code&gt;base_&lt;/code&gt; 不是首成员时，它也能正确减去相应偏移，但前提是 &lt;code&gt;p&lt;/code&gt; 确实来自一个 &lt;code&gt;Student&lt;/code&gt; 对象。&lt;/p&gt;
&lt;h4&gt;2.3 封装成宏（container_of）&lt;/h4&gt;
&lt;p&gt;将上面的通用逻辑提取为宏，就可以用于任意结构体和成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该宏接收三个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ptr&lt;/code&gt;：指向成员的指针；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：包含该成员的结构体类型；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;member&lt;/code&gt;：成员名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它返回指向外层结构体的指针。这一宏正是从 &lt;strong&gt;Linux 内核&lt;/strong&gt; 中发展而来的经典设计。在内核中，大量结构体通过内嵌链表头或其他基础结构来连接，&lt;code&gt;container_of&lt;/code&gt; 使得代码可以从一个内嵌成员的地址反向获取完整的结构体对象，极大促进了内核数据结构库的灵活性和可重用性。&lt;/p&gt;
&lt;h4&gt;2.4 在继承模拟中使用&lt;/h4&gt;
&lt;p&gt;在我们的 OOP 框架中，&lt;code&gt;base_&lt;/code&gt; 是派生类的首成员，向下转型可以直接用 &lt;code&gt;container_of&lt;/code&gt; 完成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student* stu = Student_newWith(&quot;Bob&quot;, 20, &quot;XYZ University&quot;);
Person*  p   = (Person*)stu;                 /* 向上转型 */

/* 向下转型：前提是 p 确实来自 Student */
Student* recovered = container_of(p, Student, base_);
printf(&quot;学校：%s\n&quot;, recovered-&amp;gt;school_);   /* 安全访问 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 &lt;code&gt;offsetof(Student, base_)&lt;/code&gt; 为 0，&lt;code&gt;container_of&lt;/code&gt; 实质就是一次类型强转，没有算术运算开销。&lt;/p&gt;
&lt;h4&gt;2.5 风险与使用原则&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;container_of&lt;/code&gt; 不进行任何类型检查。如果你传入的 &lt;code&gt;p&lt;/code&gt; 实际不是从 &lt;code&gt;Student&lt;/code&gt; 来的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person*  p = Person_newWith(&quot;Alice&quot;, 30);
Student* s = container_of(p, Student, base_);  /* 灾难！ */
printf(&quot;%s\n&quot;, s-&amp;gt;school_);                     /* 未定义行为 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序会毫无警告地破坏内存。因此使用 &lt;code&gt;container_of&lt;/code&gt; 向下转型必须建立在&lt;strong&gt;严格的上下文保证&lt;/strong&gt;之上。如果需要运行时类型安全，应结合虚表中的类型标签（详见前文）进行判断。&lt;/p&gt;
&lt;h3&gt;三、虚函数防御&lt;/h3&gt;
&lt;p&gt;虚函数调用链 &lt;code&gt;对象 → 虚表 → 函数指针&lt;/code&gt; 中任何一个环节为空都会导致崩溃。防御式编程必须覆盖所有环节。&lt;/p&gt;
&lt;h4&gt;3.1 核心防御手段&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构造时立即绑定虚表&lt;/strong&gt; —— 对象“出生”即具备完整虚表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明 &lt;code&gt;vtable_&lt;/code&gt; 为 &lt;code&gt;const VTable\*&lt;/code&gt;&lt;/strong&gt; —— 阻挡意外篡改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个函数内部检查指针&lt;/strong&gt; —— &lt;code&gt;self&lt;/code&gt;、&lt;code&gt;vtable_&lt;/code&gt;、具体函数指针，逐一校验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;销毁严格使用虚表的 &lt;code&gt;destroy_&lt;/code&gt;&lt;/strong&gt; —— 保证释放完整派生类内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调用方在销毁后置 &lt;code&gt;NULL&lt;/code&gt;&lt;/strong&gt; —— 杜绝悬垂指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3.2 安全调用宏&lt;/h4&gt;
&lt;p&gt;封装一个宏，统一处理检查，且兼容各种派生类指针甚至空指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define SAFE_CALL(obj, func) \
    do { \
        Person* _self_ = (Person*)(obj); \
        if ((_self_) &amp;amp;&amp;amp; (_self_)-&amp;gt;vtable_ &amp;amp;&amp;amp; (_self_)-&amp;gt;vtable_-&amp;gt;func) { \
            (_self_)-&amp;gt;vtable_-&amp;gt;func(_self_); \
        } \
    } while(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设计要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先将 &lt;code&gt;obj&lt;/code&gt; 显式转为 &lt;code&gt;Person*&lt;/code&gt;。得益于基类首成员规则，任何有效的派生类指针（&lt;code&gt;Student*&lt;/code&gt;、&lt;code&gt;Teacher*&lt;/code&gt; 等）都可以安全地转为 &lt;code&gt;Person*&lt;/code&gt;；而 &lt;code&gt;NULL&lt;/code&gt; 转为 &lt;code&gt;Person*&lt;/code&gt; 仍是 &lt;code&gt;NULL&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;依次检查 &lt;code&gt;_self_&lt;/code&gt;、虚表指针、具体函数指针，任一为空则跳过调用，杜绝崩溃。&lt;/li&gt;
&lt;li&gt;虚函数内部根据实际对象的虚表，依然能正确执行派生类的重写版本，不影响多态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person* p = Person_newWith(&quot;Alice&quot;, 30);
SAFE_CALL(p, print_);          /* 正常调用 */
SAFE_CALL(NULL, print_);       /* obj 为 NULL，宏自动跳过，不崩溃 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.3 防御检查清单&lt;/h4&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;_self_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if (_self_ == NULL) return;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;空指针解引用崩溃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;虚表指针 &lt;code&gt;vtable_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if (_self_-&amp;gt;vtable_ == NULL) return;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调用到随机地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;函数指针&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if (_self_-&amp;gt;vtable_-&amp;gt;func == NULL) return;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;执行“纯虚函数”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;销毁后&lt;/td&gt;
&lt;td&gt;调用方将指针置 &lt;code&gt;NULL&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;h3&gt;四、完整测试代码与运行结果&lt;/h3&gt;
&lt;p&gt;以下测试代码综合演示了向上转型的地址验证（含多派生类）、向下转型（&lt;code&gt;container_of&lt;/code&gt;）以及 &lt;code&gt;SAFE_CALL&lt;/code&gt; 宏的防御效果。代码可直接编译运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test_cast_and_defense.c
 * 演示向上转型（含多派生类地址对比）、向下转型（container_of）和虚函数防御宏。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;stddef.h&amp;gt;        /* offsetof */

/* ----------  container_of 宏 ---------- */
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

/* ---------- 基类 Person ---------- */
typedef struct Person {
    const struct VTable* vtable_;
    char name_[50];
    int  age_;
} Person;

typedef struct VTable {
    void (*print_)(void* self);
    void (*greet_)(void* self);
    void (*destroy_)(void* self);
} VTable;

/* ---------- 派生类 Student ---------- */
typedef struct {
    Person base_;
    char   school_[50];
} Student;

/* ---------- 派生类 Teacher ---------- */
typedef struct {
    Person base_;
    char   subject_[50];
    int    years_;
} Teacher;

/* ---------- 虚函数实现 ---------- */
void Person_print_impl(void* self) {
    Person* p = (Person*)self;
    printf(&quot;[Person] 姓名：%s，年龄：%d\n&quot;, p-&amp;gt;name_, p-&amp;gt;age_);
}
void Person_greet_impl(void* self) {
    Person* p = (Person*)self;
    printf(&quot;[Person] 你好，我是%s！\n&quot;, p-&amp;gt;name_);
}
void Person_destroy_impl(void* self) {
    free(self);
}

void Student_print_impl(void* self) {
    Student* s = (Student*)self;
    printf(&quot;[Student] 姓名：%s，年龄：%d，学校：%s\n&quot;,
           s-&amp;gt;base_.name_, s-&amp;gt;base_.age_, s-&amp;gt;school_);
}
void Student_greet_impl(void* self) {
    Student* s = (Student*)self;
    printf(&quot;[Student] 你好，我是%s，来自%s！\n&quot;,
           s-&amp;gt;base_.name_, s-&amp;gt;school_);
}
void Student_destroy_impl(void* self) {
    free(self);
}

void Teacher_print_impl(void* self) {
    Teacher* t = (Teacher*)self;
    printf(&quot;[Teacher] 姓名：%s，年龄：%d，科目：%s\n&quot;,
           t-&amp;gt;base_.name_, t-&amp;gt;base_.age_, t-&amp;gt;subject_);
}
void Teacher_greet_impl(void* self) {
    Teacher* t = (Teacher*)self;
    printf(&quot;[Teacher] 同学们好，我是%s老师，教%s。\n&quot;, t-&amp;gt;base_.name_, t-&amp;gt;subject_);
}
void Teacher_destroy_impl(void* self) {
    free(self);
}

/* ---------- 静态虚表 ---------- */
static const VTable Person_vtable = {
    .print_   = Person_print_impl,
    .greet_   = Person_greet_impl,
    .destroy_ = Person_destroy_impl
};

static const VTable Student_vtable = {
    .print_   = Student_print_impl,
    .greet_   = Student_greet_impl,
    .destroy_ = Student_destroy_impl
};

static const VTable Teacher_vtable = {
    .print_   = Teacher_print_impl,
    .greet_   = Teacher_greet_impl,
    .destroy_ = Teacher_destroy_impl
};

/* ---------- 构造函数 ---------- */
Person* Person_newWith(const char* name, int age) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (!self) return NULL;
    self-&amp;gt;vtable_ = &amp;amp;Person_vtable;
    self-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}

Student* Student_newWith(const char* name, int age, const char* school) {
    Student* self = (Student*)malloc(sizeof(Student));
    if (!self) return NULL;
    self-&amp;gt;base_.vtable_ = &amp;amp;Student_vtable;
    self-&amp;gt;base_.age_ = age;
    strncpy(self-&amp;gt;base_.name_, name, sizeof(self-&amp;gt;base_.name_) - 1);
    self-&amp;gt;base_.name_[sizeof(self-&amp;gt;base_.name_) - 1] = &apos;\0&apos;;
    strncpy(self-&amp;gt;school_, school, sizeof(self-&amp;gt;school_) - 1);
    self-&amp;gt;school_[sizeof(self-&amp;gt;school_) - 1] = &apos;\0&apos;;
    return self;
}

Teacher* Teacher_newWith(const char* name, int age,
                         const char* subject, int years) {
    Teacher* self = (Teacher*)malloc(sizeof(Teacher));
    if (!self) return NULL;
    self-&amp;gt;base_.vtable_ = &amp;amp;Teacher_vtable;
    self-&amp;gt;base_.age_ = age;
    strncpy(self-&amp;gt;base_.name_, name, sizeof(self-&amp;gt;base_.name_) - 1);
    self-&amp;gt;base_.name_[sizeof(self-&amp;gt;base_.name_) - 1] = &apos;\0&apos;;
    strncpy(self-&amp;gt;subject_, subject, sizeof(self-&amp;gt;subject_) - 1);
    self-&amp;gt;subject_[sizeof(self-&amp;gt;subject_) - 1] = &apos;\0&apos;;
    self-&amp;gt;years_ = years;
    return self;
}

/* ---------- 安全调用宏 ---------- */
#define SAFE_CALL(obj, func) \
    do { \
        Person* _self_ = (Person*)(obj); \
        if ((_self_) &amp;amp;&amp;amp; (_self_)-&amp;gt;vtable_ &amp;amp;&amp;amp; (_self_)-&amp;gt;vtable_-&amp;gt;func) { \
            (_self_)-&amp;gt;vtable_-&amp;gt;func(_self_); \
        } \
    } while(0)

/* ---------- 测试入口 ---------- */
int main(void) {
    /* 0. 多派生类内存布局验证 */
    Student* stu = Student_newWith(&quot;Bob&quot;, 20, &quot;XYZ University&quot;);
    Teacher* tch = Teacher_newWith(&quot;Alice&quot;, 35, &quot;Physics&quot;, 10);

    printf(&quot;=== Student 内存布局 ===\n&quot;);
    printf(&quot;Student*    = %p\n&quot;, (void*)stu);
    printf(&quot;&amp;amp;base_      = %p\n&quot;, (void*)&amp;amp;stu-&amp;gt;base_);
    printf(&quot;&amp;amp;school_    = %p\n&quot;, (void*)&amp;amp;stu-&amp;gt;school_);

    printf(&quot;\n=== Teacher 内存布局 ===\n&quot;);
    printf(&quot;Teacher*    = %p\n&quot;, (void*)tch);
    printf(&quot;&amp;amp;base_      = %p\n&quot;, (void*)&amp;amp;tch-&amp;gt;base_);
    printf(&quot;&amp;amp;subject_   = %p\n&quot;, (void*)&amp;amp;tch-&amp;gt;subject_);
    printf(&quot;&amp;amp;years_     = %p\n&quot;, (void*)&amp;amp;tch-&amp;gt;years_);

    Person* ps = (Person*)stu;
    Person* pt = (Person*)tch;
    printf(&quot;\n(Person*)Student = %p  ( == Student* )\n&quot;, (void*)ps);
    printf(&quot;(Person*)Teacher = %p  ( == Teacher* )\n&quot;, (void*)pt);

    /* 1. 向上转型与多态调用 */
    printf(&quot;\n=== 向上转型与多态 ===\n&quot;);
    SAFE_CALL(ps, print_);   // Student 版本
    SAFE_CALL(pt, print_);   // Teacher 版本

    /* 2. 向下转型（container_of） */
    Student* recovered = container_of(ps, Student, base_);
    printf(&quot;\n=== 向下转型（container_of） ===\n&quot;);
    printf(&quot;学校：%s\n&quot;, recovered-&amp;gt;school_);
    printf(&quot;stu == recovered : %d\n&quot;, (void*)stu == (void*)recovered);

    /* 3. 销毁 */
    stu-&amp;gt;base_.vtable_-&amp;gt;destroy_(stu);
    tch-&amp;gt;base_.vtable_-&amp;gt;destroy_(tch);

    /* 4. 虚函数防御测试 */
    printf(&quot;\n=== 虚函数防御测试 ===\n&quot;);
    SAFE_CALL(NULL, print_);        // 传入 NULL，宏安全跳过
    ps = NULL;
    SAFE_CALL(ps, print_);          // ps 置空后也安全跳过

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== Student 内存布局 ===
Student*    = 0x55a7c2d4b2a0
&amp;amp;base_      = 0x55a7c2d4b2a0
&amp;amp;school_    = 0x55a7c2d4b2d8

=== Teacher 内存布局 ===
Teacher*    = 0x55a7c2d4b320
&amp;amp;base_      = 0x55a7c2d4b320
&amp;amp;subject_   = 0x55a7c2d4b358
&amp;amp;years_     = 0x55a7c2d4b38c

(Person*)Student = 0x55a7c2d4b2a0  ( == Student* )
(Person*)Teacher = 0x55a7c2d4b320  ( == Teacher* )

=== 向上转型与多态 ===
[Student] 姓名：Bob，年龄：20，学校：XYZ University
[Teacher] 姓名：Alice，年龄：35，科目：Physics

=== 向下转型（container_of） ===
学校：XYZ University
stu == recovered : 1

=== 虚函数防御测试 ===
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（防御测试部分无任何输出，证明空指针被安全跳过，未引发崩溃。）&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向上转型&lt;/strong&gt;由基类首成员规则保证，多个派生类内存布局的实例证明：只要基类在首位，向上转型地址完全一致，多态接口可无缝工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向下转型&lt;/strong&gt;通过 &lt;code&gt;container_of&lt;/code&gt; 宏实现，它基于 &lt;code&gt;offsetof&lt;/code&gt; 在编译期计算偏移量，反推外层结构体指针，精巧高效（源自 Linux 内核）。但转型安全性完全由程序员保证，来源错误将导致未定义行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚函数防御&lt;/strong&gt;借助修正后的 &lt;code&gt;SAFE_CALL&lt;/code&gt; 宏（先转为 &lt;code&gt;Person*&lt;/code&gt; 再检查）和构造/销毁规范，系统性消除了空指针与悬垂指针调用的风险。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C语言实现面向对象编程（OOP）基础教程</title><link>https://www.daitcc.top/posts/0134f848/</link><guid isPermaLink="true">https://www.daitcc.top/posts/0134f848/</guid><description>从零开始，用纯 C 语言逐步实现封装、成员函数、this 指针、信息隐藏、继承与多态等面向对象核心机制。全文六章，每节提供可编译运行的完整代码、逐段讲解和避坑要点，带你从结构体数据封装一直深入到虚函数表和多文件工程架构，深入理解 OOP 底层原理。</description><pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C语言实现面向对象编程（OOP）基础教程&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本教程面向已掌握C语言基础的读者，希望通过C语言自身机制来理解面向对象编程（OOP）的一些底层实现思路。
我们依次模拟封装、成员函数、this指针、信息隐藏、继承与多态等常见特性，纯用C逐步构建，不求面面俱到，但求每一步都清晰可运行。
全文共六章，每节配有完整的示例代码、分段讲解以及实践中容易踩的坑，希望能为你的OOP学习提供一个参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;::github{repo=&quot;daitcl/oop&quot;}&lt;/p&gt;
&lt;h2&gt;一、封装成员变量&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;抽象&lt;/strong&gt;：从具体事物中提取共同特征（&lt;strong&gt;属性&lt;/strong&gt;）和共同行为。&lt;br /&gt;
&lt;strong&gt;封装&lt;/strong&gt;：将抽象出的数据（&lt;strong&gt;成员变量&lt;/strong&gt;）组合成一个整体——结构体（struct）。&lt;/p&gt;
&lt;p&gt;在C语言中，结构体支持数据封装。根据&lt;strong&gt;抽象&lt;/strong&gt;的定义我们通过构建一个 &lt;strong&gt;Person&lt;/strong&gt; 类型的结构体，其中包含&lt;strong&gt;年龄&lt;/strong&gt;和&lt;strong&gt;姓名&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1.1 结构体定义与构造函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 结构体：Person，表示人的基本信息 */
typedef struct Person {
    int age_;           /**&amp;lt; 年龄 */
    char name_[50];     /**&amp;lt; 姓名（最多 49 个有效字符 + &apos;\0&apos;） */
} Person;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;typedef struct Person {...} Person;&lt;/code&gt;，我们将年龄和姓名这两个属性封装成一个新的数据类型，完成了&lt;strong&gt;数据层面的封装&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了让使用者更方便、更安全地创建和初始化 &lt;code&gt;Person&lt;/code&gt; 对象，我们为其提供专门的&lt;strong&gt;构造函数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @brief 在堆上创建 Person 对象（无参构造）
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) {
        return NULL;    /* 内存分配失败 */
    }

    /* 初始化为安全的默认值 */
    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;
    return self;
}

/**
 * @brief 在堆上创建 Person 对象，并用指定参数初始化
 * @param age  年龄
 * @param name 姓名（C 字符串）
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();   /* 复用无参构造 */
    if (self == NULL) {
        return NULL;
    }

    self-&amp;gt;age_ = age;

    /* 安全复制姓名，防止缓冲区溢出 */
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;   /* 确保以 &apos;\0&apos; 结尾 */
    return self;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：这两个函数的返回值类型都是 &lt;code&gt;Person*&lt;/code&gt;，也就是说它们会返回新构造出来的对象的指针，供外部使用。这也是一种模拟“构造函数返回对象”的惯用写法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建对象时，我们可以选择两种不同的方式——&lt;strong&gt;栈上直接初始化&lt;/strong&gt;和&lt;strong&gt;堆上动态构造&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    /* ---------- 方式 1：栈上对象 ---------- */
    Person p1 = {18, &quot;张三&quot;};       /* 直接初始化 */
    /* ---------- 方式 2：堆上对象 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        free(p2);          /* 必须手动释放，防止内存泄漏 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;选择不同的构造方式，决定了对象创建的位置（栈/堆），进而影响了其生命周期管理方式。按需选取即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;栈上对象 &lt;code&gt;p1&lt;/code&gt;&lt;/strong&gt;：随函数调用自动创建，作用域结束时自动销毁，无需手动干预，适合生命周期短、大小确定的对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆上对象 &lt;code&gt;p2&lt;/code&gt;&lt;/strong&gt;：通过 &lt;code&gt;Person_newWith&lt;/code&gt;（内部调用 &lt;code&gt;malloc&lt;/code&gt;）在堆上分配，生命周期可控，必须显式 &lt;code&gt;free&lt;/code&gt; 释放，适合需要跨函数传递或大小动态变化的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们为 &lt;code&gt;Person&lt;/code&gt; 添加两个操作数据的函数，以此演示如何通过指针来使用封装好的成员。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @brief 打印 Person 对象的信息
 * @param self 指向 Person 对象的指针
 */
void Person_print(Person* self) {
    if (self != NULL) {
        printf(&quot;年龄：%d，姓名：%s\n&quot;, self-&amp;gt;age_, self-&amp;gt;name_);
    }
}

/**
 * @brief 让 Person 对象打招呼
 * @param self 指向 Person 对象的指针
 */
void Person_greet(Person* self) {
    if (self != NULL) {
        printf(&quot;%s说：你好！\n&quot;, self-&amp;gt;name_);
    }
}

/**
 * @brief 销毁堆上创建的 Person 对象（释放内存）
 * @param self 指向待销毁对象的指针
 */
void Person_delete(Person* self) {
    free(self);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这些函数里，我们显式地传入 &lt;code&gt;Person* self&lt;/code&gt; 指针来读取和操作 &lt;code&gt;Person&lt;/code&gt; 中的数据。这种写法类似于 C++ 成员函数背后隐藏的 &lt;code&gt;this&lt;/code&gt; 指针，也是 C 语言模拟面向对象行为的基础。&lt;/p&gt;
&lt;p&gt;调用示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    /* ---------- 方式 1：栈上对象 ---------- */
    Person p1 = {18, &quot;张三&quot;};       /* 直接初始化 */
    Person_print(&amp;amp;p1);              /* 传递栈对象的地址 */

    /* ---------- 方式 2：堆上对象 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        Person_print(p2);
        Person_delete(p2);          /* 必须手动释放，防止内存泄漏 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;年龄：18，姓名：张三
年龄：20，姓名：李四
李四说：你好！
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 要点小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;typedef struct&lt;/code&gt; 将属性打包，实现数据封装。&lt;/li&gt;
&lt;li&gt;函数统一接收 &lt;code&gt;Person* self&lt;/code&gt; 指针操作对象（模拟 &lt;code&gt;this&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;堆对象必须 &lt;code&gt;Person_delete&lt;/code&gt;，栈对象自动回收。&lt;/li&gt;
&lt;li&gt;始终用 &lt;code&gt;strncpy&lt;/code&gt; + 手动置 &lt;code&gt;&apos;\0&apos;&lt;/code&gt; 防止缓冲区溢出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 完整代码如下&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test1.c
 * 最简单的 C 语言结构体用法，数据与函数分离。
 * 演示栈对象和堆对象的创建、使用与销毁。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 结构体：Person，表示人的基本信息 */
typedef struct Person {
    int age_;           /**&amp;lt; 年龄 */
    char name_[50];     /**&amp;lt; 姓名（最多 49 个有效字符 + &apos;\0&apos;） */
} Person;

/**
 * @brief 在堆上创建 Person 对象（无参构造）
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) {
        return NULL;    /* 内存分配失败 */
    }

    /* 初始化为安全的默认值 */
    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;
    return self;
}

/**
 * @brief 在堆上创建 Person 对象，并用指定参数初始化
 * @param age  年龄
 * @param name 姓名（C 字符串）
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();   /* 复用无参构造 */
    if (self == NULL) {
        return NULL;
    }

    self-&amp;gt;age_ = age;

    /* 安全复制姓名，防止缓冲区溢出 */
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;   /* 确保以 &apos;\0&apos; 结尾 */
    return self;
}

/**
 * @brief 打印 Person 对象的信息
 * @param self 指向 Person 对象的指针
 */
void Person_print(Person* self) {
    if (self != NULL) {
        printf(&quot;年龄：%d，姓名：%s\n&quot;, self-&amp;gt;age_, self-&amp;gt;name_);
    }
}

/**
 * @brief 让 Person 对象打招呼
 * @param self 指向 Person 对象的指针
 */
void Person_greet(Person* self) {
    if (self != NULL) {
        printf(&quot;%s说：你好！\n&quot;, self-&amp;gt;name_);
    }
}

/**
 * @brief 销毁堆上创建的 Person 对象（释放内存）
 * @param self 指向待销毁对象的指针
 */
void Person_delete(Person* self) {
    free(self);
}

int main(void) {
    /* ---------- 方式 1：栈上对象 ---------- */
    /* 这种写法（聚合初始化）要求初始化值的类型、顺序与结构体成员完全一致，且字符串字面量不能超过数组长度。 */
    Person p1 = {18, &quot;张三&quot;};       /* 直接初始化 */
    Person_print(&amp;amp;p1);              /* 传递栈对象的地址 */

    /* ---------- 方式 2：堆上对象 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        Person_print(p2);
        Person_delete(p2);          /* 必须手动释放，防止内存泄漏 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、封装成员函数&lt;/h2&gt;
&lt;p&gt;在C语言中，函数是全局的。为了让结构体实现“自带”行为，可以在结构体中存放&lt;strong&gt;函数指针&lt;/strong&gt;，这些指针指向具体的操作函数。调用时通过 &lt;code&gt;对象.函数指针(参数)&lt;/code&gt; 的语法，模拟C++的成员函数。&lt;/p&gt;
&lt;h3&gt;2.1 修改结构体，加入函数指针&lt;/h3&gt;
&lt;p&gt;首先，我们在 &lt;code&gt;Person&lt;/code&gt; 结构体中增加函数指针成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test2.c
 * 在结构体中添加函数指针成员，模拟 C++ 的成员函数。
 * 每个对象单独存储函数指针（内存开销较大），通过指针调用实现行为绑定。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 前向声明，便于结构体内使用自身类型指针 */
typedef struct Person Person;

/**
 * 结构体：Person
 * 包含数据成员（age_, name_）和函数指针成员（print_, sayHello_, destroy_）。
 */
struct Person {
    int age_;               /**&amp;lt; 年龄 */
    char name_[50];         /**&amp;lt; 姓名 */

    /* 函数指针成员（模拟成员函数） */
    void (*print_)(Person* self);       /**&amp;lt; 打印信息 */
    void (*greet_)(Person* self);    /**&amp;lt; 打招呼 */
    void (*destroy_)(Person* self);     /**&amp;lt; 销毁对象（仅用于堆对象） */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：因为这些函数指针的参数类型为 &lt;code&gt;Person*&lt;/code&gt;，所以需要先用 &lt;code&gt;typedef struct Person Person;&lt;/code&gt; 进行前向声明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来，我们需要在&lt;strong&gt;构造函数&lt;/strong&gt;里完成函数指针与具体函数的绑定，这样通过 &lt;code&gt;对象.函数指针(参数)&lt;/code&gt; 调用时才知道执行哪个函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @brief 在堆上创建 Person 对象（无参构造）
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;

    /* 初始化数据成员 */
    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;

    /* 绑定函数指针（每个对象独立存储一份） */
    self-&amp;gt;print_    = Person_print_impl;
    self-&amp;gt;greet_ = Person_greet_impl;
    self-&amp;gt;destroy_  = Person_destroy_impl;

    return self;
}

/**
 * @brief 在堆上创建 Person 对象，并用指定参数初始化
 * @param age  年龄
 * @param name 姓名
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();
    if (self == NULL) return NULL;

    self-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由此，我们就可以写出使用函数指针的 &lt;code&gt;main&lt;/code&gt; 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    /* ---------- 栈上对象：必须手动初始化函数指针 ---------- */
    Person p1;                     /* 无法再用 {18, &quot;张三&quot;} 聚合初始化，因为结构体含函数指针 */
    p1.age_ = 18;
    strncpy(p1.name_, &quot;张三&quot;, sizeof(p1.name_) - 1);
    p1.name_[sizeof(p1.name_) - 1] = &apos;\0&apos;;

    /* 手动绑定函数指针（若未绑定直接调用会段错误） */
    p1.print_   = Person_print_impl;
    p1.greet_   = Person_greet_impl;
    p1.destroy_ = NULL;            /* 栈对象绝不可调用 destroy，设为 NULL 更安全 */

    p1.print_(&amp;amp;p1);                /* 通过函数指针调用，显式传入 &amp;amp;p1 */

    /* ---------- 堆上对象：构造函数已自动绑定 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        p2-&amp;gt;print_(p2);
        p2-&amp;gt;greet_(p2);            /* 可以调用多个行为函数 */
        p2-&amp;gt;destroy_(p2);          /* 释放堆内存 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;年龄：18，姓名：张三
年龄：20，姓名：李四
李四说：你好！
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 要点小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数指针成员&lt;/strong&gt;：&lt;code&gt;void (*print_)(Person* self);&lt;/code&gt; 声明了一个指向函数的指针，该函数接受 &lt;code&gt;Person*&lt;/code&gt; 且无返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;绑定&lt;/strong&gt;：在构造函数中将全局函数地址赋给指针成员，对象便“拥有”了行为。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;调用&lt;/strong&gt;：&lt;code&gt;p-&amp;gt;print_(p)&lt;/code&gt; 需要显式传入 &lt;code&gt;p&lt;/code&gt; 自身，因为函数指针无法自动获取 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;栈对象注意事项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因结构体含函数指针，不便使用聚合初始化，需逐成员赋值。&lt;/li&gt;
&lt;li&gt;函数指针必须&lt;strong&gt;手动绑定&lt;/strong&gt;，否则调用会崩溃。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destroy_&lt;/code&gt; 应设为 &lt;code&gt;NULL&lt;/code&gt;，严禁对栈对象执行 &lt;code&gt;free&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 完整代码&lt;/h3&gt;
&lt;p&gt;（此处放置完整的、修正后的 &lt;code&gt;test2.c&lt;/code&gt; 代码，与上面展示的一致，包括函数实现的原型声明和 &lt;code&gt;_impl&lt;/code&gt; 函数的定义。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test2.c
 * 在结构体中添加函数指针成员，模拟 C++ 的成员函数。
 * 每个对象单独存储函数指针（内存开销较大），通过指针调用实现行为绑定。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 前向声明，便于结构体内使用自身类型指针 */
typedef struct Person Person;

/**
 * 结构体：Person
 * 包含数据成员（age_, name_）和函数指针成员（print_, greet_, destroy_）。
 */
struct Person {
    int age_;               /**&amp;lt; 年龄 */
    char name_[50];         /**&amp;lt; 姓名 */

    /* 函数指针成员（模拟成员函数） */
    void (*print_)(Person* self);       /**&amp;lt; 打印信息 */
    void (*greet_)(Person* self);    /**&amp;lt; 打招呼 */
    void (*destroy_)(Person* self);     /**&amp;lt; 销毁对象（仅用于堆对象） */
};

/* 函数实现的原型声明 */
void Person_print_impl(Person* self);
void Person_greet_impl(Person* self);
void Person_destroy_impl(Person* self);

/**
 * @brief 在堆上创建 Person 对象（无参构造）
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;

    /* 初始化数据成员 */
    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;

    /* 绑定函数指针（每个对象独立存储一份） */
    self-&amp;gt;print_    = Person_print_impl;
    self-&amp;gt;greet_ = Person_greet_impl;
    self-&amp;gt;destroy_  = Person_destroy_impl;

    return self;
}

/**
 * @brief 在堆上创建 Person 对象，并用指定参数初始化
 * @param age  年龄
 * @param name 姓名
 * @return 成功返回指向 Person 的指针，失败返回 NULL
 */
Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();
    if (self == NULL) return NULL;

    self-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}

/**
 * @brief 打印 Person 对象信息（函数指针绑定的实现）
 * @param self 指向当前对象的指针
 */
void Person_print_impl(Person* self) {
    if (self != NULL) {
        printf(&quot;年龄：%d，姓名：%s\n&quot;, self-&amp;gt;age_, self-&amp;gt;name_);
    }
}

/**
 * @brief 打招呼的实现
 */
void Person_greet_impl(Person* self) {
    if (self != NULL) {
        printf(&quot;%s说：你好！\n&quot;, self-&amp;gt;name_);
    }
}

/**
 * @brief 销毁堆对象的实现
 */
void Person_destroy_impl(Person* self) {
    free(self);
}

int main(void) {
    /* ---------- 栈上对象：必须手动初始化函数指针 ---------- */
    Person p1;
    p1.age_ = 18;
    strncpy(p1.name_, &quot;张三&quot;, sizeof(p1.name_) - 1);
	p1.name_[sizeof(p1.name_) - 1] = &apos;\0&apos;;

    /* 绑定函数指针（若未绑定直接调用会导致段错误） */
    p1.print_    = Person_print_impl;
    p1.greet_ = Person_greet_impl;
    p1.destroy_  = Person_destroy_impl;   /* 栈对象通常不应调用 destroy_ */

    p1.print_(&amp;amp;p1);   /* 通过函数指针调用，需显式传入对象自身 */

    /* ---------- 堆上对象：构造函数已自动绑定 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        p2-&amp;gt;print_(p2);
        p2-&amp;gt;destroy_(p2);   /* 释放堆内存 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、隐藏this指针（仅供学习）&lt;/h2&gt;
&lt;p&gt;C++ 的成员函数可以隐式获取 &lt;code&gt;this&lt;/code&gt; 指针，调用时无需显式传递对象自身。在 C 语言中，我们可以用一个&lt;strong&gt;全局变量&lt;/strong&gt;来模拟这种机制：在调用成员函数前，先把当前对象的地址存入一个全局指针，函数内部直接访问该全局指针即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;警告&lt;/strong&gt;：这种方法&lt;strong&gt;非线程安全&lt;/strong&gt;，且极易因忘记设置全局指针而出错。本节仅用于帮助理解 &lt;code&gt;this&lt;/code&gt; 的工作原理，&lt;strong&gt;实际项目中切勿使用&lt;/strong&gt;。工程上的安全做法会在第五章介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.1 引入全局 this 指针&lt;/h3&gt;
&lt;p&gt;先把 &lt;code&gt;Person&lt;/code&gt; 中的函数指针改为无参形式，因为它们将依靠全局指针来获取操作对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

typedef struct Person Person;

struct Person {
    int age_;
    char name_[50];

    /* 函数指针成员，均为无参函数（内部通过全局 person_this 访问对象） */
    void (*print_)(void);
    void (*greet_)(void);
    void (*destroy_)(void);
};

/* 全局 this 指针，指向当前正在操作的对象 */
Person* person_this = NULL;

/* 函数实现的原型声明 */
void Person_print_impl(void);
void Person_greet_impl(void);
void Person_destroy_impl(void);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码将 &lt;code&gt;print_&lt;/code&gt;、&lt;code&gt;greet_&lt;/code&gt; 和 &lt;code&gt;destroy_&lt;/code&gt; 的参数列表置为 &lt;code&gt;void&lt;/code&gt;，意味着调用时不再需要手动传入对象指针。对象本身通过&lt;strong&gt;全局变量 &lt;code&gt;person_this&lt;/code&gt;&lt;/strong&gt; 来定位，这就是对 &lt;code&gt;this&lt;/code&gt; 指针的模拟。&lt;/p&gt;
&lt;p&gt;此外，我们定义一个宏 &lt;code&gt;PERSON_CAST&lt;/code&gt;，用来在调用前方便地设置 &lt;code&gt;person_this&lt;/code&gt;，同时返回对象指针，以便支持链式调用风格。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 宏：PERSON_CAST
 * 将全局 person_this 设为指定对象，并返回该对象指针。
 * 用法：PERSON_CAST(p2)-&amp;gt;print_();
 */
#define PERSON_CAST(object) (person_this = (object))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造函数同样需要适配：在创建对象并绑定函数指针时，自动将 &lt;code&gt;person_this&lt;/code&gt; 指向新对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @brief 堆上创建 Person 对象（无参构造）
 */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;

    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;

    /* 绑定无参函数指针 */
    self-&amp;gt;print_   = Person_print_impl;
    self-&amp;gt;greet_   = Person_greet_impl;
    self-&amp;gt;destroy_ = Person_destroy_impl;

    person_this = self;   /* 使新对象成为当前操作对象 */
    return self;
}

/**
 * @brief 堆上创建 Person 对象（带参构造）
 */
Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();
    if (self == NULL) return NULL;

    self-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数实现内部直接使用 &lt;code&gt;person_this&lt;/code&gt; 来访问成员，不再需要 &lt;code&gt;self&lt;/code&gt; 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Person_print_impl(void) {
    if (person_this != NULL) {
        printf(&quot;年龄：%d，姓名：%s\n&quot;, person_this-&amp;gt;age_, person_this-&amp;gt;name_);
    }
}

void Person_greet_impl(void) {
    if (person_this != NULL) {
        printf(&quot;%s说：你好！\n&quot;, person_this-&amp;gt;name_);
    }
}

void Person_destroy_impl(void) {
    free(person_this);
    /* 注意：这里未将 person_this 置为 NULL，外部需自行管理 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    /* ---------- 栈上对象 ---------- */
    Person p1;
    p1.age_ = 18;
    strncpy(p1.name_, &quot;张三&quot;, sizeof(p1.name_) - 1);
    p1.name_[sizeof(p1.name_) - 1] = &apos;\0&apos;;

    /* 手动绑定函数指针 */
    p1.print_   = Person_print_impl;
    p1.greet_   = Person_greet_impl;
    p1.destroy_ = NULL;                    /* 栈对象绝不调用 destroy_ */

    person_this = &amp;amp;p1;                     /* 设置当前操作对象 */
    p1.print_();                           /* 无需传参，直接调用 */

    /* ---------- 堆上对象 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        PERSON_CAST(p2)-&amp;gt;print_();         /* 通过宏设置 this 并调用 */
        PERSON_CAST(p2)-&amp;gt;greet_();         /* 另一个行为 */
        PERSON_CAST(p2)-&amp;gt;destroy_();       /* 释放堆内存 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;年龄：18，姓名：张三
年龄：20，姓名：李四
李四说：你好！
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 要点小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全局 &lt;code&gt;person_this&lt;/code&gt;&lt;/strong&gt;：扮演了 &lt;code&gt;this&lt;/code&gt; 指针的角色，函数内部通过它找到当前对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无参函数指针&lt;/strong&gt;：&lt;code&gt;void (*print_)(void)&lt;/code&gt; 等不再需要显式传入 &lt;code&gt;Person*&lt;/code&gt;，调用更接近 C++ 的语法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PERSON_CAST&lt;/code&gt; 宏&lt;/strong&gt;：一行代码完成“设置当前对象 + 返回对象指针”，模拟 &lt;code&gt;obj-&amp;gt;method()&lt;/code&gt; 的连贯写法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈对象&lt;/strong&gt;：仍需手动绑定函数指针，且使用前必须将 &lt;code&gt;person_this&lt;/code&gt; 指向该对象。切勿调用 &lt;code&gt;destroy_&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;严重局限&lt;/strong&gt;：任何时刻只有一个 &lt;code&gt;person_this&lt;/code&gt;，嵌套调用或多线程下会完全混乱，详见下一节。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 缺陷一览&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;strong&gt;非线程安全&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;多个线程同时修改全局 &lt;code&gt;person_this&lt;/code&gt;，会导致数据错乱或崩溃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;不支持重入&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在一个成员函数内部调用另一个对象的成员函数时，&lt;code&gt;person_this&lt;/code&gt; 会被覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;极易忘记设置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;栈对象若忘记写 &lt;code&gt;person_this = &amp;amp;p1;&lt;/code&gt;，调用行为函数会操作空指针或错误对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;销毁后悬空&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;destroy_&lt;/code&gt; 释放内存后 &lt;code&gt;person_this&lt;/code&gt; 未置 &lt;code&gt;NULL&lt;/code&gt;，后续误用造成悬垂指针&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;请牢记：本方案&lt;strong&gt;仅用于教学演示&lt;/strong&gt;，帮助理解 C++ 中 &lt;code&gt;this&lt;/code&gt; 的幕后机制。实际开发中请直接跳至第5章的工程化实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.4 完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test3.c
 * 引入全局指针 person_this 模拟 C++ 的 this 指针，使成员函数无需显式传参。
 * 仅供理解原理，非线程安全，切勿在工程中使用。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

typedef struct Person Person;

struct Person {
    int age_;
    char name_[50];

    /* 函数指针成员，均为无参函数（依赖全局 person_this） */
    void (*print_)(void);
    void (*greet_)(void);
    void (*destroy_)(void);
};

/* 全局 this 指针，指向当前操作的对象 */
Person* person_this = NULL;

/**
 * 宏：将全局 person_this 设置为指定对象，并返回该对象指针，支持链式调用。
 */
#define PERSON_CAST(object) (person_this = (object))

/* 函数原型 */
void Person_print_impl(void);
void Person_greet_impl(void);
void Person_destroy_impl(void);

Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;

    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;

    self-&amp;gt;print_   = Person_print_impl;
    self-&amp;gt;greet_   = Person_greet_impl;
    self-&amp;gt;destroy_ = Person_destroy_impl;

    person_this = self;
    return self;
}

Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();
    if (self == NULL) return NULL;

    self-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}

void Person_print_impl(void) {
    if (person_this != NULL) {
        printf(&quot;年龄：%d，姓名：%s\n&quot;, person_this-&amp;gt;age_, person_this-&amp;gt;name_);
    }
}

void Person_greet_impl(void) {
    if (person_this != NULL) {
        printf(&quot;%s说：你好！\n&quot;, person_this-&amp;gt;name_);
    }
}

void Person_destroy_impl(void) {
    free(person_this);
}

int main(void) {
    /* 栈上对象 */
    Person p1;
    p1.age_ = 18;
    strncpy(p1.name_, &quot;张三&quot;, sizeof(p1.name_) - 1);
    p1.name_[sizeof(p1.name_) - 1] = &apos;\0&apos;;

    p1.print_   = Person_print_impl;
    p1.greet_   = Person_greet_impl;
    p1.destroy_ = NULL;                     /* 栈对象严禁释放 */

    person_this = &amp;amp;p1;
    p1.print_();

    /* 堆上对象 */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        PERSON_CAST(p2)-&amp;gt;print_();
        PERSON_CAST(p2)-&amp;gt;greet_();
        PERSON_CAST(p2)-&amp;gt;destroy_();       /* 释放堆内存 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、隐藏成员变量&lt;/h2&gt;
&lt;p&gt;C语言的结构体所有成员默认都是公有的。若想实现&lt;strong&gt;私有成员&lt;/strong&gt;（外部代码无法直接访问），可使用&lt;strong&gt;不透明指针&lt;/strong&gt;技术：在公开接口中只暴露一个 &lt;code&gt;void*&lt;/code&gt; 指针，真正的数据定义在源文件内部，外部完全看不到细节。&lt;/p&gt;
&lt;h3&gt;4.1 使用私有数据结构&lt;/h3&gt;
&lt;p&gt;将真正的年龄、姓名放入一个独立的 &lt;code&gt;PersonPrivate&lt;/code&gt; 结构体，对外只暴露 &lt;code&gt;void* private_&lt;/code&gt; 指针。同时，函数指针依然使用全局 &lt;code&gt;person_this&lt;/code&gt;，不传 &lt;code&gt;self&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 前向声明 */
typedef struct Person Person;

/* 私有数据结构体（定义在 .c 文件内部，此处为演示写在同一文件） */
typedef struct PersonPrivate {
    int age_;
    char name_[50];
} PersonPrivate;

/* 公有接口结构体 */
struct Person {
    void* private_;                     /* 指向私有数据的指针（不透明） */

    /* 无参函数指针，依赖全局 person_this */
    void (*print_)(void);
    void (*greet_)(void);
    void (*destroy_)(void);
    void (*setAge_)(int age);
    int  (*getAge_)(void);
    const char* (*getName_)(void);
    void (*setName_)(const char* name);
};

/* 全局 this 指针（线程不安全，仅演示用） */
Person* person_this = NULL;

/* 设置当前对象并支持链式调用的宏 */
#define PERSON_CAST(object) (person_this = (object))

/* 函数原型 */
void Person_print_impl(void);
void Person_greet_impl(void);
void Person_destroy_impl(void);
void Person_setAge_impl(int age);
int  Person_getAge_impl(void);
const char* Person_getName_impl(void);
void Person_setName_impl(const char* name);

Person* Person_new(void);
Person* Person_newWith(int age, const char* name);

/* ---- 私有数据的分配与释放 ---- */
static PersonPrivate* PersonPrivate_new(void) {
    PersonPrivate* priv = (PersonPrivate*)malloc(sizeof(PersonPrivate));
    if (priv != NULL) {
        priv-&amp;gt;age_ = 0;
        priv-&amp;gt;name_[0] = &apos;\0&apos;;
    }
    return priv;
}

static void PersonPrivate_delete(PersonPrivate* priv) {
    free(priv);
}

/* ---- 构造函数 ---- */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;

    self-&amp;gt;private_ = PersonPrivate_new();
    if (self-&amp;gt;private_ == NULL) {
        free(self);
        return NULL;
    }

    self-&amp;gt;print_   = Person_print_impl;
    self-&amp;gt;greet_   = Person_greet_impl;
    self-&amp;gt;destroy_ = Person_destroy_impl;
    self-&amp;gt;setAge_  = Person_setAge_impl;
    self-&amp;gt;getAge_  = Person_getAge_impl;
    self-&amp;gt;getName_ = Person_getName_impl;
    self-&amp;gt;setName_ = Person_setName_impl;

    person_this = self;   /* 自动设为当前对象 */
    return self;
}

Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();
    if (self == NULL) return NULL;

    PersonPrivate* priv = (PersonPrivate*)self-&amp;gt;private_;
    priv-&amp;gt;age_ = age;
    strncpy(priv-&amp;gt;name_, name, sizeof(priv-&amp;gt;name_) - 1);
    priv-&amp;gt;name_[sizeof(priv-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}

/* ---- 行为函数实现（通过全局 person_this 访问对象和私有数据） ---- */
void Person_print_impl(void) {
    if (person_this == NULL) return;
    PersonPrivate* priv = (PersonPrivate*)person_this-&amp;gt;private_;
    if (priv != NULL) {
        printf(&quot;年龄：%d，姓名：%s\n&quot;, priv-&amp;gt;age_, priv-&amp;gt;name_);
    }
}

void Person_greet_impl(void) {
    if (person_this == NULL) return;
    PersonPrivate* priv = (PersonPrivate*)person_this-&amp;gt;private_;
    if (priv != NULL) {
        printf(&quot;%s说：你好！\n&quot;, priv-&amp;gt;name_);
    }
}

void Person_destroy_impl(void) {
    if (person_this == NULL) return;
    /* 先释放私有数据，再释放外壳 */
    if (person_this-&amp;gt;private_ != NULL) {
        PersonPrivate_delete((PersonPrivate*)person_this-&amp;gt;private_);
        person_this-&amp;gt;private_ = NULL;
    }
    free(person_this);
    /* 注意：未将 person_this 置 NULL，外部调用后容易误用 */
}

void Person_setAge_impl(int age) {
    if (person_this == NULL) return;
    PersonPrivate* priv = (PersonPrivate*)person_this-&amp;gt;private_;
    if (priv != NULL) priv-&amp;gt;age_ = age;
}

int Person_getAge_impl(void) {
    if (person_this == NULL) return -1;
    PersonPrivate* priv = (PersonPrivate*)person_this-&amp;gt;private_;
    return priv ? priv-&amp;gt;age_ : -1;
}

const char* Person_getName_impl(void) {
    if (person_this == NULL) return NULL;
    PersonPrivate* priv = (PersonPrivate*)person_this-&amp;gt;private_;
    return priv ? priv-&amp;gt;name_ : NULL;
}

void Person_setName_impl(const char* name) {
    if (person_this == NULL || name == NULL) return;
    PersonPrivate* priv = (PersonPrivate*)person_this-&amp;gt;private_;
    if (priv != NULL) {
        strncpy(priv-&amp;gt;name_, name, sizeof(priv-&amp;gt;name_) - 1);
        priv-&amp;gt;name_[sizeof(priv-&amp;gt;name_) - 1] = &apos;\0&apos;;
    }
}

/* ---- 使用示例 ---- */
int main(void) {
    /* ---------- 栈上对象：手动设置私有数据和函数指针 ---------- */
    Person p1;
    p1.private_ = PersonPrivate_new();
    if (p1.private_ == NULL) return 1;

    p1.print_   = Person_print_impl;
    p1.greet_   = Person_greet_impl;
    p1.destroy_ = NULL;                     /* 栈对象绝不能调用 destroy_ */
    p1.setAge_  = Person_setAge_impl;
    p1.getAge_  = Person_getAge_impl;
    p1.getName_ = Person_getName_impl;
    p1.setName_ = Person_setName_impl;

    person_this = &amp;amp;p1;
    p1.setName_(&quot;张三&quot;);
    p1.setAge_(18);
    p1.print_();

    /* 释放私有数据（栈对象自身内存不释放） */
    PersonPrivate_delete((PersonPrivate*)p1.private_);
    p1.private_ = NULL;

    /* ---------- 堆上对象：构造函数自动绑定 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        PERSON_CAST(p2)-&amp;gt;print_();
        PERSON_CAST(p2)-&amp;gt;destroy_();   /* destroy_ 会释放私有数据和外壳 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;年龄：18，姓名：张三
年龄：20，姓名：李四
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 要点小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不透明指针 &lt;code&gt;void\* private_&lt;/code&gt;&lt;/strong&gt;：对外隐藏了 &lt;code&gt;PersonPrivate&lt;/code&gt; 的成员细节，外部代码无法直接访问年龄或姓名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有数据分配&lt;/strong&gt;：通过 &lt;code&gt;PersonPrivate_new()&lt;/code&gt; 在堆上分配，&lt;code&gt;Person&lt;/code&gt; 只是保存其地址。栈对象需手动调用分配与释放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局 &lt;code&gt;person_this&lt;/code&gt; 依然存在&lt;/strong&gt;：所有无参函数仍依赖它来定位当前对象，带来了第三章的全部局限性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口完整&lt;/strong&gt;：除打印和打招呼外，增加了 &lt;code&gt;setAge_&lt;/code&gt;、&lt;code&gt;getAge_&lt;/code&gt;、&lt;code&gt;getName_&lt;/code&gt;、&lt;code&gt;setName_&lt;/code&gt; 等访问器，模拟了完整的成员函数集。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 注意事项&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;全局 this 不安全&lt;/td&gt;
&lt;td&gt;非线程安全，且嵌套调用时 &lt;code&gt;person_this&lt;/code&gt; 会被覆盖，与第三章相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;栈对象初始化繁琐&lt;/td&gt;
&lt;td&gt;需要手动分配 &lt;code&gt;private_&lt;/code&gt;、逐个绑定函数指针，极易遗漏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;destroy 后悬空&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Person_destroy_impl&lt;/code&gt; 未将 &lt;code&gt;person_this&lt;/code&gt; 置 &lt;code&gt;NULL&lt;/code&gt;，可能导致悬垂指针&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;栈对象不宜 destroy&lt;/td&gt;
&lt;td&gt;栈对象外壳在栈上，&lt;code&gt;free&lt;/code&gt; 会导致崩溃，必须将 &lt;code&gt;destroy_&lt;/code&gt; 设为 &lt;code&gt;NULL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;本节展示了信息隐藏的基本手段。实际项目中，建议直接阅读第五章，它去掉了全局 &lt;code&gt;this&lt;/code&gt;，提供了栈/堆统一的安全初始化方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.4 完整代码&lt;/h3&gt;
&lt;p&gt;（上述代码合并即为完整可运行程序，不再单独赘列。注意编译时需包含 &lt;code&gt;stdio.h&lt;/code&gt;、&lt;code&gt;stdlib.h&lt;/code&gt;、&lt;code&gt;string.h&lt;/code&gt;。）&lt;/p&gt;
&lt;h2&gt;五、多文件与显式 this 参数（推荐）&lt;/h2&gt;
&lt;p&gt;综合前几章的探索，最安全、可维护、可工程化的方式是：&lt;strong&gt;显式传递 &lt;code&gt;self&lt;/code&gt; 指针&lt;/strong&gt;（彻底避免全局变量），结合&lt;strong&gt;多文件组织&lt;/strong&gt;，并支持&lt;strong&gt;栈对象和堆对象的统一初始化&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据成员通过不透明指针 &lt;code&gt;PersonPrivate*&lt;/code&gt; 隐藏，保护内部细节。&lt;/li&gt;
&lt;li&gt;所有函数显式接收 &lt;code&gt;Person* self&lt;/code&gt;，完全可重入，线程安全。&lt;/li&gt;
&lt;li&gt;栈对象使用 &lt;code&gt;Person_init&lt;/code&gt; / &lt;code&gt;Person_cleanup&lt;/code&gt;，堆对象使用 &lt;code&gt;Person_newWith&lt;/code&gt; / &lt;code&gt;destroy&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.1 头文件 person.h&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * person.h
 * Person 类的公有接口声明。
 * 采用私有数据封装，支持栈对象初始化与清理。
 */

#ifndef PERSON_H
#define PERSON_H

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 前向声明私有数据结构 */
typedef struct PersonPrivate PersonPrivate;

/**
 * 公有接口结构体 Person
 * 外部只看到一个不透明指针和一组函数指针。
 */
typedef struct Person {
    PersonPrivate* private_;            /* 私有数据（不透明） */

    /* 行为函数指针，每个都显式接收 Person* self */
    void (*print)(struct Person* self);
    void (*greet)(struct Person* self);
    void (*destroy)(struct Person* self);
    void (*setAge)(struct Person* self, int age);
    int  (*getAge)(struct Person* self);
    const char* (*getName)(struct Person* self);
    void (*setName)(struct Person* self, const char* name);
} Person;

/* 堆对象构造函数 */
Person* Person_new(void);
Person* Person_newWith(int age, const char* name);

/* 栈对象初始化与清理 */
void Person_init(Person* self);
void Person_cleanup(Person* self);

#endif /* PERSON_H */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 实现文件 person.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * person.c
 * Person 类的实现。
 * 私有数据结构体定义在此，对外不可见。
 * 所有成员函数实现均以 static 限定，仅通过公有接口暴露。
 */

#include &quot;person.h&quot;

/* ==================== 私有数据结构体完整定义 ==================== */
struct PersonPrivate {
    int age_;
    char name_[50];
};

/* ==================== 静态成员函数（内部可见） ==================== */
static void Person_print_impl(Person* self) {
    if (self == NULL || self-&amp;gt;private_ == NULL) return;
    PersonPrivate* priv = self-&amp;gt;private_;
    printf(&quot;年龄：%d，姓名：%s\n&quot;, priv-&amp;gt;age_, priv-&amp;gt;name_);
}

static void Person_greet_impl(Person* self) {
    if (self == NULL || self-&amp;gt;private_ == NULL) return;
    PersonPrivate* priv = self-&amp;gt;private_;
    printf(&quot;%s说：你好！\n&quot;, priv-&amp;gt;name_);
}

static void Person_destroy_impl(Person* self) {
    if (self == NULL) return;
    /* 释放私有数据 */
    if (self-&amp;gt;private_ != NULL) {
        free(self-&amp;gt;private_);
        self-&amp;gt;private_ = NULL;
    }
    free(self);   /* 注意：堆对象会被释放，栈对象不应调用此函数 */
}

static void Person_setAge_impl(Person* self, int age) {
    if (self == NULL || self-&amp;gt;private_ == NULL) return;
    self-&amp;gt;private_-&amp;gt;age_ = age;
}

static int Person_getAge_impl(Person* self) {
    if (self == NULL || self-&amp;gt;private_ == NULL) return -1;
    return self-&amp;gt;private_-&amp;gt;age_;
}

static const char* Person_getName_impl(Person* self) {
    if (self == NULL || self-&amp;gt;private_ == NULL) return NULL;
    return self-&amp;gt;private_-&amp;gt;name_;
}

static void Person_setName_impl(Person* self, const char* name) {
    if (self == NULL || self-&amp;gt;private_ == NULL || name == NULL) return;
    strncpy(self-&amp;gt;private_-&amp;gt;name_, name, sizeof(self-&amp;gt;private_-&amp;gt;name_) - 1);
    self-&amp;gt;private_-&amp;gt;name_[sizeof(self-&amp;gt;private_-&amp;gt;name_) - 1] = &apos;\0&apos;;
}

/* ==================== 私有数据分配与释放 ==================== */
static PersonPrivate* PersonPrivate_new(void) {
    PersonPrivate* priv = (PersonPrivate*)malloc(sizeof(PersonPrivate));
    if (priv != NULL) {
        priv-&amp;gt;age_ = 0;
        priv-&amp;gt;name_[0] = &apos;\0&apos;;
    }
    return priv;
}

/* ==================== 构造函数（堆对象） ==================== */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;

    self-&amp;gt;private_ = PersonPrivate_new();
    if (self-&amp;gt;private_ == NULL) {
        free(self);
        return NULL;
    }

    self-&amp;gt;print   = Person_print_impl;
    self-&amp;gt;greet   = Person_greet_impl;
    self-&amp;gt;destroy = Person_destroy_impl;
    self-&amp;gt;setAge  = Person_setAge_impl;
    self-&amp;gt;getAge  = Person_getAge_impl;
    self-&amp;gt;getName = Person_getName_impl;
    self-&amp;gt;setName = Person_setName_impl;

    return self;
}

Person* Person_newWith(int age, const char* name) {
    Person* self = Person_new();
    if (self == NULL) return NULL;

    self-&amp;gt;private_-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;private_-&amp;gt;name_, name, sizeof(self-&amp;gt;private_-&amp;gt;name_) - 1);
    self-&amp;gt;private_-&amp;gt;name_[sizeof(self-&amp;gt;private_-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}

/* ==================== 栈对象初始化与清理 ==================== */
void Person_init(Person* self) {
    if (self == NULL) return;

    self-&amp;gt;private_ = PersonPrivate_new();
    if (self-&amp;gt;private_ == NULL) {
        fprintf(stderr, &quot;栈对象私有内存分配失败\n&quot;);
        return;
    }

    self-&amp;gt;print   = Person_print_impl;
    self-&amp;gt;greet   = Person_greet_impl;
    self-&amp;gt;destroy = NULL;               /* 栈对象绝对不能 free 外壳 */
    self-&amp;gt;setAge  = Person_setAge_impl;
    self-&amp;gt;getAge  = Person_getAge_impl;
    self-&amp;gt;getName = Person_getName_impl;
    self-&amp;gt;setName = Person_setName_impl;
}

void Person_cleanup(Person* self) {
    if (self == NULL) return;
    if (self-&amp;gt;private_ != NULL) {
        free(self-&amp;gt;private_);
        self-&amp;gt;private_ = NULL;
    }
    /* 不释放 self，它在栈上 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.3 测试文件 test5.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test5.c
 * 演示栈对象与堆对象的统一使用方式。
 */

#include &quot;person.h&quot;

int main(void) {
    /* ---------- 栈上对象 ---------- */
    Person p1;
    Person_init(&amp;amp;p1);

    p1.setName(&amp;amp;p1, &quot;张三&quot;);
    p1.setAge(&amp;amp;p1, 18);
    p1.print(&amp;amp;p1);               /* 年龄：18，姓名：张三 */
    p1.greet(&amp;amp;p1);               /* 张三说：你好！ */

    Person_cleanup(&amp;amp;p1);

    /* ---------- 堆上对象 ---------- */
    Person* p2 = Person_newWith(20, &quot;李四&quot;);
    if (p2 != NULL) {
        p2-&amp;gt;print(p2);           /* 年龄：20，姓名：李四 */
        p2-&amp;gt;destroy(p2);         /* 释放私有数据 + 外壳 */
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;年龄：18，姓名：张三
张三说：你好！
年龄：20，姓名：李四
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.4 要点小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;显式 &lt;code&gt;self&lt;/code&gt; 指针&lt;/strong&gt;：所有函数都接收 &lt;code&gt;Person* self&lt;/code&gt; 作为第一个参数，无需全局状态，天然线程安全。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不透明私有数据&lt;/strong&gt;：&lt;code&gt;PersonPrivate&lt;/code&gt; 定义在 &lt;code&gt;.c&lt;/code&gt; 文件中，外部代码只能通过 &lt;code&gt;setAge&lt;/code&gt;、&lt;code&gt;getName&lt;/code&gt; 等接口访问，无法直接触碰成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;栈/堆统一接口&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈对象：声明 &lt;code&gt;Person p1;&lt;/code&gt; → &lt;code&gt;Person_init(&amp;amp;p1)&lt;/code&gt; → 使用 → &lt;code&gt;Person_cleanup(&amp;amp;p1)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;堆对象：&lt;code&gt;Person* p = Person_newWith(...)&lt;/code&gt; → 使用 → &lt;code&gt;p-&amp;gt;destroy(p)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;防御性编程&lt;/strong&gt;：每个函数开头均检查 &lt;code&gt;self&lt;/code&gt; 和 &lt;code&gt;private_&lt;/code&gt; 是否为 &lt;code&gt;NULL&lt;/code&gt;，避免崩溃。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;栈对象 &lt;code&gt;destroy&lt;/code&gt; 设为 &lt;code&gt;NULL&lt;/code&gt;&lt;/strong&gt;：在 &lt;code&gt;Person_init&lt;/code&gt; 中明确将其置空，防止误调导致对栈内存执行 &lt;code&gt;free&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.5 注意事项&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;/td&gt;
&lt;td&gt;必须有 &lt;code&gt;Person_init&lt;/code&gt; 和 &lt;code&gt;Person_cleanup&lt;/code&gt;，否则内存泄漏或重复释放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;栈对象禁止调用 destroy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;destroy&lt;/code&gt; 内部会 &lt;code&gt;free(self)&lt;/code&gt;，栈对象调用即崩溃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;堆对象销毁后指针悬空&lt;/td&gt;
&lt;td&gt;&lt;code&gt;p2-&amp;gt;destroy(p2)&lt;/code&gt; 后 &lt;code&gt;p2&lt;/code&gt; 成为悬垂指针，应避免再次使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多文件编译&lt;/td&gt;
&lt;td&gt;对外只提供 &lt;code&gt;person.h&lt;/code&gt;，私有结构体修改不影响用户代码&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.6 完整项目文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;person.h&lt;/code&gt; —— 公有接口（如上）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;person.c&lt;/code&gt; —— 实现（如上）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test5.c&lt;/code&gt; —— 测试（如上）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编译命令（gcc）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  gcc -std=c99 -Wall -Wextra -o test5 test5.c person.c
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;六、继承与多态&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;继承&lt;/strong&gt;：让新类型（派生类）复用已有类型（基类）的属性和行为。
&lt;strong&gt;多态&lt;/strong&gt;：通过基类接口调用函数时，实际执行的是派生类的版本，即“同一接口，不同表现”。&lt;/p&gt;
&lt;p&gt;C 语言没有原生的继承和多态语法，但可以通过&lt;strong&gt;结构体嵌套&lt;/strong&gt;（将基类作为派生类的第一个成员）和&lt;strong&gt;函数指针&lt;/strong&gt;（或虚函数表）来模拟。&lt;/p&gt;
&lt;h3&gt;6.1 简单继承——每个对象独立函数指针&lt;/h3&gt;
&lt;p&gt;最直观的方式是基类中声明函数指针成员，派生类通过包含基类实例“继承”这些指针。构造函数中，派生类可以&lt;strong&gt;重写&lt;/strong&gt;某些函数指针，也可以复用基类的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test6_inheritance_simple.c
 * 每个对象独立存储函数指针，直观但内存开销较大。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* ---------------------------- 基类 Person ---------------------------- */
typedef struct Person {
    char name_[50];
    int age_;

    /* 函数指针成员，显式传递 self（使用 void* 以兼容派生类） */
    void (*print_)(void* self);
    void (*greet_)(void* self);
} Person;

/* 基类构造函数 */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;
    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;
    /* 函数指针在构造函数中不绑定，由派生类或专用初始化函数处理 */
    self-&amp;gt;print_ = NULL;
    self-&amp;gt;greet_ = NULL;
    return self;
}

/* 基类行为实现 */
void Person_print_impl(void* self) {
    Person* p = (Person*)self;
    printf(&quot;[Person] 姓名：%s，年龄：%d\n&quot;, p-&amp;gt;name_, p-&amp;gt;age_);
}

void Person_greet_impl(void* self) {
    Person* p = (Person*)self;
    printf(&quot;[Person] 你好，我是%s！\n&quot;, p-&amp;gt;name_);
}

/* 便捷的初始化函数：为基类对象绑定默认行为 */
void Person_init_default(Person* self) {
    if (self == NULL) return;
    self-&amp;gt;print_ = Person_print_impl;
    self-&amp;gt;greet_ = Person_greet_impl;
}

/* ---------------------------- 派生类 Student ---------------------------- */
/* 结构体嵌套：基类对象作为第一个成员，保证内存兼容 */
typedef struct Student {
    Person base_;           /* 继承的属性和函数指针 */
    char school_[50];
} Student;

/* 派生类构造函数：分配内存并初始化 */
Student* Student_new(void) {
    Student* self = (Student*)malloc(sizeof(Student));
    if (self == NULL) return NULL;
    self-&amp;gt;base_.age_ = 0;
    self-&amp;gt;base_.name_[0] = &apos;\0&apos;;
    self-&amp;gt;school_[0] = &apos;\0&apos;;

    /* 重写 print_，继承 greet_（绑定基类实现） */
    self-&amp;gt;base_.print_ = Student_print_impl;
    self-&amp;gt;base_.greet_ = Person_greet_impl;   /* 行为继承 */

    return self;
}

void Student_print_impl(void* self) {
    Student* s = (Student*)self;
    printf(&quot;[Student] 姓名：%s，年龄：%d，学校：%s\n&quot;,
           s-&amp;gt;base_.name_, s-&amp;gt;base_.age_, s-&amp;gt;school_);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键设计&lt;/strong&gt;：&lt;code&gt;Student&lt;/code&gt; 的第一个成员是 &lt;code&gt;Person base_&lt;/code&gt;。这样，指向 &lt;code&gt;Student&lt;/code&gt; 的指针可以安全地转换为 &lt;code&gt;Person*&lt;/code&gt;，且访问 &lt;code&gt;base_&lt;/code&gt; 的成员与直接访问 &lt;code&gt;Person&lt;/code&gt; 内存完全一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;使用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    /* 创建基类对象并手动绑定函数指针 */
    Person* person = Person_new();
    Person_init_default(person);                /* 绑定默认行为 */
    strncpy(person-&amp;gt;name_, &quot;Alice&quot;, sizeof(person-&amp;gt;name_) - 1);
    person-&amp;gt;name_[sizeof(person-&amp;gt;name_) - 1] = &apos;\0&apos;;
    person-&amp;gt;age_ = 30;
    person-&amp;gt;print_(person);
    person-&amp;gt;greet_(person);
    free(person);

    /* 创建派生类对象（构造函数已绑定函数指针） */
    Student* student = Student_new();
    strncpy(student-&amp;gt;base_.name_, &quot;Bob&quot;, sizeof(student-&amp;gt;base_.name_) - 1);
    student-&amp;gt;base_.name_[sizeof(student-&amp;gt;base_.name_) - 1] = &apos;\0&apos;;
    student-&amp;gt;base_.age_ = 20;
    strncpy(student-&amp;gt;school_, &quot;XYZ University&quot;, sizeof(student-&amp;gt;school_) - 1);
    student-&amp;gt;school_[sizeof(student-&amp;gt;school_) - 1] = &apos;\0&apos;;

    student-&amp;gt;base_.print_(student);             /* 调用 Student 自己的 print_ */
    student-&amp;gt;base_.greet_(student);             /* 调用继承自 Person 的 greet_ */
    free(student);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Person] 姓名：Alice，年龄：30
[Person] 你好，我是Alice！
[Student] 姓名：Bob，年龄：20，学校：XYZ University
[Person] 你好，我是Bob！
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6.1.1 要点小结&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构体嵌套&lt;/strong&gt;：派生类把基类作为第一个成员，实现“is‑a”关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数指针重写&lt;/strong&gt;：派生类在构造函数中将某些函数指针指向自己的实现，其余保留基类实现，即可完成&lt;strong&gt;行为继承&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存布局&lt;/strong&gt;：因为基类在派生类的最前面，&lt;code&gt;(Person*)student&lt;/code&gt; 是安全的向上转型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个对象独立指针&lt;/strong&gt;：虽然简单直观，但每个对象都保存一份函数指针（此处 2 个指针 16 字节），对象数量多时内存开销较大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.1.2 注意事项&lt;/h4&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;调用时跳转到 &lt;code&gt;NULL&lt;/code&gt;，程序崩溃&lt;/td&gt;
&lt;td&gt;构造函数中统一绑定，或提供类似 &lt;code&gt;Person_init_default&lt;/code&gt; 的初始化函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;错误使用 &lt;code&gt;strcpy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;名称过长可能溢出 &lt;code&gt;name_[50]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;统一使用 &lt;code&gt;strncpy&lt;/code&gt; + 末尾置 &lt;code&gt;&apos;\0&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向上转型时误解内存&lt;/td&gt;
&lt;td&gt;若基类不是第一个成员，强制转换后布局错乱&lt;/td&gt;
&lt;td&gt;始终将基类放在结构体最前面&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;6.2 虚函数表（vtable）——多态的高效实现&lt;/h3&gt;
&lt;p&gt;独立函数指针会为每个对象重复存储相同的函数地址。更好的方式是将这些指针集中放在一个&lt;strong&gt;静态常量表&lt;/strong&gt;里，每个对象只保存一个指向该表的指针。这正是 C++ 虚函数的底层机制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * test7_vtable.c
 * 使用虚函数表（vtable）实现继承与多态。
 */

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* 前向声明 */
typedef struct Person Person;
typedef struct Student Student;

/* 虚函数表结构体，使用 const 保护 */
typedef struct VTable {
    void (*print_)(void* self);
    void (*greet_)(void* self);
    void (*destroy_)(void* self);
} VTable;

/* 基类 Person */
struct Person {
    const VTable* vtable_;      /* 指向虚表的指针（不可修改） */
    char name_[50];
    int age_;
};

/* 派生类 Student */
struct Student {
    Person base_;               /* 继承 */
    char school_[50];
};

/* --------------- 函数实现声明 --------------- */
void Person_print_impl(void* self);
void Person_greet_impl(void* self);
void Person_destroy_impl(void* self);

void Student_print_impl(void* self);
void Student_greet_impl(void* self);
void Student_destroy_impl(void* self);

/* 静态虚表（每个类一个），使用指定初始化器，强健且易读 */
static const VTable Person_vtable = {
    .print_   = Person_print_impl,
    .greet_   = Person_greet_impl,
    .destroy_ = Person_destroy_impl
};

static const VTable Student_vtable = {
    .print_   = Student_print_impl,
    .greet_   = Student_greet_impl,
    .destroy_ = Student_destroy_impl
};

/* --------------- 基类构造函数 --------------- */
Person* Person_new(void) {
    Person* self = (Person*)malloc(sizeof(Person));
    if (self == NULL) return NULL;
    self-&amp;gt;vtable_ = &amp;amp;Person_vtable;   /* 绑定虚表 */
    self-&amp;gt;age_ = 0;
    self-&amp;gt;name_[0] = &apos;\0&apos;;
    return self;
}

Person* Person_newWith(const char* name, int age) {
    Person* self = Person_new();
    if (self == NULL) return NULL;
    self-&amp;gt;age_ = age;
    strncpy(self-&amp;gt;name_, name, sizeof(self-&amp;gt;name_) - 1);
    self-&amp;gt;name_[sizeof(self-&amp;gt;name_) - 1] = &apos;\0&apos;;
    return self;
}

/* --------------- 派生类构造函数 --------------- */
Student* Student_new(void) {
    Student* self = (Student*)malloc(sizeof(Student));
    if (self == NULL) return NULL;
    self-&amp;gt;base_.vtable_ = &amp;amp;Student_vtable;   /* 绑定派生类虚表 */
    self-&amp;gt;base_.age_ = 0;
    self-&amp;gt;base_.name_[0] = &apos;\0&apos;;
    self-&amp;gt;school_[0] = &apos;\0&apos;;
    return self;
}

Student* Student_newWith(const char* name, int age, const char* school) {
    Student* self = Student_new();
    if (self == NULL) return NULL;
    self-&amp;gt;base_.age_ = age;
    strncpy(self-&amp;gt;base_.name_, name, sizeof(self-&amp;gt;base_.name_) - 1);
    self-&amp;gt;base_.name_[sizeof(self-&amp;gt;base_.name_) - 1] = &apos;\0&apos;;
    strncpy(self-&amp;gt;school_, school, sizeof(self-&amp;gt;school_) - 1);
    self-&amp;gt;school_[sizeof(self-&amp;gt;school_) - 1] = &apos;\0&apos;;
    return self;
}

/* --------------- 函数实现 --------------- */
void Person_print_impl(void* self) {
    Person* p = (Person*)self;
    printf(&quot;[Person] 姓名：%s，年龄：%d\n&quot;, p-&amp;gt;name_, p-&amp;gt;age_);
}

void Person_greet_impl(void* self) {
    Person* p = (Person*)self;
    printf(&quot;[Person] 你好，我是%s！\n&quot;, p-&amp;gt;name_);
}

void Person_destroy_impl(void* self) {
    free(self);
}

void Student_print_impl(void* self) {
    Student* s = (Student*)self;
    printf(&quot;[Student] 姓名：%s，年龄：%d，学校：%s\n&quot;,
           s-&amp;gt;base_.name_, s-&amp;gt;base_.age_, s-&amp;gt;school_);
}

void Student_greet_impl(void* self) {
    Student* s = (Student*)self;
    printf(&quot;[Student] 你好，我是%s，来自%s！\n&quot;,
           s-&amp;gt;base_.name_, s-&amp;gt;school_);
}

void Student_destroy_impl(void* self) {
    free(self);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用示例（多态调用）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    Person* p = Person_newWith(&quot;Alice&quot;, 30);
    Student* s = Student_newWith(&quot;Bob&quot;, 20, &quot;XYZ University&quot;);

    /* 通过基类指针调用，实现多态 */
    Person* ps = (Person*)s;          /* 向上转型 */

    p-&amp;gt;vtable_-&amp;gt;print_(p);            /* Person 版本 */
    ps-&amp;gt;vtable_-&amp;gt;print_(ps);          /* 多态：实际调用 Student 的 print_ */
    ps-&amp;gt;vtable_-&amp;gt;greet_(ps);          /* 多态：实际调用 Student 的 greet_ */

    /* 销毁（虚表中有统一的 destroy_） */
    p-&amp;gt;vtable_-&amp;gt;destroy_(p);
    ps-&amp;gt;vtable_-&amp;gt;destroy_(ps);        /* 正确释放 Student 内存 */

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Person] 姓名：Alice，年龄：30
[Student] 姓名：Bob，年龄：20，学校：XYZ University
[Student] 你好，我是Bob，来自XYZ University！
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6.2.1 要点小结&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚表 &lt;code&gt;VTable&lt;/code&gt;&lt;/strong&gt;：将函数指针集中存储在一个静态 &lt;code&gt;const&lt;/code&gt; 结构体中，每个类一个实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象存储 &lt;code&gt;vtable_&lt;/code&gt; 指针&lt;/strong&gt;：所有对象共享同一个虚表，内存节省（每个对象仅增加一个指针）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多态调用&lt;/strong&gt;：通过 &lt;code&gt;obj-&amp;gt;vtable_-&amp;gt;print_(obj)&lt;/code&gt; 这种“二次跳转”实现晚绑定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造时绑定&lt;/strong&gt;：在 &lt;code&gt;Person_new&lt;/code&gt; / &lt;code&gt;Student_new&lt;/code&gt; 中将 &lt;code&gt;vtable_&lt;/code&gt; 指向对应虚表，确保对象创建完毕即可使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;const&lt;/code&gt; 保护&lt;/strong&gt;：虚表声明为 &lt;code&gt;static const&lt;/code&gt;，防止运行时被意外篡改。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6.2.2 注意事项&lt;/h4&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;&lt;code&gt;vtable_&lt;/code&gt; 为 &lt;code&gt;NULL&lt;/code&gt; 或野指针&lt;/td&gt;
&lt;td&gt;构造函数中必须立即绑定虚表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向上转型后误用 &lt;code&gt;free&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;若派生类大小与基类不同，导致内存错误&lt;/td&gt;
&lt;td&gt;通过虚表内的 &lt;code&gt;destroy_&lt;/code&gt; 释放，保证使用正确的 &lt;code&gt;free&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;忘记给派生类定义独立的虚表&lt;/td&gt;
&lt;td&gt;调用的仍是基类函数，丢失多态&lt;/td&gt;
&lt;td&gt;每个派生类都定义自己的 &lt;code&gt;static const VTable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字符串处理不安全&lt;/td&gt;
&lt;td&gt;任意一处 &lt;code&gt;strcpy&lt;/code&gt; 都可能溢出&lt;/td&gt;
&lt;td&gt;全项目统一使用 &lt;code&gt;strncpy&lt;/code&gt; + 末尾置 &lt;code&gt;&apos;\0&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;6.3 多文件&lt;/h3&gt;
&lt;p&gt;在真实项目中，我们通常将每个类的声明放在独立的头文件，实现放在对应的源文件，并利用 &lt;code&gt;const&lt;/code&gt; 保护虚表。编译时只需链接目标文件，修改私有实现不会影响用户代码。&lt;/p&gt;
&lt;p&gt;文件结构示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;project/
├── person.h
├── person.c
├── student.h
├── student.c
└── main.c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;person.h&lt;/strong&gt;（公有接口，遵循最小包含原则）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef PERSON_H
#define PERSON_H

#ifdef __cplusplus
extern &quot;C&quot; {
#endif

/* 前向声明，隐藏实现细节 */
typedef struct Person Person;
typedef struct VTable VTable;

struct VTable {
    void (*print_)(void* self);
    void (*greet_)(void* self);
    void (*destroy_)(void* self);
};

struct Person {
    const VTable* vtable_;
    char name_[50];
    int age_;
};

/* 构造函数与公有函数 */
Person* Person_new(void);
Person* Person_newWith(const char* name, int age);
void Person_destroy(Person* self);   /* 便捷封装，直接调用虚表销毁 */

#ifdef __cplusplus
}
#endif

#endif /* PERSON_H */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;student.h&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef STUDENT_H
#define STUDENT_H

#include &quot;person.h&quot;

typedef struct Student {
    Person base_;
    char school_[50];
} Student;

Student* Student_new(void);
Student* Student_newWith(const char* name, int age, const char* school);
/* 析构函数可以直接使用 Person_destroy((Person*)student) */

#endif /* STUDENT_H */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;person.c&lt;/strong&gt; 和 &lt;strong&gt;student.c&lt;/strong&gt; 的实现与 6.2 节类似，注意将虚表及实现的函数标记为 &lt;code&gt;static&lt;/code&gt;，只通过公有接口暴露。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;main.c&lt;/strong&gt; 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;person.h&quot;
#include &quot;student.h&quot;

int main(void) {
    Person* p = Person_newWith(&quot;Alice&quot;, 30);
    Student* s = Student_newWith(&quot;Bob&quot;, 20, &quot;XYZ University&quot;);

    p-&amp;gt;vtable_-&amp;gt;print_(p);                  /* Person 版本 */
    ((Person*)s)-&amp;gt;vtable_-&amp;gt;print_(s);       /* 多态：Student 版本 */

    Person_destroy(p);
    Person_destroy((Person*)s);             /* 正确释放 Student 内存 */

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译命令（严格模式）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -std=c99 -Wall -Wextra -pedantic -o program main.c person.c student.c
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4 本章总结&lt;/h3&gt;
&lt;p&gt;本章展示了 C 语言模拟&lt;strong&gt;继承与多态&lt;/strong&gt;的两种方式，完成了从基础封装到面向对象编程的完整演进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简单继承（独立函数指针）&lt;/strong&gt;
每个对象持有自己的函数指针，直观易懂。派生类通过替换部分指针实现重写，保留其余指针复用基类行为。代价是每个对象多占用函数指针的存储空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高效多态（虚函数表）&lt;/strong&gt;
将函数指针集中存放在静态 &lt;code&gt;const&lt;/code&gt; 虚表中，对象只保留一个指向虚表的指针。所有同类对象共享同一虚表，大幅节省内存。调用时通过 &lt;code&gt;obj-&amp;gt;vtable_-&amp;gt;print_(obj)&lt;/code&gt; 实现运行时多态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键实践&lt;/strong&gt;（贯穿全系列）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;基类放在派生类第一个成员&lt;/strong&gt;——保证内存布局兼容，向上转型安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构造函数中立即绑定虚表或函数指针&lt;/strong&gt;——对象创建完毕即可用，杜绝野指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚表用 &lt;code&gt;static const&lt;/code&gt; 保护&lt;/strong&gt;——防止意外修改，语义更清晰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;始终使用 &lt;code&gt;strncpy&lt;/code&gt; + 强制尾 &lt;code&gt;&apos;\0&apos;&lt;/code&gt;&lt;/strong&gt;——杜绝字符串溢出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防御式编程&lt;/strong&gt;——每个行为函数先检查指针有效性。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Qt6基础教程：多线程串口通信实战</title><link>https://www.daitcc.top/posts/13725773/</link><guid isPermaLink="true">https://www.daitcc.top/posts/13725773/</guid><description>本文介绍一种线程安全、高内聚低耦合的设计模式：将串口操作封装到工作线程中，通过 SerialManager 对外提供简洁接口，实现串口通信与界面逻辑的完全分离。</description><pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Qt6基础教程：多线程串口通信实战&lt;/h1&gt;
&lt;p&gt;在实际 Qt 项目中，串口通信往往需要长时间运行、频繁收发数据。如果在 GUI 线程中直接操作 &lt;code&gt;QSerialPort&lt;/code&gt;，则 &lt;code&gt;readyRead&lt;/code&gt; 信号和 &lt;code&gt;write&lt;/code&gt; 调用可能阻塞界面，造成卡顿。本文介绍一种&lt;strong&gt;线程安全、高内聚低耦合&lt;/strong&gt;的设计模式：将串口操作封装到工作线程中，通过 &lt;code&gt;SerialManager&lt;/code&gt; 对外提供简洁接口，实现串口通信与界面逻辑的完全分离。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;daitcl/Qt-demo&quot;}&lt;/p&gt;
&lt;h2&gt;一、创建串口工作类 &lt;code&gt;SerialWorker&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;我们要设计的第一个类是 &lt;code&gt;SerialWorker&lt;/code&gt;，它继承自 &lt;code&gt;QObject&lt;/code&gt;，以便后续可以轻松移动到工作线程。&lt;/p&gt;
&lt;h3&gt;1.1 基本框架&lt;/h3&gt;
&lt;p&gt;头文件 &lt;code&gt;serialworker.h&lt;/code&gt; 最开始只需要包含必要的头文件和类声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef SERIALWORKER_H
#define SERIALWORKER_H

#include &amp;lt;QObject&amp;gt;
#include &amp;lt;QSerialPort&amp;gt;

class SerialWorker : public QObject
{
    Q_OBJECT

};
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着添加构造函数和析构函数。注意构造时接收父对象指针，以便启用 Qt 的对象树内存管理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SerialWorker : public QObject
{
    Q_OBJECT
public:
    explicit SerialWorker(QObject *parent = nullptr);
    ~SerialWorker();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的实现文件 &lt;code&gt;serialworker.&lt;/code&gt; 暂时如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;serialworker.h&quot;
#include &amp;lt;QDebug&amp;gt;

SerialWorker::SerialWorker(QObject *parent)
    : QObject(parent)
{
}

SerialWorker::~SerialWorker()
{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 加入串口对象成员&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;SerialWorker&lt;/code&gt; 中增加一个 &lt;code&gt;QSerialPort&lt;/code&gt; 指针，并在构造函数中创建它，同时将其设置为 &lt;code&gt;this&lt;/code&gt; 的子对象。这样，当 &lt;code&gt;SerialWorker&lt;/code&gt; 销毁时，&lt;code&gt;m_serial&lt;/code&gt; 会被 Qt 自动清理，无需手动 &lt;code&gt;delete&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SerialWorker : public QObject
{
    // ... 已有代码 ...
private:
    QSerialPort *m_serial = nullptr;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SerialWorker::SerialWorker(QObject *parent)
    : QObject(parent)
    , m_serial(new QSerialPort(this))
{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;new QSerialPort(this)&lt;/code&gt; 将 &lt;code&gt;m_serial&lt;/code&gt; 作为 &lt;code&gt;SerialWorker&lt;/code&gt; 的子对象，自动纳入 Qt 对象树。父对象析构时会递归删除所有子对象，因此我们不需要在析构函数中写 &lt;code&gt;delete m_serial&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.3 串口的打开与关闭&lt;/h3&gt;
&lt;p&gt;为了能让外部控制串口，我们提供 &lt;code&gt;open&lt;/code&gt; 和 &lt;code&gt;close&lt;/code&gt; 两个槽函数。由于它们将来可能被跨线程调用，所以定义为 &lt;code&gt;public slots&lt;/code&gt;。同时，为了便于传递串口参数，先定义一个配置结构体 &lt;code&gt;SerialConfig&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct SerialConfig {
    QString portName;
    qint32 baudRate = 115200;
    QSerialPort::DataBits dataBits = QSerialPort::Data8;
    QSerialPort::Parity parity = QSerialPort::NoParity;
    QSerialPort::StopBits stopBits = QSerialPort::OneStop;
    QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl;
};

class SerialWorker : public QObject
{
    Q_OBJECT
public:
    explicit SerialWorker(QObject *parent = nullptr);
    ~SerialWorker();
public slots:
    void open(const SerialConfig &amp;amp;config);
    void close();
private:
    QSerialPort *m_serial = nullptr;
    void applyConfig(const SerialConfig &amp;amp;config);
    bool m_isOpen = false;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialWorker::open(const SerialConfig &amp;amp;config)
{
    if (m_isOpen) {
        close();  // 如果已打开，先关闭
    }
    applyConfig(config);
    m_serial-&amp;gt;open(QIODevice::ReadWrite);
    m_isOpen = true;
}

void SerialWorker::close()
{
    if (m_serial-&amp;gt;isOpen()) {
        m_serial-&amp;gt;clear();
        m_serial-&amp;gt;close();
    }
    m_isOpen = false;
}

void SerialWorker::applyConfig(const SerialConfig &amp;amp;config)
{
    m_serial-&amp;gt;setPortName(config.portName);
    m_serial-&amp;gt;setBaudRate(config.baudRate);
    m_serial-&amp;gt;setDataBits(config.dataBits);
    m_serial-&amp;gt;setParity(config.parity);
    m_serial-&amp;gt;setStopBits(config.stopBits);
    m_serial-&amp;gt;setFlowControl(config.flowControl);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么打开前要调用 &lt;code&gt;close&lt;/code&gt;？&lt;/strong&gt;
如果串口已经打开，直接修改端口名、波特率等参数可能不会立即生效，有些参数（如端口名）甚至要求串口处于关闭状态。先关闭再应用新配置，可以保证所有参数按照预期重新初始化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.4 接收数据：解决粘包与界面卡顿&lt;/h3&gt;
&lt;p&gt;串口数据是流式传输的，&lt;code&gt;readyRead&lt;/code&gt; 信号可能在数据未完整时就被触发。如果每收到几个字节就立即通知上层，会导致频繁的信号发射和界面刷新，效率很低。更好的做法是：将数据暂存到缓冲区，等待一小段时间（例如 100 毫秒），如果这段时间内没有新数据到来，再将完整的缓冲区数据一次性发出。这既能避免粘包，又能减少界面刷新次数。&lt;/p&gt;
&lt;p&gt;我们使用一个 &lt;code&gt;QTimer&lt;/code&gt; 来实现延迟聚合。同时为了防止缓冲区无限增长，设定一个最大容量（如 10 MB），超出则强制发送。&lt;/p&gt;
&lt;p&gt;修改类定义，添加必要的成员和槽函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SerialWorker : public QObject
{
    Q_OBJECT
public:
    // ... 已有 public 成员 ...
signals:
    void dataReceived(const QByteArray &amp;amp;data);
private slots:
    void onReadyRead();
    void onTimeout();
private:
    QSerialPort *m_serial = nullptr;
    QTimer *m_delayTimer = nullptr;
    QByteArray m_buffer;
    int m_receiveDelay = 100;   // 默认100ms
    bool m_isOpen = false;
    void applyConfig(const SerialConfig &amp;amp;config);
    void flushBuffer();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造函数中创建定时器并连接信号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SerialWorker::SerialWorker(QObject *parent)
    : QObject(parent)
    , m_serial(new QSerialPort(this))
    , m_delayTimer(new QTimer(this))
{
    m_delayTimer-&amp;gt;setSingleShot(true); // 单次触发模式
    connect(m_serial, &amp;amp;QSerialPort::readyRead, this, &amp;amp;SerialWorker::onReadyRead);
    connect(m_delayTimer, &amp;amp;QTimer::timeout, this, &amp;amp;SerialWorker::onTimeout);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;onReadyRead&lt;/code&gt; 以及相关辅助函数的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialWorker::onReadyRead()
{
    QByteArray chunk = m_serial-&amp;gt;readAll();
    m_buffer.append(chunk);

    // 防止缓冲区无限增长（例如 10MB 上限）
    constexpr int MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10 MiB
    if (m_buffer.size() &amp;gt; MAX_BUFFER_SIZE) {
        flushBuffer();
    }

    if (m_receiveDelay == 0) {
        flushBuffer(); // 立即发送
    } else {
        m_delayTimer-&amp;gt;start(m_receiveDelay); // 重启定时器
    }
}

void SerialWorker::onTimeout()
{
    flushBuffer();
}

void SerialWorker::flushBuffer()
{
    if (m_buffer.isEmpty()) return;
    emit dataReceived(m_buffer);
    m_buffer.clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：每次 &lt;code&gt;readyRead&lt;/code&gt; 都会重置定时器，只有在延时期间没有新数据到达时，定时器才会超时并真正发出数据。这既能保证实时性，又能有效聚合小数据包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.5 发送数据：支持文本与十六进制&lt;/h3&gt;
&lt;p&gt;为了灵活发送数据，我们再实现一个 &lt;code&gt;sendData&lt;/code&gt; 槽函数同时提供一个 &lt;code&gt;setReceiveDelay&lt;/code&gt; 接口，用于动态调整接收聚合延时。&lt;/p&gt;
&lt;p&gt;类定义中增加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public slots:
    void sendData(const QByteArray &amp;amp;data);
    void setReceiveDelay(int ms);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialWorker::sendData(const QByteArray &amp;amp;data)
{
    if (!m_isOpen) return;
    if (data.isEmpty()) return;

    QByteArray outData = data;
    m_serial-&amp;gt;write(outData);
}

void SerialWorker::setReceiveDelay(int ms)
{
    m_receiveDelay = ms;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上述 &lt;code&gt;sendData&lt;/code&gt; 中仅完成了格式转换，实际写入操作 &lt;code&gt;m_serial-&amp;gt;write(outData)&lt;/code&gt; 请根据项目需要添加。同时，示例代码中 &lt;code&gt;onTimeout&lt;/code&gt; 槽函数已在前面实现，但类声明中未显式声明，实际使用时请确保声明与定义一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.6 完善错误处理与状态信号&lt;/h3&gt;
&lt;p&gt;为了便于上层感知串口状态变化和错误，我们增加一组信号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;signals:
    void opened();                          // 打开成功
    void closed();                          // 关闭成功
    void errorOccurred(const QString &amp;amp;errorString);
    void dataReceived(const QByteArray &amp;amp;data);
    void configApplied(const QString &amp;amp;configStr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时完善 &lt;code&gt;open&lt;/code&gt; 和 &lt;code&gt;sendData&lt;/code&gt; 的实现，增加错误检查和信号发射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完善后的 &lt;code&gt;open&lt;/code&gt; 函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialWorker::open(const SerialConfig &amp;amp;config)
{
    if (m_isOpen) {
        close();
    }
    
    applyConfig(config);
    if (!m_serial-&amp;gt;open(QIODevice::ReadWrite)) {
        emit errorOccurred(tr(&quot;打开串口失败: %1&quot;).arg(m_serial-&amp;gt;errorString()));
        return;
    }
    m_isOpen = true;
    emit opened();
    emit configApplied(config.toString());  // 需要 SerialConfig 实现 toString()，或自行构造字符串
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完善后的 &lt;code&gt;sendData&lt;/code&gt; 函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialWorker::sendData(const QByteArray &amp;amp;data)
{
    if (!m_isOpen) {
        emit errorOccurred(tr(&quot;串口未打开，无法发送数据&quot;));
        return;
    }
    if (data.isEmpty()) return;
    
    QByteArray outData = data;
    
    qint64 written = m_serial-&amp;gt;write(outData);
    if (written != outData.size()) {
        emit errorOccurred(tr(&quot;数据发送不完整&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设置接收延迟的槽函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialWorker::setReceiveDelay(int ms)
{
    m_receiveDelay = ms;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，&lt;code&gt;SerialWorker&lt;/code&gt; 类已经具备了打开、关闭、发送、接收（带延迟聚合）以及完整错误通知的能力。&lt;/p&gt;
&lt;h2&gt;二、创建管理类 &lt;code&gt;SerialManager&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SerialWorker&lt;/code&gt; 已经实现了串口的所有底层操作，但它仍然运行在创建它的线程中。为了实现真正的多线程隔离，需要一个管理类，负责：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;SerialWorker&lt;/code&gt; 移动到一个独立的 &lt;code&gt;QThread&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;对外提供线程安全的调用接口（通过信号槽自动处理线程切换）。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;SerialWorker&lt;/code&gt; 发出的信号转发给上层，避免上层直接与工作对象耦合。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.1 定义 &lt;code&gt;SerialManager&lt;/code&gt; 类的基本框架&lt;/h3&gt;
&lt;p&gt;创建头文件 &lt;code&gt;serialmanager.h&lt;/code&gt;，定义 &lt;code&gt;SerialManager&lt;/code&gt; 类，继承自 &lt;code&gt;QObject&lt;/code&gt;。需要包含一个 &lt;code&gt;QThread&lt;/code&gt; 成员和一个 &lt;code&gt;SerialWorker&lt;/code&gt; 指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// serialmanager.h
#ifndef SERIALMANAGER_H
#define SERIALMANAGER_H

#include &amp;lt;QObject&amp;gt;
#include &amp;lt;QThread&amp;gt;
#include &quot;serialworker.h&quot;

class SerialManager : public QObject
{
    Q_OBJECT
public:
    explicit SerialManager(QObject *parent = nullptr);
    ~SerialManager();

private:
    QThread m_workerThread;
    SerialWorker *m_worker = nullptr;
};

#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 添加对外公开接口&lt;/h3&gt;
&lt;p&gt;为了让 GUI 层能够控制串口，需要提供 &lt;code&gt;open&lt;/code&gt;、&lt;code&gt;close&lt;/code&gt;、&lt;code&gt;sendData&lt;/code&gt;、&lt;code&gt;setReceiveDelay&lt;/code&gt; 等函数。这些函数将被设计为 &lt;code&gt;Q_INVOKABLE&lt;/code&gt;（方便 QML 调用，但不是必须），并且可以在任意线程中安全调用——它们内部只发射信号，不执行实际操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SerialManager : public QObject
{
    Q_OBJECT
public:
    explicit SerialManager(QObject *parent = nullptr);
    ~SerialManager();

    // 对外公开接口（可从任意线程安全调用）
    Q_INVOKABLE void open(const SerialConfig &amp;amp;config);
    Q_INVOKABLE void close();
    Q_INVOKABLE void sendData(const QByteArray &amp;amp;data);
    Q_INVOKABLE void setReceiveDelay(int ms);

    // ... 其余成员
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 添加内部请求信号&lt;/h3&gt;
&lt;p&gt;公开接口本身不直接操作 &lt;code&gt;SerialWorker&lt;/code&gt;，而是发射一个“请求信号”。这些信号会被连接到 &lt;code&gt;SerialWorker&lt;/code&gt; 的对应槽函数。由于信号槽可以跨线程，&lt;code&gt;SerialWorker&lt;/code&gt; 将在自己的线程中执行这些槽。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SerialManager : public QObject
{
    // ...
signals:
    // 请求信号（用于转发给 SerialWorker）
    void requestOpen(const SerialConfig &amp;amp;config);
    void requestClose();
    void requestSendData(const QByteArray &amp;amp;data);
    void requestSetReceiveDelay(int ms);

    // 转发信号（稍后添加）
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 添加转发信号&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SerialWorker&lt;/code&gt; 会发出一些状态信号（如 &lt;code&gt;opened&lt;/code&gt;、&lt;code&gt;dataReceived&lt;/code&gt; 等）。上层只需要连接 &lt;code&gt;SerialManager&lt;/code&gt; 的信号即可，因此需要定义一组相同的信号用于转发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SerialManager : public QObject
{
    // ...
signals:
    // ... 请求信号

    // 转发信号（来自 SerialWorker 的事件）
    void opened();
    void closed();
    void errorOccurred(const QString &amp;amp;errorString);
    void dataReceived(const QByteArray &amp;amp;data);
    void configApplied(const QString &amp;amp;configStr);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 实现构造函数&lt;/h3&gt;
&lt;p&gt;构造函数需要完成以下工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建 &lt;code&gt;SerialWorker&lt;/code&gt; 对象（此时它还在主线程）。&lt;/li&gt;
&lt;li&gt;将工作对象移动到 &lt;code&gt;m_workerThread&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;/ul&gt;
&lt;pre&gt;&lt;code&gt;// serialmanager.
#include &quot;serialmanager.h&quot;

SerialManager::SerialManager(QObject *parent)
    : QObject(parent)
{
    m_worker = new SerialWorker();
    m_worker-&amp;gt;moveToThread(&amp;amp;m_workerThread);

    // 连接请求信号到工作对象的槽
    connect(this, &amp;amp;SerialManager::requestOpen, m_worker, &amp;amp;SerialWorker::open);
    connect(this, &amp;amp;SerialManager::requestClose, m_worker, &amp;amp;SerialWorker::close);
    connect(this, &amp;amp;SerialManager::requestSendData, m_worker, &amp;amp;SerialWorker::sendData);
    connect(this, &amp;amp;SerialManager::requestSetReceiveDelay, m_worker, &amp;amp;SerialWorker::setReceiveDelay);

    // 连接工作对象的信号到转发信号
    connect(m_worker, &amp;amp;SerialWorker::opened, this, &amp;amp;SerialManager::opened);
    connect(m_worker, &amp;amp;SerialWorker::closed, this, &amp;amp;SerialManager::closed);
    connect(m_worker, &amp;amp;SerialWorker::errorOccurred, this, &amp;amp;SerialManager::errorOccurred);
    connect(m_worker, &amp;amp;SerialWorker::dataReceived, this, &amp;amp;SerialManager::dataReceived);
    connect(m_worker, &amp;amp;SerialWorker::configApplied, this, &amp;amp;SerialManager::configApplied);

    m_workerThread.start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6 实现析构函数&lt;/h3&gt;
&lt;p&gt;析构时需要安全地停止线程并释放工作对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SerialManager::~SerialManager()
{
    m_workerThread.quit();
    m_workerThread.wait();
    delete m_worker;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;m_worker&lt;/code&gt; 是在构造函数中用 &lt;code&gt;new&lt;/code&gt; 创建的，并且被移到了工作线程。虽然它没有父对象，但我们在析构函数中手动 &lt;code&gt;delete&lt;/code&gt; 是安全的，因为 &lt;code&gt;wait()&lt;/code&gt; 保证了线程已经退出，不会再有槽函数被调用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.7 实现公开接口&lt;/h3&gt;
&lt;p&gt;每个公开接口只需要发射对应的请求信号。由于信号槽是异步的，调用者不会阻塞。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SerialManager::open(const SerialConfig &amp;amp;config)
{
    emit requestOpen(config);
}

void SerialManager::close()
{
    emit requestClose();
}

void SerialManager::sendData(const QByteArray &amp;amp;data)
{
    emit requestSendData(data);
}

void SerialManager::setReceiveDelay(int ms)
{
    emit requestSetReceiveDelay(ms);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、创建简易Demo&lt;/h2&gt;
&lt;h3&gt;3.1 搭建基础界面与 &lt;code&gt;SerialManager&lt;/code&gt; 集成&lt;/h3&gt;
&lt;p&gt;首先创建一个继承自 &lt;code&gt;QWidget&lt;/code&gt; 的界面类，并加入必要的成员变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;头文件 &lt;code&gt;widget.h&lt;/code&gt; 初始结构：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef WIDGET_H
#define WIDGET_H

#include &amp;lt;QWidget&amp;gt;
#include &quot;serial/serialmanager.h&quot;

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    SerialManager *manager = nullptr;   // 串口管理器（多线程安全）
};

#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现文件 &lt;code&gt;widget.cpp&lt;/code&gt; 构造函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;widget.h&quot;
#include &quot;ui_widget.h&quot;

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , manager(new SerialManager(this))   // manager 作为子对象，自动释放
    , ui(new Ui::Widget)
{
    ui-&amp;gt;setupUi(this);
}

Widget::~Widget()
{
    delete ui;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;SerialManager&lt;/code&gt; 内部已经封装了工作线程，我们只需像普通 &lt;code&gt;QObject&lt;/code&gt; 一样创建它，并设置父对象即可。所有串口操作都可以通过 &lt;code&gt;manager&lt;/code&gt; 的公开接口安全调用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.2 初始化串口参数下拉框&lt;/h3&gt;
&lt;p&gt;串口调试助手需要让用户选择波特率、数据位等参数。我们在 &lt;code&gt;Widget&lt;/code&gt; 中添加一个私有方法 &lt;code&gt;initUIConfig()&lt;/code&gt;，在构造函数中调用它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 &lt;code&gt;widget.h&lt;/code&gt; 中添加：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private:
    void initUIConfig();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现 &lt;code&gt;initUIConfig()&lt;/code&gt;：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Widget::initUIConfig()
{
    // 波特率
    ui-&amp;gt;comBaudRate-&amp;gt;addItems({&quot;1200&quot;, &quot;2400&quot;, &quot;4800&quot;, &quot;9600&quot;, &quot;19200&quot;, &quot;38400&quot;, &quot;57600&quot;, &quot;115200&quot;});
    ui-&amp;gt;comBaudRate-&amp;gt;setEditable(true);
    ui-&amp;gt;comBaudRate-&amp;gt;setValidator(new QIntValidator(0, 1000000, this));
    ui-&amp;gt;comBaudRate-&amp;gt;setCurrentIndex(3);   // 默认 9600

    // 数据位
    ui-&amp;gt;comDataBits-&amp;gt;addItems({&quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;});
    ui-&amp;gt;comDataBits-&amp;gt;setCurrentIndex(3);   // 默认 8

    // 校验位
    ui-&amp;gt;comParity-&amp;gt;addItems({&quot;Even&quot;, &quot;Mark&quot;, &quot;None&quot;, &quot;Odd&quot;, &quot;Space&quot;});
    ui-&amp;gt;comParity-&amp;gt;setCurrentIndex(2);     // 默认 None

    // 停止位
    ui-&amp;gt;comStopBits-&amp;gt;addItems({&quot;1&quot;, &quot;1.5&quot;, &quot;2&quot;});
    ui-&amp;gt;comStopBits-&amp;gt;setCurrentIndex(0);   // 默认 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;addItems&lt;/code&gt; 一次性添加常用选项。&lt;/li&gt;
&lt;li&gt;波特率设置为可编辑 + 整数校验器，方便用户手动输入非常用波特率。&lt;/li&gt;
&lt;li&gt;默认选择最常用的参数（9600/8/N/1）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.3 获取可用串口列表&lt;/h3&gt;
&lt;p&gt;动态扫描系统串口是调试助手的基本功能。我们添加 &lt;code&gt;getportInfo()&lt;/code&gt; 方法，并在构造函数中调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;声明与实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// widget.h
private:
    void getportInfo();

// widget.
void Widget::getportInfo()
{
    ui-&amp;gt;comPortName-&amp;gt;clear();
    QList&amp;lt;QSerialPortInfo&amp;gt; ports = QSerialPortInfo::availablePorts();
    if (ports.isEmpty()) {
        ui-&amp;gt;comPortName-&amp;gt;addItem(&quot;无可用串口&quot;);
        ui-&amp;gt;OpenComBtn-&amp;gt;setEnabled(false);
        return;
    }
    ui-&amp;gt;OpenComBtn-&amp;gt;setEnabled(true);
    for (const QSerialPortInfo &amp;amp;info : ports)
        ui-&amp;gt;comPortName-&amp;gt;addItem(info.portName());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;QSerialPortInfo::availablePorts()&lt;/code&gt; 获取当前系统中的串口列表。&lt;/li&gt;
&lt;li&gt;若无可用串口，禁用“打开串口”按钮并显示提示项。&lt;/li&gt;
&lt;li&gt;若有串口，启用按钮并填充端口名。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.4 打开与关闭串口&lt;/h3&gt;
&lt;p&gt;这是与 &lt;code&gt;SerialManager&lt;/code&gt; 交互的第一步。我们需要连接按钮的点击信号，并在槽函数中调用 &lt;code&gt;manager-&amp;gt;open(config)&lt;/code&gt; 或 &lt;code&gt;manager-&amp;gt;close()&lt;/code&gt;。同时，为了知道串口何时真正打开/关闭，需要连接 &lt;code&gt;SerialManager&lt;/code&gt; 的状态信号。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 &lt;code&gt;Widget&lt;/code&gt; 中添加槽函数和状态处理：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// widget.h
private slots:
    void onOpenCloseButtonClicked();
    void handleOpened();
    void handleClosed();
    void handleErrorOccurred(const QString &amp;amp;error);
private:
    bool isSerialOpen = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;构造函数中连接信号：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在 Widget 构造函数中添加
connect(ui-&amp;gt;OpenComBtn, &amp;amp;QPushButton::clicked, this, &amp;amp;Widget::onOpenCloseButtonClicked);
connect(manager, &amp;amp;SerialManager::opened, this, &amp;amp;Widget::handleOpened);
connect(manager, &amp;amp;SerialManager::closed, this, &amp;amp;Widget::handleClosed);
connect(manager, &amp;amp;SerialManager::errorOccurred, this, &amp;amp;Widget::handleErrorOccurred);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现打开/关闭逻辑：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Widget::onOpenCloseButtonClicked()
{
    if (!isSerialOpen) {
        // 准备配置
        SerialConfig config;
        config.portName = ui-&amp;gt;comPortName-&amp;gt;currentText();
        config.baudRate = ui-&amp;gt;comBaudRate-&amp;gt;currentText().toInt();

        // 数据位映射
        switch (ui-&amp;gt;comDataBits-&amp;gt;currentIndex()) {
        case 0: config.dataBits = QSerialPort::Data5; break;
        case 1: config.dataBits = QSerialPort::Data6; break;
        case 2: config.dataBits = QSerialPort::Data7; break;
        default: config.dataBits = QSerialPort::Data8; break;
        }
        // 校验位映射
        switch (ui-&amp;gt;comParity-&amp;gt;currentIndex()) {
        case 0: config.parity = QSerialPort::EvenParity; break;
        case 1: config.parity = QSerialPort::MarkParity; break;
        case 2: config.parity = QSerialPort::NoParity; break;
        case 3: config.parity = QSerialPort::OddParity; break;
        default: config.parity = QSerialPort::SpaceParity; break;
        }
        // 停止位映射
        switch (ui-&amp;gt;comStopBits-&amp;gt;currentIndex()) {
        case 0: config.stopBits = QSerialPort::OneStop; break;
        case 1: config.stopBits = QSerialPort::OneAndHalfStop; break;
        default: config.stopBits = QSerialPort::TwoStop; break;
        }

        manager-&amp;gt;open(config);
        ui-&amp;gt;OpenComBtn-&amp;gt;setEnabled(false);
        ui-&amp;gt;OpenComBtn-&amp;gt;setText(&quot;正在打开...&quot;);
    } else {
        manager-&amp;gt;close();
    }
}

void Widget::handleOpened()
{
    isSerialOpen = true;
    ui-&amp;gt;OpenComBtn-&amp;gt;setEnabled(true);
    ui-&amp;gt;OpenComBtn-&amp;gt;setText(&quot;关闭串口&quot;);
}

void Widget::handleClosed()
{
    isSerialOpen = false;
    ui-&amp;gt;OpenComBtn-&amp;gt;setEnabled(true);
    ui-&amp;gt;OpenComBtn-&amp;gt;setText(&quot;打开串口&quot;);
}

void Widget::handleErrorOccurred(const QString &amp;amp;error)
{
    QMessageBox::critical(this, &quot;串口错误&quot;, error);
    isSerialOpen = false;
    ui-&amp;gt;OpenComBtn-&amp;gt;setEnabled(true);
    ui-&amp;gt;OpenComBtn-&amp;gt;setText(&quot;打开串口&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;设计要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打开时先禁用按钮并显示“正在打开...”，防止重复点击。&lt;/li&gt;
&lt;li&gt;打开成功/失败后，通过 &lt;code&gt;handleOpened&lt;/code&gt; 或 &lt;code&gt;handleErrorOccurred&lt;/code&gt; 恢复按钮状态。&lt;/li&gt;
&lt;li&gt;关闭操作同样异步，最终由 &lt;code&gt;handleClosed&lt;/code&gt; 更新界面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.5 实现数据发送（支持纯文本与十六进制）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SerialManager&lt;/code&gt; 只接受 &lt;code&gt;QByteArray&lt;/code&gt; 类型的原始字节数据。因此，我们需要根据界面上的“Hex发送”复选框，将用户输入的文本转换成对应的字节数组。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;添加槽函数和辅助方法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// widget.h
private slots:
    void onSendButtonClicked();
private:
    void initCheckbox();          // 初始化 Hex 模式相关信号槽
    void formatTxEditForHex();    // 实时格式化 Hex 输入
    void convertTextToHex();      // 文本 -&amp;gt; Hex 字符串
    void convertHexToText();      // Hex 字符串 -&amp;gt; 文本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;发送按钮槽函数实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Widget::onSendButtonClicked()
{
    QString text = ui-&amp;gt;TxEdit-&amp;gt;toPlainText().trimmed();
    if (text.isEmpty()) return;

    bool hexFlag = ui-&amp;gt;HexTxBox-&amp;gt;isChecked();
    QByteArray data;

    if (hexFlag) {
        // 移除空格，校验偶数个字符
        QString hexStr = text;
        hexStr.remove(&apos; &apos;);
        if (hexStr.length() % 2 != 0) {
            QMessageBox::warning(this, &quot;错误&quot;, &quot;十六进制数据必须为偶数个字符，请补零后重试&quot;);
            return;
        }
        data = QByteArray::fromHex(hexStr.toUtf8());
        if (data.isEmpty() &amp;amp;&amp;amp; !hexStr.isEmpty()) {
            QMessageBox::warning(this, &quot;错误&quot;, &quot;无效的十六进制数据&quot;);
            return;
        }
    } else {
        data = text.toUtf8();   // 文本模式使用 UTF-8 编码
    }
    manager-&amp;gt;sendData(data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Hex 输入辅助功能（实时格式化和模式切换）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Widget::initCheckbox()
{
    // 切换 Hex 发送模式时，转换当前文本框内容
    connect(ui-&amp;gt;HexTxBox, &amp;amp;QCheckBox::stateChanged, this, [=](int state) {
        if (state == Qt::Checked)
            convertTextToHex();
        else
            convertHexToText();
    });

    // Hex 模式下，每次文本改变时自动添加空格、过滤非法字符
    connect(ui-&amp;gt;TxEdit, &amp;amp;QTextEdit::textChanged, this, [=]() {
        if (ui-&amp;gt;HexTxBox-&amp;gt;isChecked())
            formatTxEditForHex();
    });
}

void Widget::formatTxEditForHex()
{
    // 实现细节参考最终源码：提取十六进制字符、每两个加空格、保持光标位置
    // 此处省略完整代码
}

void Widget::convertTextToHex()
{
    QString plainText = ui-&amp;gt;TxEdit-&amp;gt;toPlainText();
    QByteArray bytes = plainText.toLocal8Bit();
    QString hexStr = bytes.toHex(&apos; &apos;).toUpper();
    ui-&amp;gt;TxEdit-&amp;gt;blockSignals(true);
    ui-&amp;gt;TxEdit-&amp;gt;setPlainText(hexStr);
    ui-&amp;gt;TxEdit-&amp;gt;blockSignals(false);
}

void Widget::convertHexToText()
{
    QString hexText = ui-&amp;gt;TxEdit-&amp;gt;toPlainText();
    QString pureHex = hexText;
    pureHex.remove(&apos; &apos;);
    QByteArray bytes = QByteArray::fromHex(pureHex.toUtf8());
    if (bytes.isEmpty() &amp;amp;&amp;amp; !pureHex.isEmpty()) {
        QMessageBox::warning(this, &quot;错误&quot;, &quot;无效的十六进制数据，无法转换为文本&quot;);
        return;
    }
    QString plainText = QString::fromLocal8Bit(bytes);
    if (plainText.isEmpty() &amp;amp;&amp;amp; !bytes.isEmpty())
        plainText = QString::fromLatin1(bytes);
    ui-&amp;gt;TxEdit-&amp;gt;blockSignals(true);
    ui-&amp;gt;TxEdit-&amp;gt;setPlainText(plainText);
    ui-&amp;gt;TxEdit-&amp;gt;blockSignals(false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;界面层负责数据格式转换，&lt;code&gt;SerialManager&lt;/code&gt; 保持简洁（只发送原始字节）。&lt;/li&gt;
&lt;li&gt;Hex 模式下，提供实时格式化（自动插入空格、限制输入字符）和模式切换时的内容转换，提升用户体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.6 实现数据接收与显示&lt;/h3&gt;
&lt;p&gt;接收数据通过连接 &lt;code&gt;SerialManager::dataReceived&lt;/code&gt; 信号完成。我们可以在槽函数中根据用户设置（Hex显示、时间戳、自动换行）格式化数据并追加到接收文本框。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;添加槽函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// widget.h
private slots:
    void handleDataReceived(const QByteArray &amp;amp;data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;构造函数中连接信号：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect(manager, &amp;amp;SerialManager::dataReceived, this, &amp;amp;Widget::handleDataReceived);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现接收处理：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Widget::handleDataReceived(const QByteArray &amp;amp;data)
{
    if (data.isEmpty()) return;

    // 根据 Hex 复选框决定显示格式
    QString display;
    if (ui-&amp;gt;HexRxBox-&amp;gt;isChecked()) {
        display = data.toHex(&apos; &apos;).toUpper();
        if (!display.isEmpty()) display += &apos; &apos;;
    } else {
        display = QString::fromUtf8(data);
        if (display.isEmpty()) display = QString::fromLatin1(data);
    }

    ui-&amp;gt;RxEdit-&amp;gt;moveCursor(QTextCursor::End);

    // 时间戳
    if (ui-&amp;gt;TimeBox-&amp;gt;isChecked()) {
        QString timestamp = QDateTime::currentDateTime().toString(&quot;[yyyy/MM/dd hh:mm:ss] &quot;);
        ui-&amp;gt;RxEdit-&amp;gt;insertPlainText(timestamp);
    }

    ui-&amp;gt;RxEdit-&amp;gt;insertPlainText(display);

    // 自动换行
    if (ui-&amp;gt;AWBox-&amp;gt;isChecked())
        ui-&amp;gt;RxEdit-&amp;gt;insertPlainText(&quot;\n&quot;);

    ui-&amp;gt;RxEdit-&amp;gt;moveCursor(QTextCursor::End);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;data.toHex(&apos; &apos;)&lt;/code&gt; 将字节数组转换为带空格的十六进制字符串，便于阅读。&lt;/li&gt;
&lt;li&gt;文本模式优先尝试 UTF-8 解码，失败则回退到 Latin1。&lt;/li&gt;
&lt;li&gt;支持可选的时间戳和自动换行，所有选项实时生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.7 实现定时发送&lt;/h3&gt;
&lt;p&gt;定时发送功能利用 &lt;code&gt;QTimer&lt;/code&gt; 周期性调用发送按钮的槽函数。用户可通过复选框启用/禁用，并通过数值调节框改变间隔。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;添加成员和槽函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// widget.h
private:
    QTimer *m_sendTimer = nullptr;
private slots:
    void onTRBoxToggled(bool checked);
    void onTimeoutSend();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;构造函数中初始化定时器并连接信号：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在 Widget 构造函数中添加
m_sendTimer = new QTimer(this);
m_sendTimer-&amp;gt;setSingleShot(false);
connect(m_sendTimer, &amp;amp;QTimer::timeout, this, &amp;amp;Widget::onTimeoutSend);
connect(ui-&amp;gt;TRBox, &amp;amp;QCheckBox::toggled, this, &amp;amp;Widget::onTRBoxToggled);
connect(ui-&amp;gt;TVBox, QOverload&amp;lt;int&amp;gt;::of(&amp;amp;QSpinBox::valueChanged), this, [=](int value) {
    if (m_sendTimer-&amp;gt;isActive())
        m_sendTimer-&amp;gt;setInterval(value);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现槽函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Widget::onTRBoxToggled(bool checked)
{
    if (checked) {
        int interval = ui-&amp;gt;TVBox-&amp;gt;value();
        if (interval &amp;lt;= 0) {
            QMessageBox::warning(this, &quot;警告&quot;, &quot;定时发送间隔必须大于0&quot;);
            ui-&amp;gt;TRBox-&amp;gt;setChecked(false);
            return;
        }
        m_sendTimer-&amp;gt;setInterval(interval);
        m_sendTimer-&amp;gt;start();
    } else {
        m_sendTimer-&amp;gt;stop();
    }
}

void Widget::onTimeoutSend()
{
    onSendButtonClicked();   // 复用发送逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;设计说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定时器周期根据 &lt;code&gt;ui-&amp;gt;TVBox&lt;/code&gt; 的值动态调整。&lt;/li&gt;
&lt;li&gt;启用定时发送前检查间隔合法性。&lt;/li&gt;
&lt;li&gt;复用 &lt;code&gt;onSendButtonClicked&lt;/code&gt; 避免重复代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.8 附加功能：清空发送/接收编辑框&lt;/h3&gt;
&lt;p&gt;为了界面友好，添加两个清空按钮非常简单，使用 Lambda 表达式直接连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在构造函数中添加：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect(ui-&amp;gt;ClearRxBtn, &amp;amp;QPushButton::clicked, this, [=]() { ui-&amp;gt;RxEdit-&amp;gt;clear(); });
connect(ui-&amp;gt;ClearTxBtn, &amp;amp;QPushButton::clicked, this, [=]() { ui-&amp;gt;TxEdit-&amp;gt;clear(); });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、 回顾与总结&lt;/h2&gt;
&lt;p&gt;通过本教程的三部分，我们完成了一个&lt;strong&gt;多线程串口通信模块&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SerialWorker&lt;/code&gt;&lt;/strong&gt;：封装串口底层操作，利用延迟聚合优化接收效率，并提供打开、关闭、发送、错误处理等完整功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SerialManager&lt;/code&gt;&lt;/strong&gt;：将工作对象移至独立线程，通过信号槽提供线程安全的公开接口，并将工作对象的状态信号转发给上层，实现了界面与串口逻辑的完全解耦。&lt;/li&gt;
&lt;li&gt;**&lt;code&gt;Widget&lt;/code&gt; **：展示了如何使用 &lt;code&gt;SerialManager&lt;/code&gt; 构建一个串口调试助手，包括参数配置、串口扫描、数据收发（支持文本/十六进制、时间戳、自动换行）、定时发送等功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/daitcl/Qt-demo&quot;&gt;完整工程地址&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Qt6基础教程：串口通信与Qt Serial Port模块详解</title><link>https://www.daitcc.top/posts/4b316c3d/</link><guid isPermaLink="true">https://www.daitcc.top/posts/4b316c3d/</guid><description>本文详细介绍了Qt6 Serial Port模块的使用，包括QSerialPortInfo枚举串口及跨平台注意事项，QSerialPort的参数配置（波特率、数据位、停止位、校验位）的setter/getter函数与两种赋值方式，串口的打开/关闭，以及异步与同步两种数据读写方式，为串口通信编程提供了完整的基础教程。</description><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Qt6基础教程：串口通信与Qt Serial Port模块详解&lt;/h1&gt;
&lt;p&gt;Qt Serial Port 模块用于串口通信编程，主要提供了包括配置、I/O操作、RS-232引脚控制信号的获取和设置。如果要在一个项目中使用Qt Serial Port模块，需要在项目的配置文件中加入如下语句：&lt;/p&gt;
&lt;p&gt;头文件中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;QSerialPortInfo&amp;gt; //串口信息
#include &amp;lt;QSerialPort&amp;gt; //串口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;cmake&lt;/code&gt;时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find_package(Qt6 REQUIRED COMPONENTS SerialPort)
target_link_libraries(mytarget PRIVATE Qt6::SerialPort)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;qmake&lt;/code&gt;时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QT += serialport
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Qt 串口的通信协议比较简单，Qt Serial Port模块中只有两个类：&lt;code&gt;QSerialPortInfo&lt;/code&gt;和&lt;code&gt;QSerialPort&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡注意：Qt Serial Port 不支持 如回声、控制CR/LF等功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、QSerialPortInfo&lt;/h2&gt;
&lt;h3&gt;1.QSerialPortInfo 类介绍&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;QSerialPortInfo&lt;/code&gt; 是一个辅助类，用于获取系统中已连接的串口设备信息。它不直接用于通信，而是用来查询串口的名称、描述、制造商等属性，帮助用户选择合适的串口。&lt;/p&gt;
&lt;p&gt;通过阅读&lt;a href=&quot;https://doc.qt.io/qt-6.8/qserialportinfo.html&quot;&gt;官方文档&lt;/a&gt;可以看到&lt;code&gt;QSerialPortInfo&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;QList&amp;lt;QSerialPortInfo&amp;gt; availablePorts()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回当前系统中所有可用串口的信息列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QList&amp;lt;qint32&amp;gt; standardBaudRates()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回系统支持的标准波特率列表（单位：bps）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每个 &lt;code&gt;QSerialPortInfo&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;portName()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;串口名称（如 &lt;code&gt;COM1&lt;/code&gt;、&lt;code&gt;/dev/ttyUSB0&lt;/code&gt;），&lt;strong&gt;打开串口时必须使用此值&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设备描述文字，适合显示给用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;manufacturer()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;制造商名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemLocation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统路径（不同平台格式不同）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vendorIdentifier()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;USB 设备的供应商 ID（VID），仅 USB 设备有效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;productIdentifier()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;USB 设备的产品 ID（PID）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serialNumber()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设备序列号（若有）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hasVendorIdentifier()&lt;/code&gt; / &lt;code&gt;hasProductIdentifier()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断是否存在 VID/PID（虚拟串口通常没有）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isNull()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断串口是否为空（无效）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swap()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;交换两个 &lt;code&gt;QSerialPortInfo&lt;/code&gt; 对象的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 跨平台注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;打开端口&lt;/strong&gt;：始终使用 &lt;code&gt;portName()&lt;/code&gt;，这是唯一跨平台可靠的标识符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备标识&lt;/strong&gt;：如需持久化识别设备（如自动连接特定硬件），推荐组合使用 &lt;code&gt;vendorIdentifier()&lt;/code&gt; + &lt;code&gt;productIdentifier()&lt;/code&gt; + &lt;code&gt;serialNumber()&lt;/code&gt;。虚拟串口或非 USB 串口可能缺少这些信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户显示&lt;/strong&gt;：&lt;code&gt;description()&lt;/code&gt; 和 &lt;code&gt;manufacturer()&lt;/code&gt; 提供了对用户友好的描述，适合在下拉列表中展示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免使用 &lt;code&gt;systemLocation()&lt;/code&gt;&lt;/strong&gt;：该值在不同平台差异很大，不宜作为设备标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 实战示例：枚举并显示串口信息&lt;/h3&gt;
&lt;p&gt;以下代码演示了如何获取所有可用串口，并输出其详细信息（实际应用中通常用于填充下拉列表）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;QCoreApplication&amp;gt;
#include &amp;lt;QDebug&amp;gt;
#include &amp;lt;QSerialPortInfo&amp;gt;

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() &amp;lt;&amp;lt; &quot;=== 可用串口列表 ===&quot;;
    const QList&amp;lt;QSerialPortInfo&amp;gt; ports = QSerialPortInfo::availablePorts();   // 值拷贝
    const QList&amp;lt;QSerialPortInfo&amp;gt; &amp;amp;ports = QSerialPortInfo::availablePorts();  // 引用绑定
    const auto ports = QSerialPortInfo::availablePorts(); //使用auto 简化书写 
    for (const QSerialPortInfo &amp;amp;info : ports) {
        qDebug() &amp;lt;&amp;lt; &quot;-----------------------------&quot;;
        qDebug() &amp;lt;&amp;lt; &quot;端口名:&quot; &amp;lt;&amp;lt; info.portName();
        qDebug() &amp;lt;&amp;lt; &quot;描述:&quot; &amp;lt;&amp;lt; info.description();
        qDebug() &amp;lt;&amp;lt; &quot;制造商:&quot; &amp;lt;&amp;lt; info.manufacturer();

        // 只有真实 USB 设备才存在 VID/PID，虚拟串口通常没有
        if (info.hasVendorIdentifier()) {
            qDebug() &amp;lt;&amp;lt; &quot;VID:&quot; &amp;lt;&amp;lt; QString::asprintf(&quot;0x%04X&quot;, info.vendorIdentifier());
            qDebug() &amp;lt;&amp;lt; &quot;PID:&quot; &amp;lt;&amp;lt; QString::asprintf(&quot;0x%04X&quot;, info.productIdentifier());
        }

        qDebug() &amp;lt;&amp;lt; &quot;系统位置:&quot; &amp;lt;&amp;lt; info.systemLocation();
        if (!info.serialNumber().isEmpty())
            qDebug() &amp;lt;&amp;lt; &quot;序列号:&quot; &amp;lt;&amp;lt; info.serialNumber();
    }

    qDebug() &amp;lt;&amp;lt; &quot;=== 标准波特率 ===&quot;;
    const auto baudRates = QSerialPortInfo::standardBaudRates();
    for (qint32 baud : baudRates) {
        qDebug() &amp;lt;&amp;lt; baud;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果示例&lt;/strong&gt;（Windows 环境）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== 可用串口列表 ===
-----------------------------
端口名: &quot;COM1&quot;
描述: &quot;通信端口&quot;
制造商: &quot;(标准端口类型)&quot;
系统位置: &quot;\\\\.\\COM1&quot;
-----------------------------
端口名: &quot;COM2&quot;
描述: &quot;Virtual Serial Port (Eltima Software)&quot;
制造商: &quot;ELTIMA Software&quot;
系统位置: &quot;\\\\.\\COM2&quot;
-----------------------------
端口名: &quot;COM3&quot;
描述: &quot;Virtual Serial Port (Eltima Software)&quot;
制造商: &quot;ELTIMA Software&quot;
系统位置: &quot;\\\\.\\COM3&quot;
=== 标准波特率 ===
110
300
...
115200
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：&lt;code&gt;QString::asprintf(&quot;0x%04X&quot;, value)&lt;/code&gt; 用于将整数格式化为 4 位十六进制字符串，方便查看 VID/PID。格式说明符 &lt;code&gt;%04X&lt;/code&gt; 表示输出至少 4 位十六进制数字，不足左侧补 0。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二、QSerialPort&lt;/h2&gt;
&lt;h3&gt;1.QSerialPort类介绍&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;QSerialPort&lt;/code&gt; 是访问具体某个串口的类，它可以设置串口通信的参数，打开串口后就可以读写串口数据。通过&lt;a href=&quot;https://doc.qt.io/qt-6.8/qserialport.html&quot;&gt;官方文档&lt;/a&gt;可以看出&lt;code&gt;QSerialPort&lt;/code&gt; 的父类是&lt;code&gt;QIODevice&lt;/code&gt;，所以它属于&lt;strong&gt;I/O设备类&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;QSerialPort的基本使用流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置串口通信参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开/关闭串口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行串口数据的读/写&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1.1 设置串口通信参数&lt;/h4&gt;
&lt;p&gt;串口通信参数主要只考虑&lt;strong&gt;波特率&lt;/strong&gt;、&lt;strong&gt;数据位个数&lt;/strong&gt;、&lt;strong&gt;停止位个数&lt;/strong&gt;和&lt;strong&gt;奇偶校验位&lt;/strong&gt;，在Qt中通过以下函数设置&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;&lt;code&gt;bool setBaudRate(qint32 baudRate, Directions dir = AllDirections)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qint32 baudRate(Directions dir = AllDirections) const&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据位&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool setDataBits(QSerialPort::DataBits dataBits)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::DataBits dataBits() const&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;停止位&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool setStopBits(QSerialPort::StopBits stopBits)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::StopBits stopBits() const&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;校验位&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool setParity(QSerialPort::Parity parity)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::Parity parity() const&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流控&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool setFlowControl(QSerialPort::FlowControl flow)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::FlowControl flowControl() const&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：波特率获取函数返回的是 &lt;code&gt;qint32&lt;/code&gt; 类型，可直接用于显示或比较；其他参数返回对应的枚举类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;1.1.1 波特率&lt;/h5&gt;
&lt;p&gt;设置函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool setBaudRate(qint32 baudRate, QSerialPort::Directions directions = AllDirections);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;baudRate&lt;/code&gt;：可直接指定数值（如 9600、115200），也可使用 &lt;code&gt;QSerialPort&lt;/code&gt; 预定义的常量（如 &lt;code&gt;QSerialPort::Baud115200&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;directions&lt;/code&gt;：默认 &lt;code&gt;AllDirections&lt;/code&gt;，同时设置输入和输出方向。也可单独设置 &lt;code&gt;Input&lt;/code&gt; 或 &lt;code&gt;Output&lt;/code&gt;（很少用到）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qint32 baudRate(QSerialPort::Directions directions = AllDirections) const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前波特率数值，可直接用于显示或比较。&lt;/p&gt;
&lt;h5&gt;1.2.1 数据位&lt;/h5&gt;
&lt;p&gt;设置函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool setDataBits(QSerialPort::DataBits dataBits);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数 &lt;code&gt;dataBits&lt;/code&gt; 是 &lt;code&gt;QSerialPort::DataBits&lt;/code&gt; 枚举类型。有两种使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 方式一：使用枚举常量（推荐，可读性好）
comPort.setDataBits(QSerialPort::Data8);   // 8位数据

// 方式二：使用数值构造（等效）
comPort.setDataBits(QSerialPort::DataBits(8));   // 8位数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QSerialPort::DataBits dataBits() const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前数据位的枚举值。可通过与枚举常量比较或转换为整型来获取实际位数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (comPort.dataBits() == QSerialPort::Data8) {
    qDebug() &amp;lt;&amp;lt; &quot;当前为 8 位数据&quot;;
}
int bits = static_cast&amp;lt;int&amp;gt;(comPort.dataBits());  // bits = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;QSerialPort::DataBits&lt;/code&gt; 枚举常量与数值的对应关系如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::Data5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&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;QSerialPort::Data6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据帧里一个字符的数据位数为6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::Data7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据帧里一个字符的数据位数为7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::Data8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据帧里一个字符的数据位数为8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;1.3.1 停止位&lt;/h5&gt;
&lt;p&gt;设置函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool setStopBits(QSerialPort::StopBits stopBits);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 方式一：使用枚举常量
comPort.setStopBits(QSerialPort::OneStop);   // 1个停止位

// 方式二：使用数值构造
comPort.setStopBits(QSerialPort::StopBits(1));   // 1个停止位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QSerialPort::StopBits stopBits() const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回停止位枚举值，可通过与枚举常量比较或转换为整型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (comPort.stopBits() == QSerialPort::OneStop) {
    qDebug() &amp;lt;&amp;lt; &quot;停止位为 1&quot;;
}
int stop = static_cast&amp;lt;int&amp;gt;(comPort.stopBits());  // stop = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;QSerialPort::StopBits&lt;/code&gt; 枚举常量与数值的对应关系如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::OneStop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1个停止位。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::OneAndHalfStop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;表示 1.5 个停止位。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::TwoStop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2个停止位。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;1.4.1 校验位&lt;/h5&gt;
&lt;p&gt;设置函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool setParity(QSerialPort::Parity parity);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 方式一：使用枚举常量
comPort.setParity(QSerialPort::NoParity);   // 无校验

// 方式二：使用数值构造
comPort.setParity(QSerialPort::Parity(0));   // 无校验
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QSerialPort::Parity parity() const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回校验位枚举值，可通过与枚举常量比较或转换为整型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (comPort.parity() == QSerialPort::NoParity) {
    qDebug() &amp;lt;&amp;lt; &quot;无校验&quot;;
}
int parityVal = static_cast&amp;lt;int&amp;gt;(comPort.parity());  // parityVal = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;QSerialPort::Parity&lt;/code&gt; 枚举常量与数值的对应关系如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::NoParity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::EvenParity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;偶校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::OddParity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;奇校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::SpaceParity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;校验位始终为0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QSerialPort::MarkParity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;校验位始终为1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;💡&lt;strong&gt;注意&lt;/strong&gt;：在串口通信中 通常默认使用 &lt;strong&gt;8个数据位&lt;/strong&gt;，&lt;strong&gt;1个停止位&lt;/strong&gt;，&lt;strong&gt;无奇偶校验位&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;1.5.1 设置示例&lt;/h5&gt;
&lt;p&gt;以下代码展示了两种设置串口参数的方式，两者效果完全相同：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式一：使用枚举常量（推荐）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QSerialPort comPort;
comPort.setPortName(&quot;COM3&quot;);
comPort.setBaudRate(QSerialPort::Baud115200);      // 预定义常量
comPort.setDataBits(QSerialPort::Data8);           // 枚举常量
comPort.setStopBits(QSerialPort::OneStop);         // 枚举常量
comPort.setParity(QSerialPort::NoParity);          // 枚举常量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式二：使用数值构造&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QSerialPort comPort;
comPort.setPortName(&quot;COM3&quot;);
comPort.setBaudRate(115200);                       // 直接数值
comPort.setDataBits(QSerialPort::DataBits(8));     // 数值构造
comPort.setStopBits(QSerialPort::StopBits(1));     // 数值构造
comPort.setParity(QSerialPort::Parity(0));         // 数值构造
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种方式完全等效，可以根据代码风格选择任一种。通常推荐使用枚举常量，因为可读性更好；而数值构造在某些需要动态设置参数的场景下更为灵活。&lt;/p&gt;
&lt;h4&gt;1.2 打开串口&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;void setPort(const QSerialPortInfo &amp;amp;serialPortInfo) //设置串口，通过serialPortInfo 设置，
void setPortName(const QString &amp;amp;name) //设置串口，通过串口名
virtual void close() override //关闭
virtual bool open(QIODeviceBase::OpenMode mode) override //打开
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 函数&lt;code&gt;setPort()&lt;/code&gt;以一个&lt;strong&gt;QSerialPortInfo 类型变量&lt;/strong&gt;作为参数。&lt;code&gt;setPortName()&lt;/code&gt;以串口名称作为参数， 串口名称来自**QSerialPortInfo::portName()**函数的返回值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用&lt;code&gt;open()&lt;/code&gt;打开一个串口，参数&lt;strong&gt;mode&lt;/strong&gt;设置打开的模式，只能设置为&lt;code&gt;QIODeviceBase::ReadOnly&lt;/code&gt;、 &lt;code&gt;QIODeviceBase::WriteOnly&lt;/code&gt; 或 &lt;code&gt;QIODeviceBase::ReadWrite&lt;/code&gt;，不能设置为其他模式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，串口总是 以独占方式打开的，也就是其他进程或线程无法访问一个已经被打开的串口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;串口使用结束后，不再需要使用串口通信时，要调用函数&lt;code&gt;close()&lt;/code&gt;关闭串口。&lt;/p&gt;
&lt;h4&gt;1.3 数据读写&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;QSerialPort&lt;/code&gt; 支持 Qt 标准的 I/O 接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写数据&lt;/strong&gt;：&lt;code&gt;write(const QByteArray &amp;amp;data)&lt;/code&gt; 返回实际写入的字节数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读数据&lt;/strong&gt;：&lt;code&gt;readAll()&lt;/code&gt; 读取所有可用数据，或使用 &lt;code&gt;read(char *data, qint64 maxSize)&lt;/code&gt; 指定读取长度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;接收数据&lt;/strong&gt;：串口有数据到达时会发射 &lt;code&gt;readyRead()&lt;/code&gt; 信号，通常在此信号对应的槽函数中读取数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送数据&lt;/strong&gt;：直接调用 &lt;code&gt;write()&lt;/code&gt;，无需等待信号。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;write()&lt;/code&gt; 只是将数据放入系统缓冲区，并不保证数据立即发出。如需确保数据发送完成，可调用 &lt;code&gt;flush()&lt;/code&gt; 或 &lt;code&gt;waitForBytesWritten()&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1.3 数据读写&lt;/h4&gt;
&lt;p&gt;打开串口后，就可以通过 &lt;code&gt;QSerialPort&lt;/code&gt; 的读写函数进行数据收发。Qt 提供了**异步（非阻塞）&lt;strong&gt;和&lt;/strong&gt;同步（阻塞）**两种方式。在 GUI 程序中推荐使用异步方式，避免界面卡顿；在非 GUI 程序或独立线程中可使用同步方式。&lt;/p&gt;
&lt;h5&gt;1.3.1 异步读写方式&lt;/h5&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;&lt;strong&gt;读相关&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qint64 bytesAvailable()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回接收缓冲区中等待读取的字节数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QByteArray read(qint64 maxSize)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读取最多 &lt;code&gt;maxSize&lt;/code&gt; 个字节&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QByteArray readAll()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读取缓冲区内的全部数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool canReadLine()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断是否有完整的行数据（以换行符结束）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QByteArray readLine(qint64 maxSize = 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读取一行数据，最多 &lt;code&gt;maxSize&lt;/code&gt; 字节&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;写相关&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qint64 write(const char *data, qint64 maxSize)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将 &lt;code&gt;data&lt;/code&gt; 的前 &lt;code&gt;maxSize&lt;/code&gt; 字节写入串口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qint64 write(const char *data)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写入以 &lt;code&gt;\0&lt;/code&gt; 结尾的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qint64 write(const QByteArray &amp;amp;data)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写入字节数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;信号&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void readyRead()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;接收缓冲区有新数据时发射&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void bytesWritten(qint64 bytes)&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;strong&gt;数据接收&lt;/strong&gt;：当串口有数据到达时，&lt;code&gt;QSerialPort&lt;/code&gt; 会发射 &lt;code&gt;readyRead()&lt;/code&gt; 信号。通常在该信号对应的槽函数中调用 &lt;code&gt;read()&lt;/code&gt; 或 &lt;code&gt;readAll()&lt;/code&gt; 读取数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect(serial, &amp;amp;QSerialPort::readyRead, this, &amp;amp;MyClass::onReadyRead);

void MyClass::onReadyRead()
{
    QByteArray data = serial-&amp;gt;readAll();  // 读取所有可用数据
    // 处理数据...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;readyRead()&lt;/code&gt; 信号不会在每个字节到达时都发射，而是在收到一定量数据或数据流稳定后发射。因此，下位机编程时也应避免逐字节发送，建议攒够一批数据再发送，便于上位机处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;数据发送&lt;/strong&gt;：调用 &lt;code&gt;write()&lt;/code&gt; 函数将数据放入发送缓冲区，函数立即返回，不会等待数据实际发送完毕。当数据真正通过串口发出后，会发射 &lt;code&gt;bytesWritten()&lt;/code&gt; 信号（如果需要确认发送完成，可连接该信号）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QByteArray sendData = &quot;Hello\n&quot;;
qint64 written = serial-&amp;gt;write(sendData);
if (written == sendData.size()) {
    // 数据已放入发送缓冲区，发送完成后会发射 bytesWritten()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.3.2 同步（阻塞）读写方式&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;QSerialPort&lt;/code&gt; 提供了两个阻塞式等待函数，用于同步等待数据发送或接收完成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool waitForBytesWritten(int msecs = 30000);  // 最多等待 msecs 毫秒，直到数据发送完毕
bool waitForReadyRead(int msecs = 30000);     // 最多等待 msecs 毫秒，直到有数据可读
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;waitForBytesWritten()&lt;/code&gt; 会阻塞当前线程，直到发送缓冲区中的数据全部写入串口（即 &lt;code&gt;bytesWritten&lt;/code&gt; 信号发射）或超时。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waitForReadyRead()&lt;/code&gt; 会阻塞当前线程，直到接收缓冲区中有新数据到达（即 &lt;code&gt;readyRead&lt;/code&gt; 信号发射）或超时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serial-&amp;gt;write(&quot;AT\r\n&quot;);
if (serial-&amp;gt;waitForBytesWritten(1000)) {
    qDebug() &amp;lt;&amp;lt; &quot;数据已发送&quot;;
}

if (serial-&amp;gt;waitForReadyRead(2000)) {
    QByteArray response = serial-&amp;gt;readAll();
    qDebug() &amp;lt;&amp;lt; &quot;接收到响应:&quot; &amp;lt;&amp;lt; response;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要提示&lt;/strong&gt;：这两个函数会阻塞当前线程的事件循环。如果在 GUI 线程中调用，可能导致界面无响应。因此，&lt;strong&gt;阻塞式读写应在非 GUI 线程（如 &lt;code&gt;QThread&lt;/code&gt;）中使用&lt;/strong&gt;，或仅在控制台程序中使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;1.3.3 选择建议&lt;/h5&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;GUI 程序&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;/td&gt;
&lt;td&gt;同步（阻塞）&lt;/td&gt;
&lt;td&gt;代码逻辑简单，易于理解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要实时性/频繁收发&lt;/td&gt;
&lt;td&gt;异步 + 多线程&lt;/td&gt;
&lt;td&gt;可将串口操作放在独立线程，主线程保持响应&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>GitHub关闭AI数据训练</title><link>https://www.daitcc.top/posts/587601d2/</link><guid isPermaLink="true">https://www.daitcc.top/posts/587601d2/</guid><description>自2026年4月24日起，GitHub将默认使用所有Copilot Free、Pro及Pro+用户的交互数据训练AI模型（企业、学生、教师除外），用户需在Settings→Copilot→Privacy中手动关闭“Allow GitHub to use my data for AI model training”以拒绝，且私有仓库中使用Copilot时产生的提示词与输出仍会被收集。</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;GitHub关闭AI数据训练&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GitHub 这次的政策调整核心就是：从 2026 年 4 月 24 日起，你的 Copilot 交互数据会被默认拿去训练 AI，除非你手动关闭。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最关键的一点是，这个设置 &lt;strong&gt;默认是开启的&lt;/strong&gt;，如果你什么都不做，4 月 24 日之后你的数据就会自动被用于训练。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;如何关闭数据收集&lt;/h3&gt;
&lt;p&gt;操作非常简单，跟着这几步走就行：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;登录&lt;/strong&gt;你的 GitHub 账号。&lt;/li&gt;
&lt;li&gt;点击右上角头像，进入 &lt;strong&gt;Settings&lt;/strong&gt; (设置)。&lt;/li&gt;
&lt;li&gt;在左侧菜单栏找到并点击 &lt;strong&gt;Copilot&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在页面中找到 &lt;strong&gt;Privacy&lt;/strong&gt; (隐私) 这一部分。&lt;/li&gt;
&lt;li&gt;你会看到一个选项：&lt;strong&gt;&quot;Allow GitHub to use my data for AI model training&quot;&lt;/strong&gt; (允许 GitHub 使用我的数据进行 AI 模型训练)。点击旁边的下拉菜单，把它改为 &lt;strong&gt;&quot;Disabled&quot;&lt;/strong&gt; (禁用) 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你有多个 GitHub 账号，记得对每一个账号都重复一次上述操作。&lt;/p&gt;
&lt;h3&gt;关于这次新规，你可能关心的几个细节&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;谁受影响？谁豁免？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;受影响&lt;/strong&gt;：如果你用的是 &lt;strong&gt;Copilot Free (免费版)&lt;/strong&gt;、&lt;strong&gt;Pro (个人专业版)&lt;/strong&gt; 或 &lt;strong&gt;Pro+&lt;/strong&gt;，那么新规对你生效，默认会被收集数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;豁免&lt;/strong&gt;：企业用户 (Copilot Business 和 Enterprise)、学生和教师 &lt;strong&gt;不在&lt;/strong&gt; 这次新政的范围内，他们的数据不会被用于训练。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;什么数据会被收集？&lt;/strong&gt;
不仅仅是代码片段。只要你使用 Copilot，它可能会收集：
&lt;ul&gt;
&lt;li&gt;你输入的 &lt;strong&gt;提示词 (Prompts)&lt;/strong&gt; 和 Copilot 给出的 &lt;strong&gt;输出建议&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;光标周围的 &lt;strong&gt;代码上下文&lt;/strong&gt;、文件名、仓库结构。&lt;/li&gt;
&lt;li&gt;你对建议的 &lt;strong&gt;反馈&lt;/strong&gt; (比如点👍或👎)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我的私有仓库安全吗？&lt;/strong&gt;
&lt;strong&gt;情况比较复杂&lt;/strong&gt;。GitHub 强调，它们&lt;strong&gt;不会&lt;/strong&gt;去扫描和训练你放在仓库里&quot;静态&quot;的私有代码。&lt;strong&gt;但是&lt;/strong&gt;，一旦你在私有仓库里&lt;strong&gt;打开了 Copilot 并进行交互&lt;/strong&gt;，这次对话产生的提示词和生成的代码片段&lt;strong&gt;就会被收集&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简单来说，&lt;strong&gt;只要你用 Copilot，数据就可能被采集，跟你写代码的仓库是公开还是私有无关&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>Mihomo 配置文件详细介绍</title><link>https://www.daitcc.top/posts/4584a3ea/</link><guid isPermaLink="true">https://www.daitcc.top/posts/4584a3ea/</guid><description>本配置文件为 Mihomo 设计了全面的代理方案，支持混合端口、TUN 透明代理和 IPv6，允许局域网共享。内置 DNS 采用 Fake-IP 模式，智能分流国内外解析。通过 proxy-providers 管理多个订阅源，并利用正则过滤生成地区分组（香港、日本等）、自动优选、负载均衡及故障转移策略。广告拦截通过 rule-providers 动态拉取规则集优先拒绝。支持两种规则模式：内置地理数据（geodata-mode）自动下载 GeoIP/GeoSite，或外部规则集（MRS 格式）实现精准分流。Web 管理面板自动部署，方便实时切换节点与查看状态。整体配置兼顾灵活性与性能，适合网关或桌面全局代理场景。</description><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Mihomo 配置文件详细介绍&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 合规使用声明&lt;/strong&gt; ：本教程仅用于&lt;strong&gt;内网流量调试、开发测试及合法网络管理&lt;/strong&gt;场景，禁止用于任何违法用途。请遵守当地法律法规。&lt;/p&gt;
&lt;h2&gt;一、配置文件概览&lt;/h2&gt;
&lt;p&gt;本配置文件是一个功能全面的 Mihomo 配置，适用于作为网关或桌面代理使用。它集成了以下核心特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础代理服务&lt;/strong&gt;：混合端口、HTTP/SOCKS5 代理，支持 IPv6 和局域网共享。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUN 透明代理&lt;/strong&gt;：实现系统全局代理，无需每个应用单独配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 配置&lt;/strong&gt;：使用 Fake-IP 模式，智能分流国内外域名解析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内置地理数据源&lt;/strong&gt;：通过 &lt;code&gt;geodata-mode&lt;/code&gt; 自动下载 GeoIP/GeoSite 数据库，用于域名和 IP 规则匹配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量嗅探&lt;/strong&gt;：识别未加密流量中的域名，增强分流准确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理提供者&lt;/strong&gt;：支持多个订阅源，并可自动更新节点列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广告拦截&lt;/strong&gt;：通过 &lt;code&gt;rule-providers&lt;/code&gt; 拉取广告规则集，优先拒绝广告请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活的代理分组&lt;/strong&gt;：包括手动选择地区分组、自动优选、负载均衡、故障转移等多种策略，并支持选择节点来源（所有提供者或指定提供者）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web 管理面板&lt;/strong&gt;：自动下载并启用 metacubexd 面板，方便通过浏览器管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、配置模块详解&lt;/h2&gt;
&lt;h3&gt;1. 基础配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mixed-port: 7890
ipv6: true
allow-lan: true
unified-delay: false
tcp-concurrent: true
external-controller: 0.0.0.0:9090
external-ui: ui
external-ui-url: &quot;https://ghproxy.cn/github.com/MetaCubeX/metacubexd/archive/gh-pages.zip&quot;
find-process-mode: strict
global-client-fingerprint: chrome
profile:
  store-selected: true
  store-fake-ip: true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mixed-port&lt;/strong&gt;：同时支持 HTTP 和 SOCKS5 的混合代理端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ipv6&lt;/strong&gt;：启用 IPv6 支持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;allow-lan&lt;/strong&gt;：允许局域网其他设备使用此代理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unified-delay&lt;/strong&gt;：关闭统一延迟测试，保持各节点独立延迟数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tcp-concurrent&lt;/strong&gt;：启用 TCP 并发，提升多连接场景性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;external-controller&lt;/strong&gt;：RESTful API 监听地址，&lt;code&gt;0.0.0.0:9090&lt;/code&gt; 表示监听所有接口，允许局域网访问面板。如需仅本机访问，可改为 &lt;code&gt;127.0.0.1:9090&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;external-ui&lt;/strong&gt; 和 &lt;strong&gt;external-ui-url&lt;/strong&gt;：自动下载并挂载 Web 面板（metacubexd），访问 &lt;code&gt;http://&amp;lt;设备IP&amp;gt;:9090/ui&lt;/code&gt; 即可打开面板。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;find-process-mode&lt;/strong&gt;：进程查找模式，&lt;code&gt;strict&lt;/code&gt; 提高匹配准确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;global-client-fingerprint&lt;/strong&gt;：设置全局 TLS 指纹，伪装成 Chrome 浏览器，减少 TLS 指纹识别风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;profile&lt;/strong&gt;：保存用户手动选择的节点和 Fake-IP 映射，重启后恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 内置地理数据源&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;geodata-mode: true
geox-url:
  geoip: &quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat&quot;
  geosite: &quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat&quot;
  mmdb: &quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb&quot;
  asn: &quot;https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;geodata-mode&lt;/strong&gt;：启用内置地理数据模式，规则中的 &lt;code&gt;GEOIP&lt;/code&gt; 和 &lt;code&gt;GEOSITE&lt;/code&gt; 将从配置的 URL 下载数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;geox-url&lt;/strong&gt;：指定各数据库的下载地址，Mihomo 会自动下载并缓存。&lt;code&gt;geoip-lite.dat&lt;/code&gt; 为精简版 IP 归属地数据库，&lt;code&gt;geosite.dat&lt;/code&gt; 为域名分类数据库，&lt;code&gt;country-lite.mmdb&lt;/code&gt; 为 MaxMind 格式的 IP 数据库，&lt;code&gt;GeoLite2-ASN.mmdb&lt;/code&gt; 为 ASN 信息数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 流量嗅探&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sniffer:
  enable: true
  sniff:
    HTTP:
      ports: [80, 8080-8880]
      override-destination: true
    TLS:
      ports: [443, 8443]
    QUIC:
      ports: [443, 8443]
  skip-domain:
    - &quot;Mijia Cloud&quot;
    - &quot;+.push.apple.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;enable&lt;/strong&gt;：开启流量嗅探，能够从 HTTP、TLS 和 QUIC 流量中提取域名，用于分流决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sniff&lt;/strong&gt;：定义各协议监听的端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;override-destination&lt;/strong&gt;：当嗅探到域名后，使用该域名替代原始目标地址进行规则匹配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;skip-domain&lt;/strong&gt;：跳过对指定域名的嗅探（如小米云、苹果推送），避免干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. TUN 透明代理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tun:
  enable: true
  stack: mixed
  dns-hijack:
    - &quot;any:53&quot;
    - &quot;tcp://any:53&quot;
  auto-route: true
  auto-redirect: true
  auto-detect-interface: true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;enable&lt;/strong&gt;：启用 TUN 虚拟网卡，实现全局代理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stack&lt;/strong&gt;：网络栈，&lt;code&gt;mixed&lt;/code&gt; 同时支持 TCP 和 UDP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dns-hijack&lt;/strong&gt;：劫持所有发往 53 端口的 DNS 请求，强制使用 Mihomo 内部 DNS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;auto-route&lt;/strong&gt;：自动添加路由表，将流量导向 TUN 设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;auto-redirect&lt;/strong&gt;：自动配置 iptables 规则（仅 Linux），实现透明代理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;auto-detect-interface&lt;/strong&gt;：自动检测默认出口网卡。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. DNS 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dns:
  enable: true
  ipv6: true
  respect-rules: true
  enhanced-mode: fake-ip
  fake-ip-filter:
    - &quot;*&quot;
    - &quot;+.lan&quot;
    - &quot;+.local&quot;
    - &quot;+.market.xiaomi.com&quot;
  nameserver:
    - https://120.53.53.53/dns-query
    - https://223.5.5.5/dns-query
  proxy-server-nameserver:
    - https://120.53.53.53/dns-query
    - https://223.5.5.5/dns-query
  nameserver-policy:
    &quot;geosite:cn,private&quot;:
      - https://120.53.53.53/dns-query
      - https://223.5.5.5/dns-query
    &quot;geosite:geolocation-!cn&quot;:
      - &quot;https://dns.cloudflare.com/dns-query&quot;
      - &quot;https://dns.google/dns-query&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;enable&lt;/strong&gt;：启用内置 DNS 服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;respect-rules&lt;/strong&gt;：DNS 查询结果将遵守分流规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;enhanced-mode&lt;/strong&gt;：&lt;code&gt;fake-ip&lt;/code&gt; 模式，为每个域名分配一个假的 IP，减少真实 DNS 查询，提升隐私和速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fake-ip-filter&lt;/strong&gt;：对这些域名不返回 Fake-IP，而是进行真实 DNS 查询。&lt;code&gt;*&lt;/code&gt; 表示所有域名，但后续的 &lt;code&gt;+.lan&lt;/code&gt; 等是排除项，即对 &lt;code&gt;.lan&lt;/code&gt;、&lt;code&gt;.local&lt;/code&gt; 等域名直接查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nameserver&lt;/strong&gt;：默认 DNS 服务器（用于国内域名或未匹配策略的域名）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;proxy-server-nameserver&lt;/strong&gt;：当使用代理节点进行 DNS 查询时使用的 DNS 服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nameserver-policy&lt;/strong&gt;：根据域名分类指定不同的 DNS 服务器。这里将中国域名和私有域名交给国内 DoH，非中国域名交给国外 DoH。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 代理提供者（订阅）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;proxy-providers:
  机场A:
    type: http
    url: &quot;YOUR_SUBSCRIPTION_URL_1&quot;
    interval: 86400
    health-check:
      enable: true
      url: &quot;https://www.gstatic.com/generate_204&quot;
      interval: 300
    override:
      additional-prefix: &quot;[机场A]&quot;
  机场B:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机场A/机场B&lt;/strong&gt;：自定义的提供者名称，用于在分组中引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;type&lt;/strong&gt;：&lt;code&gt;http&lt;/code&gt; 表示通过 HTTP/HTTPS 获取节点列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;url&lt;/strong&gt;：订阅链接，请替换为实际地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;interval&lt;/strong&gt;：更新间隔（秒），&lt;code&gt;86400&lt;/code&gt; 为每天更新一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;health-check&lt;/strong&gt;：定期测试节点延迟，&lt;code&gt;enable: true&lt;/code&gt; 启用，&lt;code&gt;url&lt;/code&gt; 测试目标，&lt;code&gt;interval&lt;/code&gt; 测试频率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;override&lt;/strong&gt;：为从该提供者获取的每个节点名称添加前缀，便于识别来源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 直连节点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;proxies:
  - name: &quot;直连&quot;
    type: direct
    udp: true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;定义一个直连节点，用于不走代理的流量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. 广告拦截规则集（rule-providers）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;rule-providers:
  AD:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/AD.yaml&quot;
    path: ./rules/AD.yaml
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;rule-providers&lt;/strong&gt;：定义外部规则集提供者，用于动态更新规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AD、EasyList 等&lt;/strong&gt;：规则集名称，在 &lt;code&gt;rules&lt;/code&gt; 中用 &lt;code&gt;RULE-SET,名称&lt;/code&gt; 引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;type&lt;/strong&gt;：&lt;code&gt;http&lt;/code&gt; 表示通过 HTTP 获取规则文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;behavior&lt;/strong&gt;：规则行为，&lt;code&gt;domain&lt;/code&gt; 表示匹配域名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;url&lt;/strong&gt;：规则文件的下载地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;path&lt;/strong&gt;：本地缓存路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;interval&lt;/strong&gt;：更新间隔（秒），&lt;code&gt;86400&lt;/code&gt; 每天更新一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些广告规则集用于在规则匹配初期拒绝广告请求。&lt;/p&gt;
&lt;h3&gt;9. 代理分组&lt;/h3&gt;
&lt;p&gt;代理分组是分流策略的核心，本配置提供了多种类型的分组。&lt;/p&gt;
&lt;h4&gt;9.1 顶级手动分组（默认）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- name: 默认
  type: select
  proxies:
    - 自动选择
    - 全部节点
    - 负载均衡(散列)
    - 负载均衡(轮询)
    - 故障转移
    - 直连
    - 香港
    - 台湾
    - 日本
    - 新加坡
    - 美国
    - 其它地区
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;type: select&lt;/strong&gt;：手动选择模式，用户可在面板中从列出的代理中任选一个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;proxies&lt;/strong&gt;：列出了可供选择的子分组和直连节点，作为最终出口的候选。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9.2 地区分组（手动选择，带正则过滤）&lt;/h4&gt;
&lt;p&gt;例如香港分组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: 香港
  type: select
  # include-all: true
  # use:
  #   - 机场A
  #   - 机场B
  filter: &quot;(?i)港|hk|hongkong|hong kong&quot;
  exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;type: select&lt;/strong&gt;：手动选择节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点来源&lt;/strong&gt;：可以通过 &lt;code&gt;include-all: true&lt;/code&gt; 从所有代理提供者中筛选，也可以通过 &lt;code&gt;use&lt;/code&gt; 指定只从某些提供者中筛选。&lt;strong&gt;两者只能选其一&lt;/strong&gt;，本配置中已注释掉两种方式，您需要根据需求取消对应行的注释。
&lt;ul&gt;
&lt;li&gt;若取消注释 &lt;code&gt;include-all: true&lt;/code&gt;，则从所有提供者中筛选。&lt;/li&gt;
&lt;li&gt;若取消注释 &lt;code&gt;use&lt;/code&gt;，则只从列出的提供者（如机场A、机场B）中筛选。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;filter&lt;/strong&gt;：正则表达式，匹配节点名称中包含“港”、“hk”等关键词的节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exclude-filter&lt;/strong&gt;：排除包含回国、国内省市、运营商等关键词的节点，确保不选中回国线。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他地区分组（台湾、日本、美国、新加坡、其它地区）类似，正则表达式已针对各区域调整。&lt;/p&gt;
&lt;h4&gt;9.3 自动优选分组&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- name: 自动选择
  type: url-test
  # include-all: true
  # use:
  #   - 机场A
  #   - 机场B
  tolerance: 10
  exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;type: url-test&lt;/strong&gt;：自动测试节点延迟，选择延迟最低的节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tolerance&lt;/strong&gt;：延迟差在 10ms 内的节点视为同等，避免频繁切换。&lt;/li&gt;
&lt;li&gt;同样支持 &lt;code&gt;include-all&lt;/code&gt; 或 &lt;code&gt;use&lt;/code&gt; 选择节点来源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9.4 全部节点&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- name: 全部节点
  type: select
  # include-all: true
  # use:
  #   - 机场A
  #   - 机场B
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;手动选择所有节点中的具体某个节点，不经过正则过滤。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9.5 负载均衡分组&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- name: 负载均衡(散列)
  type: load-balance
  # include-all: true
  # use:
  #   - 机场A
  #   - 机场B
  strategy: consistent-hashing
- name: 负载均衡(轮询)
  type: load-balance
  strategy: round-robin
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;type: load-balance&lt;/strong&gt;：负载均衡策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;strategy&lt;/strong&gt;：&lt;code&gt;consistent-hashing&lt;/code&gt; 一致性哈希，适合需要保持会话一致性的场景；&lt;code&gt;round-robin&lt;/code&gt; 轮询，轮流使用节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9.6 故障转移&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- name: 故障转移
  type: fallback
  # include-all: true
  # use:
  #   - 机场A
  #   - 机场B
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;type: fallback&lt;/strong&gt;：按顺序尝试节点，失败时切换到下一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9.7 国内流量分组&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- name: 国内
  type: select
  proxies:
    - 直连
    - 默认
    - 自动选择
    - 香港
    - 台湾
    - 日本
    - 新加坡
    - 美国
    - 其它地区
    - 全部节点
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;专用于国内域名，默认选项为 &lt;code&gt;直连&lt;/code&gt;，但用户可以手动切换为其他分组。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9.8 服务分组&lt;/h4&gt;
&lt;p&gt;为各常用服务（Google、Telegram、Twitter 等）定义了独立分组，大部分为 &lt;code&gt;select&lt;/code&gt; 类型，允许用户手动选择出口。其中 Telegram 分组为 &lt;code&gt;url-test&lt;/code&gt; 类型，并配有复杂的正则过滤，自动优选境外节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Telegram
  type: url-test
  # include-all: true
  # use:
  #   - 机场A
  #   - 机场B
  url: &quot;https://www.gstatic.com/generate_204&quot;
  interval: 300
  tolerance: 50
  filter: &quot;(?i)(港|hk|hongkong|hong kong|台|tw|taiwan|日|jp|japan|美|us|unitedstates|united states|新|sg|singapore)&quot;
  exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;url&lt;/strong&gt;：测速目标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;interval&lt;/strong&gt;：测速间隔。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tolerance&lt;/strong&gt;：延迟容忍度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;filter/exclude-filter&lt;/strong&gt;：筛选境外节点，排除回国节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10. 规则（rules）&lt;/h3&gt;
&lt;p&gt;规则按照从上到下的顺序匹配，匹配即停止。本配置的规则顺序为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;广告拦截&lt;/strong&gt;：使用 &lt;code&gt;RULE-SET&lt;/code&gt; 引用广告规则集，匹配到的请求直接 &lt;code&gt;REJECT&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有 IP 直连&lt;/strong&gt;：&lt;code&gt;GEOIP,private,直连,no-resolve&lt;/code&gt;，所有私有 IP 地址（如 192.168.x.x）直接连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;域名规则&lt;/strong&gt;：使用 &lt;code&gt;GEOSITE&lt;/code&gt; 匹配各服务域名，指向对应的服务分组。例如 &lt;code&gt;GEOSITE,google,Google&lt;/code&gt; 将 Google 域名指向 Google 分组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;国内域名&lt;/strong&gt;：&lt;code&gt;GEOSITE,cn,国内&lt;/code&gt;，将中国域名指向“国内”分组（默认直连）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非中国域名&lt;/strong&gt;：&lt;code&gt;GEOSITE,geolocation-!cn,其他&lt;/code&gt;，将非中国域名指向“其他”分组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP 规则&lt;/strong&gt;：&lt;code&gt;GEOIP,CN,国内,no-resolve&lt;/code&gt;，将中国 IP 指向“国内”分组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终兜底&lt;/strong&gt;：&lt;code&gt;MATCH,其他&lt;/code&gt;，所有未匹配流量最终走“其他”分组。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;三、如何选择节点来源&lt;/h2&gt;
&lt;p&gt;在代理分组中，我们提供了两种方式来指定节点来源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;include-all: true&lt;/code&gt;&lt;/strong&gt;：从所有已定义的 &lt;code&gt;proxy-providers&lt;/code&gt; 中筛选节点。适合希望合并多个订阅源的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;use&lt;/code&gt;&lt;/strong&gt;：列出特定的提供者名称，只从这些提供者中筛选节点。适合只想使用部分订阅源的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;操作方式&lt;/strong&gt;：在分组配置中，找到对应的注释行，取消注释所需方式的行，并注释掉另一种方式。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: 香港
  type: select
  include-all: true   # 取消注释此行，表示从所有提供者中筛选
  # use:              # 注释掉 use 块，表示不使用指定提供者
  #   - 机场A
  #   - 机场B
  filter: ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: 香港
  type: select
  # include-all: true   # 注释掉 include-all
  use:                  # 取消注释 use 块
    - 机场A
    - 机场B
  filter: ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;include-all&lt;/code&gt; 和 &lt;code&gt;use&lt;/code&gt; 是互斥的，只能选择其一。&lt;/p&gt;
&lt;h2&gt;四、广告规则集的更新与自定义&lt;/h2&gt;
&lt;p&gt;广告规则集通过 &lt;code&gt;rule-providers&lt;/code&gt; 定义，配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rule-providers:
  AD:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/AD.yaml&quot;
    path: ./rules/AD.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每个规则集每天（&lt;code&gt;86400&lt;/code&gt; 秒）自动从指定 URL 拉取最新版本，并缓存到本地 &lt;code&gt;./rules/&lt;/code&gt; 目录下。&lt;/li&gt;
&lt;li&gt;如需添加自定义广告规则，可以新建一个 &lt;code&gt;rule-provider&lt;/code&gt;，指向自己维护的规则文件，或者在本地 &lt;code&gt;path&lt;/code&gt; 文件中手动添加规则。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;rules&lt;/code&gt; 部分，使用 &lt;code&gt;RULE-SET,名称,REJECT&lt;/code&gt; 即可引用该规则集进行广告拦截。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;五、使用注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;替换订阅链接&lt;/strong&gt;：请务必将 &lt;code&gt;YOUR_SUBSCRIPTION_URL_1&lt;/code&gt; 和 &lt;code&gt;YOUR_SUBSCRIPTION_URL_2&lt;/code&gt; 替换为您的实际订阅地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点来源选择&lt;/strong&gt;：根据您的需求，在每个需要筛选节点的分组中正确选择 &lt;code&gt;include-all&lt;/code&gt; 或 &lt;code&gt;use&lt;/code&gt;，并确保提供者名称与 &lt;code&gt;proxy-providers&lt;/code&gt; 中定义的一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正则表达式&lt;/strong&gt;：地区分组和服务分组中的正则表达式基于常见节点命名习惯编写，可能无法覆盖所有情况。如果某些节点未被正确归类，可自行调整正则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首次启动&lt;/strong&gt;：首次启动时会自动下载 GeoIP/GeoSite 数据库、Web 面板以及广告规则集，请确保网络畅通。如果下载失败，可手动下载对应文件放入指定路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web 面板访问&lt;/strong&gt;：启动后，通过浏览器访问 &lt;code&gt;http://&amp;lt;设备IP&amp;gt;:9090/ui&lt;/code&gt; 即可打开面板。如需从外网访问，请确保防火墙允许 9090 端口，并注意安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUN 模式依赖&lt;/strong&gt;：TUN 模式需要 root/管理员权限，并确保系统支持（Linux 需开启 &lt;code&gt;net.ipv4.ip_forward&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;六、故障排查&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;查看日志：&lt;code&gt;sudo journalctl -u Mihomo -f&lt;/code&gt;（如果使用 systemd 管理）。&lt;/li&gt;
&lt;li&gt;检查配置文件语法：&lt;code&gt;/usr/local/bin/Mihomo -t -d /etc/Mihomo&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;确认订阅链接是否可访问，规则文件 URL 是否有效。&lt;/li&gt;
&lt;li&gt;如果某些网站无法访问，尝试调整规则顺序或修改对应分组策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;七、完整配置文件参考&lt;/h2&gt;
&lt;h3&gt;1.基于内置地理数据 的 Mihomo 配置文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ========== 基础配置 ==========
mixed-port: 7890
ipv6: true
allow-lan: true
unified-delay: false
tcp-concurrent: true
external-controller: 0.0.0.0:9090      # 允许局域网访问面板
external-ui: ui
external-ui-url: &quot;https://ghproxy.cn/github.com/MetaCubeX/metacubexd/archive/gh-pages.zip&quot;

find-process-mode: strict
global-client-fingerprint: chrome

profile:
  store-selected: true
  store-fake-ip: true

# ========== 内置地理数据源 ==========
geodata-mode: true
geox-url:
  geoip: &quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat&quot;
  geosite: &quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat&quot;
  mmdb: &quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb&quot;
  asn: &quot;https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb&quot;

# ========== 流量嗅探 ==========
sniffer:
  enable: true
  sniff:
    HTTP:
      ports: [80, 8080-8880]
      override-destination: true
    TLS:
      ports: [443, 8443]
    QUIC:
      ports: [443, 8443]
  skip-domain:
    - &quot;Mijia Cloud&quot;
    - &quot;+.push.apple.com&quot;

# ========== TUN 透明代理 ==========
tun:
  enable: true
  stack: mixed
  dns-hijack:
    - &quot;any:53&quot;
    - &quot;tcp://any:53&quot;
  auto-route: true
  auto-redirect: true
  auto-detect-interface: true

# ========== DNS 配置 ==========
dns:
  enable: true
  ipv6: true
  respect-rules: true
  enhanced-mode: fake-ip
  fake-ip-filter:
    - &quot;*&quot;
    - &quot;+.lan&quot;
    - &quot;+.local&quot;
    - &quot;+.market.xiaomi.com&quot;
  nameserver:
    - https://120.53.53.53/dns-query
    - https://223.5.5.5/dns-query
  proxy-server-nameserver:
    - https://120.53.53.53/dns-query
    - https://223.5.5.5/dns-query
  nameserver-policy:
    &quot;geosite:cn,private&quot;:
      - https://120.53.53.53/dns-query
      - https://223.5.5.5/dns-query
    &quot;geosite:geolocation-!cn&quot;:
      - &quot;https://dns.cloudflare.com/dns-query&quot;
      - &quot;https://dns.google/dns-query&quot;

# ========== 代理提供者（订阅） ==========
proxy-providers:
  机场A:
    type: http
    url: &quot;YOUR_SUBSCRIPTION_URL_1&quot;          # 替换为实际订阅链接
    interval: 86400
    health-check:
      enable: true
      url: &quot;https://www.gstatic.com/generate_204&quot;
      interval: 300
    override:
      additional-prefix: &quot;[机场A]&quot;
  机场B:
    type: http
    url: &quot;YOUR_SUBSCRIPTION_URL_2&quot;          # 替换为实际订阅链接
    interval: 86400
    health-check:
      enable: true
      url: &quot;https://www.gstatic.com/generate_204&quot;
      interval: 300
    override:
      additional-prefix: &quot;[机场B]&quot;

# ========== 直连节点 ==========
proxies:
  - name: &quot;直连&quot;
    type: direct
    udp: true

# ========== 广告拦截规则集（YAML 格式，保留） ==========
rule-providers:
  AD:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/AD.yaml&quot;
    path: ./rules/AD.yaml
  EasyList:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/EasyList.yaml&quot;
    path: ./rules/EasyList.yaml
  EasyListChina:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/EasyListChina.yaml&quot;
    path: ./rules/EasyListChina.yaml
  EasyPrivacy:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/EasyPrivacy.yaml&quot;
    path: ./rules/EasyPrivacy.yaml
  ProgramAD:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/ProgramAD.yaml&quot;
    path: ./rules/ProgramAD.yaml

# ========== 代理分组 ==========
proxy-groups:
  # 顶级手动分组
  - name: 默认
    type: select
    proxies:
      - 自动选择
      - 全部节点
      - 负载均衡(散列)
      - 负载均衡(轮询)
      - 故障转移
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区

  # 地区分组（手动选择，带复杂正则过滤）
  # 选择使用 include-all: true（所有提供者）或 use（指定提供者）
  - name: 香港
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)港|hk|hongkong|hong kong&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 台湾
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)台|tw|taiwan&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 日本
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)日|jp|japan&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 美国
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)美|us|unitedstates|united states&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 新加坡
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)(新|sg|singapore)&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 其它地区
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)^(?!.*(?:🇭🇰|🇯🇵|🇺🇸|🇸🇬|🇨🇳|港|hk|hongkong|台|tw|taiwan|日|jp|japan|新|sg|singapore|美|us|unitedstates)).*&quot;

  # 自动优选分组
  - name: 自动选择
    type: url-test
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    tolerance: 10
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  # 全部节点（手动选择具体节点）
  - name: 全部节点
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B

  # 负载均衡分组
  - name: 负载均衡(散列)
    type: load-balance
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    strategy: consistent-hashing

  - name: 负载均衡(轮询)
    type: load-balance
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    strategy: round-robin

  # 故障转移
  - name: 故障转移
    type: fallback
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B

  # 国内流量分组（默认直连，但可手动切换）
  - name: 国内
    type: select
    proxies:
      - 直连
      - 默认
      - 自动选择
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  # ---- 服务分组 ----
  - name: Google
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: Telegram
    type: url-test
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    url: &quot;https://www.gstatic.com/generate_204&quot;
    interval: 300
    tolerance: 50
    filter: &quot;(?i)(港|hk|hongkong|hong kong|台|tw|taiwan|日|jp|japan|美|us|unitedstates|united states|新|sg|singapore)&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: Twitter
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: 哔哩哔哩
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: 巴哈姆特
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: YouTube
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: NETFLIX
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: Spotify
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: Github
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: 其他
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

# ========== 规则 ==========
rules:
  # 广告拦截（优先）
  - RULE-SET,AD,REJECT
  - RULE-SET,EasyList,REJECT
  - RULE-SET,EasyListChina,REJECT
  - RULE-SET,EasyPrivacy,REJECT
  - RULE-SET,ProgramAD,REJECT

  # 私有 IP 直连
  - GEOIP,private,直连,no-resolve

  # 域名规则（使用内置 geosite）
  - GEOSITE,google,Google
  - GEOSITE,telegram,Telegram
  - GEOSITE,twitter,Twitter
  - GEOSITE,youtube,YouTube
  - GEOSITE,bilibili,哔哩哔哩
  - GEOSITE,bahamut,巴哈姆特
  - GEOSITE,netflix,NETFLIX
  - GEOSITE,spotify,Spotify
  - GEOSITE,github,Github
  - GEOSITE,cn,国内
  - GEOSITE,geolocation-!cn,其他

  # IP 规则（仅保留国家/地区级别）
  - GEOIP,CN,国内,no-resolve

  # 最终兜底
  - MATCH,其他
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.基于外部 rule-providers 的 Mihomo 配置文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ========== 基础配置 ==========
mixed-port: 7890
ipv6: true
allow-lan: true
unified-delay: false
tcp-concurrent: true
external-controller: 0.0.0.0:9090      # 允许局域网访问面板
external-ui: ui
external-ui-url: &quot;https://ghproxy.cn/github.com/MetaCubeX/metacubexd/archive/gh-pages.zip&quot;

find-process-mode: strict
global-client-fingerprint: chrome

profile:
  store-selected: true
  store-fake-ip: true

# ========== 流量嗅探 ==========
sniffer:
  enable: true
  sniff:
    HTTP:
      ports: [80, 8080-8880]
      override-destination: true
    TLS:
      ports: [443, 8443]
    QUIC:
      ports: [443, 8443]
  skip-domain:
    - &quot;Mijia Cloud&quot;
    - &quot;+.push.apple.com&quot;

# ========== TUN 透明代理 ==========
tun:
  enable: true
  stack: mixed
  dns-hijack:
    - &quot;any:53&quot;
    - &quot;tcp://any:53&quot;
  auto-route: true
  auto-redirect: true
  auto-detect-interface: true

# ========== DNS 配置 ==========
dns:
  enable: true
  ipv6: true
  respect-rules: true
  enhanced-mode: fake-ip
  fake-ip-filter:
    - &quot;*&quot;
    - &quot;+.lan&quot;
    - &quot;+.local&quot;
    - &quot;+.market.xiaomi.com&quot;
  nameserver:
    - https://120.53.53.53/dns-query
    - https://223.5.5.5/dns-query
  proxy-server-nameserver:
    - https://120.53.53.53/dns-query
    - https://223.5.5.5/dns-query
  nameserver-policy:
    &quot;rule-set:cn_domain,private_domain&quot;:
      - https://120.53.53.53/dns-query
      - https://223.5.5.5/dns-query
    &quot;rule-set:geolocation-!cn&quot;:
      - &quot;https://dns.cloudflare.com/dns-query&quot;
      - &quot;https://dns.google/dns-query&quot;

# ========== 代理提供者（订阅） ==========
proxy-providers:
  机场A:
    type: http
    url: &quot;YOUR_SUBSCRIPTION_URL_1&quot;          # 替换为实际订阅链接
    interval: 86400
    health-check:
      enable: true
      url: &quot;https://www.gstatic.com/generate_204&quot;
      interval: 300
    override:
      additional-prefix: &quot;[机场A]&quot;
  机场B:
    type: http
    url: &quot;YOUR_SUBSCRIPTION_URL_2&quot;          # 替换为实际订阅链接
    interval: 86400
    health-check:
      enable: true
      url: &quot;https://www.gstatic.com/generate_204&quot;
      interval: 300
    override:
      additional-prefix: &quot;[机场B]&quot;

# ========== 直连节点 ==========
proxies:
  - name: &quot;直连&quot;
    type: direct
    udp: true

# ========== 规则集提供者（使用锚点简化） ==========
rule-anchor:
  ip: &amp;amp;ip {type: http, interval: 86400, behavior: ipcidr, format: mrs}
  domain: &amp;amp;domain {type: http, interval: 86400, behavior: domain, format: mrs}

rule-providers:
  # ---- 广告拦截（YAML 格式，可自行替换为 MRS） ----
  AD:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/AD.yaml&quot;
    path: ./rules/AD.yaml
  EasyList:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/EasyList.yaml&quot;
    path: ./rules/EasyList.yaml
  EasyListChina:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/EasyListChina.yaml&quot;
    path: ./rules/EasyListChina.yaml
  EasyPrivacy:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/EasyPrivacy.yaml&quot;
    path: ./rules/EasyPrivacy.yaml
  ProgramAD:
    type: http
    interval: 86400
    behavior: domain
    url: &quot;https://raw.githubusercontent.com/earoftoast/clash-rules/main/ProgramAD.yaml&quot;
    path: ./rules/ProgramAD.yaml

  # ---- 常用域名规则集（MRS 格式） ----
  private_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/private.mrs&quot;
  cn_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/cn.mrs&quot;
  google_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/google.mrs&quot;
  telegram_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/telegram.mrs&quot;
  twitter_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/twitter.mrs&quot;
  youtube_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/youtube.mrs&quot;
  bilibili_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/bilibili.mrs&quot;
  bahamut_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/bahamut.mrs&quot;
  netflix_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/netflix.mrs&quot;
  spotify_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/spotify.mrs&quot;
  github_domain:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/github.mrs&quot;
  geolocation-!cn:
    &amp;lt;&amp;lt;: *domain
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/geolocation-!cn.mrs&quot;

  # ---- IP 规则集（MRS 格式） ----
  private_ip:
    &amp;lt;&amp;lt;: *ip
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/private.mrs&quot;
  cn_ip:
    &amp;lt;&amp;lt;: *ip
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/cn.mrs&quot;
  google_ip:
    &amp;lt;&amp;lt;: *ip
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/google.mrs&quot;
  telegram_ip:
    &amp;lt;&amp;lt;: *ip
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/telegram.mrs&quot;
  twitter_ip:
    &amp;lt;&amp;lt;: *ip
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/twitter.mrs&quot;
  netflix_ip:
    &amp;lt;&amp;lt;: *ip
    url: &quot;https://ghproxy.cn/https://raw.github.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/netflix.mrs&quot;

# ========== 代理分组 ==========
proxy-groups:
  # 顶级手动分组
  - name: 默认
    type: select
    proxies:
      - 自动选择
      - 全部节点
      - 负载均衡(散列)
      - 负载均衡(轮询)
      - 故障转移
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区

  # 地区分组（手动选择，带复杂正则过滤）
  # 选择使用 include-all: true（所有提供者）或 use（指定提供者）
  - name: 香港
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)港|hk|hongkong|hong kong&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 台湾
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)台|tw|taiwan&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 日本
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)日|jp|japan&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 美国
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)美|us|unitedstates|united states&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 新加坡
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)(新|sg|singapore)&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: 其它地区
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    filter: &quot;(?i)^(?!.*(?:🇭🇰|🇯🇵|🇺🇸|🇸🇬|🇨🇳|港|hk|hongkong|台|tw|taiwan|日|jp|japan|新|sg|singapore|美|us|unitedstates)).*&quot;

  # 自动优选分组
  - name: 自动选择
    type: url-test
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    tolerance: 10
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  # 全部节点（手动选择具体节点）
  - name: 全部节点
    type: select
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B

  # 负载均衡分组
  - name: 负载均衡(散列)
    type: load-balance
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    strategy: consistent-hashing

  - name: 负载均衡(轮询)
    type: load-balance
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    strategy: round-robin

  # 故障转移
  - name: 故障转移
    type: fallback
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B

  # 国内流量分组（默认直连，但可手动切换）
  - name: 国内
    type: select
    proxies:
      - 直连
      - 默认
      - 自动选择
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  # ---- 服务分组 ----
  - name: Google
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: Telegram
    type: url-test
    # include-all: true
    # use:
    #   - 机场A
    #   - 机场B
    url: &quot;https://www.gstatic.com/generate_204&quot;
    interval: 300
    tolerance: 50
    filter: &quot;(?i)(港|hk|hongkong|hong kong|台|tw|taiwan|日|jp|japan|美|us|unitedstates|united states|新|sg|singapore)&quot;
    exclude-filter: &quot;(?i)回国|安徽|北京|重庆|福建|甘肃|广东|广西|贵州|海南|河北|黑龙江|河南|湖北|湖南|吉林|江苏|江西|辽宁|内蒙古|宁夏|青海|山东|山西|陕西|上海|四川|天津|西藏|新疆|云南|浙江|官网|国内|国际|移动|联通|电信|cn|china&quot;

  - name: Twitter
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: 哔哩哔哩
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: 巴哈姆特
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: YouTube
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: NETFLIX
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: Spotify
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: Github
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

  - name: 其他
    type: select
    proxies:
      - 默认
      - 自动选择
      - 直连
      - 香港
      - 台湾
      - 日本
      - 新加坡
      - 美国
      - 其它地区
      - 全部节点

# ========== 规则 ==========
rules:
  # 广告拦截（优先）
  - RULE-SET,AD,REJECT
  - RULE-SET,EasyList,REJECT
  - RULE-SET,EasyListChina,REJECT
  - RULE-SET,EasyPrivacy,REJECT
  - RULE-SET,ProgramAD,REJECT

  # 私有 IP 直连
  - RULE-SET,private_ip,直连,no-resolve

  # 域名规则
  - RULE-SET,google_domain,Google
  - RULE-SET,telegram_domain,Telegram
  - RULE-SET,twitter_domain,Twitter
  - RULE-SET,youtube_domain,YouTube
  - RULE-SET,bilibili_domain,哔哩哔哩
  - RULE-SET,bahamut_domain,巴哈姆特
  - RULE-SET,netflix_domain,NETFLIX
  - RULE-SET,spotify_domain,Spotify
  - RULE-SET,github_domain,Github
  - RULE-SET,cn_domain,国内
  - RULE-SET,geolocation-!cn,其他

  # IP 规则
  - RULE-SET,google_ip,Google,no-resolve
  - RULE-SET,telegram_ip,Telegram,no-resolve
  - RULE-SET,twitter_ip,Twitter,no-resolve
  - RULE-SET,netflix_ip,NETFLIX,no-resolve
  - RULE-SET,cn_ip,国内,no-resolve

  # 最终兜底
  - MATCH,其他
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用 Systemd 部署 Mihomo</title><link>https://www.daitcc.top/posts/f61ce0a5/</link><guid isPermaLink="true">https://www.daitcc.top/posts/f61ce0a5/</guid><description>本文详细介绍了如何使用 systemd 在 Linux 系统上部署和管理 mihomo 代理内核。内容涵盖：下载安装内核、创建安全强化（沙箱）的 systemd 服务单元、创建专用系统用户并设置目录权限、启动服务并设置开机自启，以及可选地通过配置文件自动部署 Web 管理面板（MetaCubeXD），实现浏览器端的可视化控制。通过这套方案，可确保 mihomo 以最小权限原则运行，并具备自动重启、日志查看、配置重载等系统级管理功能。</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;使用 Systemd 部署 Mihomo&lt;/h1&gt;
&lt;h2&gt;一、安装内核与准备配置&lt;/h2&gt;
&lt;p&gt;在将 &lt;strong&gt;mihomo&lt;/strong&gt; 交给 &lt;strong&gt;systemd&lt;/strong&gt; 管理前，需要先把它的&quot;骨架&quot;搭建好。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;下载并安装 mihomo 内核&lt;/strong&gt;：
从 &lt;strong&gt;mihomo&lt;/strong&gt; 的&lt;a href=&quot;https://github.com/MetaCubeX/mihomo/releases&quot;&gt;官方发布页面&lt;/a&gt;下载对应你系统架构（如 &lt;code&gt;linux-amd64&lt;/code&gt;）的压缩包。解压后，将二进制文件复制到系统的程序目录并赋予执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 假设你已经将文件解压并重命名为 mihomo
sudo cp mihomo /usr/local/bin/
sudo chmod +x /usr/local/bin/mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建配置目录和文件&lt;/strong&gt;：
mihomo 默认会从 &lt;code&gt;/etc/mihomo&lt;/code&gt; 目录读取配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，你需要创建一个核心配置文件 &lt;code&gt;/etc/mihomo/config.yaml&lt;/code&gt; 。你可以将从你的服务商获取的订阅链接内容，或一个基础的配置文件模板放入其中 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二、创建 systemd 配置文件&lt;/h2&gt;
&lt;p&gt;创建 systemd 配置文件 &lt;code&gt;/etc/systemd/system/mihomo.service&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/systemd/system/mihomo.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，粘贴以下配置内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=mihomo Daemon
# 等待网络完全就绪后再启动，避免启动时网络不可用
After=network-online.target
Wants=network-online.target

[Service]
# 以普通用户身份运行，最小权限原则
User=mihomo
Group=mihomo

# 资源限制
Type=simple
LimitNPROC=500
LimitNOFILE=1000000

# 官方推荐
#CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
#AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE

# 限制：仅保留运行TUN模式和绑定低端口所必需的能力
# 移除了高危的 CAP_DAC_OVERRIDE, CAP_SYS_PTRACE 等
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE

# 启用一系列安全沙箱选项，进一步隔离服务
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
# 允许写入的数据目录（如缓存、持久化连接）
ReadWritePaths=/var/lib/mihomo
# 允许写入内核文件所在目录，以便面板能够更新内核
ReadWritePaths=/usr/local/bin
# 允许写入配置文件所在目录，以便面板能够更新配置
ReadWritePaths=/etc/mihomo
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

# 服务行为
Restart=always
ExecStart=/usr/local/bin/mihomo -d /etc/mihomo
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置解读&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;[Unit] 单元描述与依赖&lt;/li&gt;
&lt;/ol&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;Description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mihomo Daemon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务的简要描述，便于管理员识别。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;After&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;network-online.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;声明此服务应在网络完全就绪的 target 之后启动，避免启动时网络不可用导致失败。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Wants&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;network-online.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;表示希望 network-online.target 被激活，但并不严格要求其成功；通常与 &lt;code&gt;After&lt;/code&gt; 配合使用以确保网络已配置好。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;[Service] 服务运行定义&lt;/li&gt;
&lt;/ol&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;strong&gt;用户与组&lt;/strong&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;User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mihomo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以非特权系统用户 &lt;code&gt;mihomo&lt;/code&gt; 的身份运行服务，避免使用 root 账户，实现最小权限原则。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mihomo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务运行时所属的组，同样为非特权组。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;服务类型与资源限制&lt;/strong&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;Type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认类型，表示 &lt;code&gt;ExecStart&lt;/code&gt; 启动的进程就是主进程，systemd 会将其作为服务的主控进程。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LimitNPROC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限制服务及其子进程的最大进程/线程数为 500。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LimitNOFILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限制文件描述符数量为 100 万，满足高并发网络连接需求。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linux Capabilities（能力集）&lt;/strong&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;CapabilityBoundingSet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限制进程可拥有的能力边界，超出此集合的能力即使通过 &lt;code&gt;exec&lt;/code&gt; 也无法获得。此处仅保留&lt;strong&gt;网络管理&lt;/strong&gt;、&lt;strong&gt;原始套接字&lt;/strong&gt;和&lt;strong&gt;绑定特权端口&lt;/strong&gt;所需的能力。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AmbientCapabilities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;td&gt;在 &lt;code&gt;exec&lt;/code&gt; 后仍保留上述能力，使得非 root 用户启动的进程也能保持这些能力。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;安全沙箱选项&lt;/strong&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;NoNewPrivileges&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁止进程及其子进程通过 &lt;code&gt;execve&lt;/code&gt; 获得新特权（如通过 setuid 或文件系统能力），即使文件有 suid 位也无效，增强安全性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PrivateTmp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为服务创建独立的临时文件空间 &lt;code&gt;/tmp&lt;/code&gt; 和 &lt;code&gt;/var/tmp&lt;/code&gt;，与其他进程隔离，防止临时文件信息泄露。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProtectSystem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以最严格模式保护系统文件：将 &lt;code&gt;/usr&lt;/code&gt;、&lt;code&gt;/boot&lt;/code&gt;、&lt;code&gt;/etc&lt;/code&gt; 挂载为只读，服务无法修改这些关键系统目录。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReadWritePaths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/lib/mihomo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在 &lt;code&gt;ProtectSystem=strict&lt;/code&gt; 下，允许服务对指定目录（如数据目录）拥有写入权限。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReadWritePaths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/local/bin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许面板更新内核文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProtectHome&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将 &lt;code&gt;/home&lt;/code&gt;、&lt;code&gt;/root&lt;/code&gt; 和 &lt;code&gt;/run/user&lt;/code&gt; 挂载为不可访问（返回空或隐藏），保护用户隐私数据。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProtectKernelTunables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁止修改内核可调参数（&lt;code&gt;/sys/...&lt;/code&gt;、&lt;code&gt;/proc/sys/...&lt;/code&gt;），防止内核配置被篡改。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProtectKernelModules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁止加载或卸载内核模块，防止内核模块攻击。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProtectControlGroups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁止修改 cgroup（控制组）配置，防止资源隔离被破坏。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;服务行为&lt;/strong&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;Restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无论退出原因（正常退出或异常崩溃），服务总是自动重启，确保高可用性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExecStart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/local/bin/mihomo -d /etc/mihomo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动 mihomo 主程序，&lt;code&gt;-d&lt;/code&gt; 指定配置目录为 &lt;code&gt;/etc/mihomo&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExecReload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/bin/kill -HUP $MAINPID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当执行 &lt;code&gt;systemctl reload mihomo&lt;/code&gt; 时，向主进程发送 HUP 信号，触发配置重载（需 mihomo 支持）。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;[Install] 安装与自启&lt;/li&gt;
&lt;/ol&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;WantedBy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;multi-user.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;表示该服务应在系统进入多用户运行级别（通常为正常启动后）时自动启动。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;三、创建用户与启动服务&lt;/h2&gt;
&lt;p&gt;配置文件写好后，需要创建对应的用户并启动服务。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建专用的系统用户&lt;/strong&gt;：
执行以下命令创建 &lt;code&gt;mihomo&lt;/code&gt; 用户和组。&lt;code&gt;--system&lt;/code&gt; 表示创建系统用户，&lt;code&gt;--no-create-home&lt;/code&gt; 表示不创建 home 目录（我们用不到）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo groupadd --system mihomo
sudo useradd --system --gid mihomo --no-create-home --shell /usr/sbin/nologin mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置配置目录的权限&lt;/strong&gt;：
确保 &lt;code&gt;mihomo&lt;/code&gt; 用户能读取配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chown -R root:mihomo /etc/mihomo
sudo chmod 750 /etc/mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的服务配置了 &lt;code&gt;ReadWritePaths=/var/lib/mihomo&lt;/code&gt;，还需要创建该目录并授予权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /var/lib/mihomo
sudo chown mihomo:mihomo /var/lib/mihomo
sudo chmod 750 /var/lib/mihomo

sudo chown mihomo:mihomo /usr/local/bin/mihomo
sudo chmod 755 /usr/local/bin/mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动服务并设置开机自启&lt;/strong&gt;：
现在，可以通知 &lt;strong&gt;systemd&lt;/strong&gt; 重载配置，并启动服务了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 重新加载 systemd 管理器配置
sudo systemctl daemon-reload
# 设置开机自启
sudo systemctl enable mihomo
# 立即启动服务
sudo systemctl start mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;四、日常管理与维护&lt;/h2&gt;
&lt;p&gt;服务运行起来后，你可以通过以下命令来管理它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看状态&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看日志&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo journalctl -u mihomo -f -n 50
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-f&lt;/code&gt; 表示实时跟踪，&lt;code&gt;-n 50&lt;/code&gt; 表示显示最后50行日志。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;停止/重启&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl stop mihomo
sudo systemctl restart mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重新加载配置&lt;/strong&gt;（发送 &lt;code&gt;HUP&lt;/code&gt; 信号）：
修改了 &lt;code&gt;config.yaml&lt;/code&gt; 后，可以优雅地重载配置而不用中断服务 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl reload mihomo
# 或者
sudo kill -HUP $(pidof mihomo)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;五、自动部署管理面板（可选）&lt;/h2&gt;
&lt;p&gt;mihomo 支持在启动时自动下载并部署 Web 管理面板（如 MetaCubeXD），让你可以通过浏览器直观地查看日志、切换节点、测试延迟等。只需在配置文件中添加几行配置，mihomo 就会在启动时自动完成面板的下载、解压和挂载。&lt;/p&gt;
&lt;h3&gt;1. 修改核心配置文件&lt;/h3&gt;
&lt;p&gt;编辑 &lt;code&gt;/etc/mihomo/config.yaml&lt;/code&gt;，在适当位置（通常在 &lt;code&gt;external-controller&lt;/code&gt; 相关配置段）添加以下三行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;external-controller: 0.0.0.0:9090      # 必须：开启外部控制 API，监听所有网卡的 9090 端口
external-ui: ui                         # 指定存放面板文件的目录名（相对路径，相对于工作目录 /etc/mihomo）
external-ui-url: &quot;https://ghproxy.cn/github.com/MetaCubeX/metacubexd/archive/gh-pages.zip&quot; # 面板的下载地址
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;external-controller&lt;/code&gt;：开启 RESTful API，mihomo 会监听此地址，供面板调用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;external-ui&lt;/code&gt;：面板静态文件的存放目录。此处使用相对路径 &lt;code&gt;ui&lt;/code&gt;，即 mihomo 将在工作目录 &lt;code&gt;/etc/mihomo&lt;/code&gt; 下创建 &lt;code&gt;ui&lt;/code&gt; 文件夹。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;external-ui-url&lt;/code&gt;：面板压缩包的下载链接。mihomo 启动时会自动从该地址下载并解压到 &lt;code&gt;ui&lt;/code&gt; 目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 确保面板目录可写&lt;/h3&gt;
&lt;p&gt;由于 &lt;code&gt;external-ui&lt;/code&gt; 指定的目录（&lt;code&gt;/etc/mihomo/ui&lt;/code&gt;）需要由 &lt;code&gt;mihomo&lt;/code&gt; 用户创建和写入文件，因此必须确保该用户对 &lt;code&gt;/etc/mihomo&lt;/code&gt; 目录有写入权限。&lt;/p&gt;
&lt;p&gt;在之前配置的 systemd 服务中，我们启用了严格的安全保护（&lt;code&gt;ProtectSystem=strict&lt;/code&gt;），默认会将 &lt;code&gt;/etc&lt;/code&gt; 挂载为只读。为了让 &lt;code&gt;mihomo&lt;/code&gt; 能在 &lt;code&gt;/etc/mihomo&lt;/code&gt; 下创建 &lt;code&gt;ui&lt;/code&gt; 文件夹并写入面板文件，&lt;strong&gt;有两种解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;h4&gt;方案 A（推荐）：修改 systemd 服务，允许写入 &lt;code&gt;/etc/mihomo&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;编辑 &lt;code&gt;/etc/systemd/system/mihomo.service&lt;/code&gt;，在 &lt;code&gt;[Service]&lt;/code&gt; 部分的 &lt;code&gt;ReadWritePaths&lt;/code&gt; 中添加一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReadWritePaths=/etc/mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样既保留了大部分系统目录的只读保护，又单独放行了配置目录。&lt;/p&gt;
&lt;h4&gt;方案 B（更安全）：将面板目录指向已有可写路径&lt;/h4&gt;
&lt;p&gt;如果你不希望放宽对 &lt;code&gt;/etc&lt;/code&gt; 的保护，可以将 &lt;code&gt;external-ui&lt;/code&gt; 设置为一个已经允许写入的绝对路径，例如 &lt;code&gt;/var/lib/mihomo/ui&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;external-ui: /var/lib/mihomo/ui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并确保该目录存在且属主为 &lt;code&gt;mihomo&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /var/lib/mihomo/ui
sudo chown mihomo:mihomo /var/lib/mihomo/ui
sudo chmod 750 /var/lib/mihomo/ui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;code&gt;/var/lib/mihomo&lt;/code&gt; 已在之前的 systemd 配置中通过 &lt;code&gt;ReadWritePaths&lt;/code&gt; 开放了写入权限，因此无需修改服务文件。&lt;/p&gt;
&lt;h3&gt;3. 防火墙放行 9090 端口&lt;/h3&gt;
&lt;p&gt;面板需要通过 &lt;code&gt;9090&lt;/code&gt; 端口访问，请确保防火墙允许外部设备连接该端口。&lt;/p&gt;
&lt;p&gt;如果使用 &lt;code&gt;firewalld&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --permanent --add-port=9090/tcp
sudo firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用 &lt;code&gt;ufw&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 9090/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 重启服务并验证&lt;/h3&gt;
&lt;p&gt;重新加载配置并重启 mihomo 服务（&lt;code&gt;reload&lt;/code&gt; 可能不会触发面板下载，建议直接 &lt;code&gt;restart&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart mihomo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看日志，确认面板是否下载成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo journalctl -u mihomo -f -n 50
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果日志中出现类似以下信息，说明面板已自动部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;... downloading ui from https://ghproxy.cn/github.com/MetaCubeX/metacubexd/archive/gh-pages.zip ...
... extracted ui to /etc/mihomo/ui
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 访问面板&lt;/h3&gt;
&lt;p&gt;在浏览器中输入 &lt;code&gt;http://&amp;lt;你的Linux设备IP&amp;gt;:9090/ui&lt;/code&gt;，即可打开管理面板。首次访问时，面板可能需要与 mihomo 建立连接，请确保 API 地址正确（默认为面板所在设备的 &lt;strong&gt;&amp;lt;你的Linux设备IP&amp;gt;:9090&lt;/strong&gt;）。&lt;/p&gt;
&lt;h3&gt;6.故障排查&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;面板无法访问&lt;/strong&gt;：检查防火墙是否放行 9090 端口；确认 &lt;code&gt;external-controller&lt;/code&gt; 配置正确且 mihomo 正常运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;面板未自动下载&lt;/strong&gt;：检查 &lt;code&gt;external-ui-url&lt;/code&gt; 是否可访问（可手动在服务器上用 &lt;code&gt;curl -I&lt;/code&gt; 测试）；确认 &lt;code&gt;mihomo&lt;/code&gt; 用户对目标目录有写入权限；查看日志中是否有权限错误或下载失败信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动部署面板&lt;/strong&gt;：如果自动下载失败，可以手动从 &lt;a href=&quot;https://github.com/MetaCubeX/metacubexd&quot;&gt;MetaCubeX/metacubexd&lt;/a&gt; 仓库下载 &lt;code&gt;gh-pages.zip&lt;/code&gt;，解压到指定的 &lt;code&gt;ui&lt;/code&gt; 目录（确保 &lt;code&gt;index.html&lt;/code&gt; 等文件直接位于目录内），然后重启服务。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用GitHub Actions 安全部署到阿里云 ECS</title><link>https://www.daitcc.top/posts/6d5ae3ef/</link><guid isPermaLink="true">https://www.daitcc.top/posts/6d5ae3ef/</guid><description>本文介绍了一种通过 GitHub Actions 安全部署到阿里云 ECS 的方案：在自动化部署时，动态获取当前运行器的公网 IP，临时将其加入安全组的 22 端口放行规则中，部署完成后立即自动删除该规则，从而避免长期开放端口带来的安全风险。</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;
&lt;h2&gt;使用GitHub Actions 安全部署到阿里云 ECS&lt;/h2&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;::github{repo=&quot;daitcl/Secure-Deploy-to-Aliyun-ECS&quot;}&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;2026年4月21日年修复报错 &lt;code&gt;&apos;AuthorizeSecurityGroup&apos; is not a valid api&lt;/code&gt; 的问题，添加了 &lt;code&gt;--version 2014-05-26 --force&lt;/code&gt; 参数，彻底解决了全新环境下首次调用阿里云 CLI 时出现的权限校验失败问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 概述&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：在 GitHub Actions 运行部署任务时，自动获取运行器的公网 IP，通过阿里云 API 将该 IP 临时加入安全组（仅允许访问 22 端口），部署完成后立即删除该规则，从而避免长期开放端口带来的安全风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：避免长期开放 22 端口，大幅降低被扫描和暴力破解的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：需要从 GitHub Actions 安全地 SSH 登录阿里云 ECS 进行部署的任何项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2. 准备工作：阿里云端配置&lt;/h3&gt;
&lt;h4&gt;2.1 确认 ECS 实例和安全组信息&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;登录阿里云控制台，进入 &lt;strong&gt;ECS 管理页面&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;找到目标实例，记录以下信息（后续需要填入 GitHub Secrets）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实例所属地域 ID&lt;/strong&gt;（例如 &lt;code&gt;cn-hangzhou&lt;/code&gt;，详见第 5 节表格）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实例绑定的安全组 ID&lt;/strong&gt;（例如 &lt;code&gt;sg-xxxxxx&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;进入该安全组的 &lt;strong&gt;入方向规则&lt;/strong&gt; 页面，&lt;strong&gt;删除所有允许 &lt;code&gt;0.0.0.0/0&lt;/code&gt; 访问 22 端口的规则&lt;/strong&gt;。目前可以保持为空，或仅保留内网访问规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2.2 创建 RAM 子账号并授权&lt;/h4&gt;
&lt;p&gt;为 GitHub Actions 创建一个专用的 RAM 子用户，并授予管理安全组的权限。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://ram.console.aliyun.com/&quot;&gt;RAM 控制台&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;创建用户：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;登录名称&lt;/strong&gt;：例如 &lt;code&gt;github-actions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问配置&lt;/strong&gt;：勾选 &lt;strong&gt;使用永久 AccessKey 访问&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AccessKey&lt;/strong&gt;：勾选 &lt;strong&gt;我确认必须创建 AccessKey&lt;/strong&gt;（系统将生成 AccessKey ID 和 Secret）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存 AccessKey ID 和 Secret&lt;/strong&gt;（务必立即保存，关闭后将无法再次查看）。&lt;/li&gt;
&lt;li&gt;为该用户授权（&lt;strong&gt;新增授权&lt;/strong&gt;）：
&lt;ul&gt;
&lt;li&gt;添加权限 &lt;strong&gt;AliyunECSFullAccess&lt;/strong&gt;（系统策略，允许管理 ECS 资源，包括安全组）。&lt;/li&gt;
&lt;li&gt;若需更精细的权限，可参考 8.3 节自定义策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;3. GitHub 仓库配置 Secrets&lt;/h3&gt;
&lt;p&gt;在 GitHub 仓库的 &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt; 中添加以下敏感信息：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret 名称&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;ALIYUN_ACCESS_KEY_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上一步创建的 RAM 用户的 AccessKey ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALIYUN_ACCESS_KEY_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对应的 AccessKey Secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALIYUN_REGION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ECS 实例所在地域 ID（例如 &lt;code&gt;cn-hangzhou&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALIYUN_SECURITY_GROUP_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安全组 ID（例如 &lt;code&gt;sg-xxxxxx&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你还需要通过 SSH 部署，请额外添加以下 Secrets（可选）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REMOTE_HOST&lt;/code&gt;：ECS 公网 IP 或域名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REMOTE_USER&lt;/code&gt;：SSH 用户名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt;：SSH 私钥&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;4. 通用 GitHub Actions 工作流模板&lt;/h3&gt;
&lt;p&gt;将以下代码片段放入你的工作流文件中（如 &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;），并在 &lt;code&gt;# 在这里插入你的构建/部署步骤&lt;/code&gt; 处替换为你自己的任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Secure Deploy to Aliyun ECS

on:
  push:
    branches: [ main ]   # 触发分支，可按需修改

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # ---------- 1. 获取当前运行器的公网 IP ----------
      - name: Get Runner Public IP
        id: ip
        run: |
          RUNNER_IP=$(curl -s http://ifconfig.me)
          echo &quot;CURRENT_IP=$RUNNER_IP&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT
          echo &quot;Runner IP: $RUNNER_IP&quot;

      # ---------- 2. 安装阿里云 CLI（二进制方式）----------
      - name: Install Aliyun CLI
        run: |
          curl -LO &quot;https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz&quot;
          tar -xzf aliyun-cli-linux-latest-amd64.tgz
          mkdir -p $HOME/.local/bin
          mv aliyun $HOME/.local/bin/
          echo &quot;$HOME/.local/bin&quot; &amp;gt;&amp;gt; $GITHUB_PATH
          aliyun --version

      # ---------- 3. 临时放行当前 IP（22 端口）----------
      - name: Authorize Security Group (Add IP)
        env:
          ALIBABA_CLOUD_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
          ALIBABA_CLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
          ALIBABA_CLOUD_REGION_ID: ${{ secrets.ALIYUN_REGION }}
        run: |
          aliyun ecs AuthorizeSecurityGroup \
            --RegionId ${{ secrets.ALIYUN_REGION }} \
            --SecurityGroupId ${{ secrets.ALIYUN_SECURITY_GROUP_ID }} \
            --IpProtocol tcp \
            --PortRange 22/22 \
            --SourceCidrIp ${{ steps.ip.outputs.CURRENT_IP }}/32 \
            --Policy accept \
            --NicType intranet \
            --version 2014-05-26 \
            --force

      # （可选）等待几秒让规则生效
      - name: Wait for rule to take effect
        run: sleep 5

      # ---------- 在这里插入你的构建/部署步骤 ----------
      # 例如：
      # - name: Build and Deploy
      #   run: |
      #     echo &quot;Your deployment commands here&quot;
      #   # 或使用 SSH Action：
      # # ---------- 验证 SSH 访问成功 ----------
      # - name: Verify SSH Connection
      #   uses: appleboy/ssh-action@v1.0.3
      #   with:
      #     host: ${{ secrets.REMOTE_HOST }}          # ECS 公网 IP 或域名
      #     username: ${{ secrets.REMOTE_USER }}      # 登录用户名（如 root 或普通用户）
      #     key: ${{ secrets.SSH_PRIVATE_KEY }}       # 私钥内容（需在 secrets 中配置）
      #     port: 22                                   # SSH 端口，默认 22
      #     script: |
      #       echo &quot;✅ SSH connection to ECS is successful!&quot;
      #       echo &quot;Hostname: $(hostname)&quot;
      #       echo &quot;Current user: $(whoami)&quot;
      #       echo &quot;Working directory: $(pwd)&quot;
      #       # 可以在此处添加更多验证命令，如检查必要目录是否存在

      # ---------- 4. 撤销放行（删除刚才添加的规则）----------
      - name: Revoke Security Group (Remove IP)
        if: always()
        env:
          ALIBABA_CLOUD_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
          ALIBABA_CLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
          ALIBABA_CLOUD_REGION_ID: ${{ secrets.ALIYUN_REGION }}
        run: |
          aliyun ecs RevokeSecurityGroup \
            --RegionId ${{ secrets.ALIYUN_REGION }} \
            --SecurityGroupId ${{ secrets.ALIYUN_SECURITY_GROUP_ID }} \
            --IpProtocol tcp \
            --PortRange 22/22 \
            --SourceCidrIp ${{ steps.ip.outputs.CURRENT_IP }}/32 \
            --NicType intranet \
            --version 2014-05-26 \
            --force || true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;5. 阿里云地域与可用区对照表&lt;/h3&gt;
&lt;p&gt;在配置 &lt;code&gt;ALIYUN_REGION&lt;/code&gt; 时，请根据你的 ECS 实例所在地域选择对应的&lt;strong&gt;地域 ID&lt;/strong&gt;（例如 &lt;code&gt;cn-hangzhou&lt;/code&gt;）。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地域名称&lt;/th&gt;
&lt;th&gt;地域ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;华北1（青岛）&lt;/td&gt;
&lt;td&gt;cn-qingdao&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华北2（北京）&lt;/td&gt;
&lt;td&gt;cn-beijing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华北3（张家口）&lt;/td&gt;
&lt;td&gt;cn-zhangjiakou&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华北5（呼和浩特）&lt;/td&gt;
&lt;td&gt;cn-huhehaote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华北6（乌兰察布）&lt;/td&gt;
&lt;td&gt;cn-wulanchabu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华东1（杭州）&lt;/td&gt;
&lt;td&gt;cn-hangzhou&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华东2（上海）&lt;/td&gt;
&lt;td&gt;cn-shanghai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华东5（南京-本地地域）&lt;/td&gt;
&lt;td&gt;cn-nanjing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华东6（福州-本地地域）&lt;/td&gt;
&lt;td&gt;cn-fuzhou&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华中1（武汉-本地地域）&lt;/td&gt;
&lt;td&gt;cn-wuhan-lr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华南1（深圳）&lt;/td&gt;
&lt;td&gt;cn-shenzhen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华南2（河源）&lt;/td&gt;
&lt;td&gt;cn-heyuan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;华南3（广州）&lt;/td&gt;
&lt;td&gt;cn-guangzhou&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;西南1（成都）&lt;/td&gt;
&lt;td&gt;cn-chengdu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中国香港&lt;/td&gt;
&lt;td&gt;cn-hongkong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新加坡&lt;/td&gt;
&lt;td&gt;ap-southeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;澳大利亚（悉尼）&lt;/td&gt;
&lt;td&gt;ap-southeast-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;马来西亚（吉隆坡）&lt;/td&gt;
&lt;td&gt;ap-southeast-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;印度尼西亚（雅加达）&lt;/td&gt;
&lt;td&gt;ap-southeast-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;菲律宾（马尼拉）&lt;/td&gt;
&lt;td&gt;ap-southeast-6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;泰国（曼谷）&lt;/td&gt;
&lt;td&gt;ap-southeast-7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;印度（孟买）&lt;/td&gt;
&lt;td&gt;ap-south-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日本（东京）&lt;/td&gt;
&lt;td&gt;ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;韩国（首尔）&lt;/td&gt;
&lt;td&gt;ap-northeast-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;美国（硅谷）&lt;/td&gt;
&lt;td&gt;us-west-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;美国（弗吉尼亚）&lt;/td&gt;
&lt;td&gt;us-east-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;德国（法兰克福）&lt;/td&gt;
&lt;td&gt;eu-central-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;英国（伦敦）&lt;/td&gt;
&lt;td&gt;eu-west-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阿联酋（迪拜）&lt;/td&gt;
&lt;td&gt;me-east-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;沙特（利雅得-合作伙伴运营）&lt;/td&gt;
&lt;td&gt;me-central-1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：安全组授权时只需要地域 ID，无需指定具体的可用区。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;6. 步骤详解&lt;/h3&gt;
&lt;h4&gt;步骤 1：获取运行器 IP&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;ifconfig.me&lt;/code&gt; 服务获取当前 GitHub Actions 运行器的公网出口 IP。&lt;/li&gt;
&lt;li&gt;将 IP 保存到 &lt;code&gt;steps.ip.outputs.CURRENT_IP&lt;/code&gt; 中，供后续步骤使用。&lt;/li&gt;
&lt;li&gt;也可使用其他服务如 &lt;code&gt;ip.sb&lt;/code&gt;、&lt;code&gt;icanhazip.com&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 2：安装阿里云 CLI&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;直接下载官方最新版 CLI 二进制文件，解压后放入 &lt;code&gt;PATH&lt;/code&gt;，避免依赖第三方 Action，更稳定可靠。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 3：添加安全组规则&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;AuthorizeSecurityGroup&lt;/code&gt; 接口，将当前 IP 添加到安全组，协议 TCP，端口 22。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--NicType intranet&lt;/code&gt; 适用于 VPC 网络的实例；如果是经典网络，可能需要改为 &lt;code&gt;internet&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新增参数说明&lt;/strong&gt;：&lt;code&gt;--version 2014-05-26&lt;/code&gt; 指定 API 版本，&lt;code&gt;--force&lt;/code&gt; 绕过本地元数据校验，两者结合可彻底解决新版 CLI 在全新环境下首次调用时出现的 &lt;code&gt;&apos;AuthorizeSecurityGroup&apos; is not a valid api&lt;/code&gt; 错误。&lt;/li&gt;
&lt;li&gt;添加规则后等待几秒，确保规则生效（可选，但推荐）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 4：插入你的构建/部署步骤&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在这里执行你原本的构建、测试、SSH 部署等操作。&lt;/li&gt;
&lt;li&gt;由于安全组已临时放行，SSH 连接可以成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;步骤 5：删除安全组规则&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;RevokeSecurityGroup&lt;/code&gt; 接口，传入与添加时完全相同的参数，精确删除规则。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if: always()&lt;/code&gt; 确保无论部署步骤成功或失败，都会执行清理，避免规则残留。&lt;/li&gt;
&lt;li&gt;末尾的 &lt;code&gt;|| true&lt;/code&gt; 可防止因规则不存在（例如添加失败）而导致撤销步骤报错，从而使整个工作流标记为失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;7. 注意事项与常见问题&lt;/h3&gt;
&lt;h4&gt;Q1：授权/撤销时返回错误 &lt;code&gt;InvalidIpProtocol.Malformed&lt;/code&gt; 或 &lt;code&gt;InvalidParameter&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;检查 &lt;code&gt;--PortRange&lt;/code&gt; 格式是否为 &lt;code&gt;22/22&lt;/code&gt;，&lt;code&gt;--SourceCidrIp&lt;/code&gt; 是否带 &lt;code&gt;/32&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;确保地域 ID、安全组 ID 正确，且 RAM 用户有相应权限。&lt;/li&gt;
&lt;li&gt;确认 &lt;code&gt;--NicType&lt;/code&gt; 与实例网络类型匹配（VPC 用 &lt;code&gt;intranet&lt;/code&gt;，经典网络用 &lt;code&gt;internet&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q2：阿里云 CLI 报错 &lt;code&gt;&apos;AuthorizeSecurityGroup&apos; is not a valid api&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;此问题常见于全新安装的阿里云 CLI，原因是首次调用时未能成功下载 ECS 产品的元数据定义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方法&lt;/strong&gt;：在命令中添加 &lt;code&gt;--version 2014-05-26 --force&lt;/code&gt; 参数（本文模板已包含），可强制跳过本地校验直接发起请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q3：SSH 部署失败（连接超时或权限被拒）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;可能原因：安全组规则尚未生效。可在授权后增加 &lt;code&gt;sleep 5&lt;/code&gt; 步骤。&lt;/li&gt;
&lt;li&gt;检查 ECS 实例内部防火墙（如 iptables）是否允许 22 端口，安全组只是网络层过滤。&lt;/li&gt;
&lt;li&gt;确认 SSH 服务正在运行，且公钥已正确配置到 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q4：撤销步骤失败，导致规则残留&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;确保撤销步骤的参数与添加步骤完全一致（包括大小写、空格、掩码等）。&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;if: always()&lt;/code&gt; 是否正确添加。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q5：阿里云 CLI 提示权限不足&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;确认 RAM 用户已授予 &lt;code&gt;AliyunECSFullAccess&lt;/code&gt; 权限，并且 AccessKey 正确无误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q6：安全组规则达到上限&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;每个安全组默认最多添加 200 条规则（以阿里云文档为准）。如果频繁执行且删除失败导致规则堆积，可能达到上限。建议定期检查并手动清理残留规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q7：GitHub Actions IP 变更&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;GitHub 的 runners IP 地址范围可能变化，但每个 job 的出口 IP 是固定的。本方案动态获取当前 IP，无需预知 IP 段，因此不受影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;8. 进阶优化&lt;/h3&gt;
&lt;h4&gt;8.1 超时保护&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在使用 SSH Action 时，可设置 &lt;code&gt;timeout&lt;/code&gt; 参数（如 &lt;code&gt;appleboy/ssh-action&lt;/code&gt; 支持 &lt;code&gt;timeout&lt;/code&gt;），防止部署任务长时间卡住导致规则一直开放。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;8.2 并发处理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;多个 workflow 同时运行时，会各自添加和删除自己的 IP 规则，互不影响。但需注意安全组规则数量上限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;8.3 自定义 RAM 策略（最小权限）&lt;/h4&gt;
&lt;p&gt;若需最小权限，可创建自定义策略，仅允许操作特定安全组。示例策略如下（需将 &lt;code&gt;cn-hangzhou&lt;/code&gt; 和 &lt;code&gt;sg-xxxxxx&lt;/code&gt; 替换为实际值）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;1&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ecs:AuthorizeSecurityGroup&quot;,
        &quot;ecs:RevokeSecurityGroup&quot;
      ],
      &quot;Resource&quot;: &quot;acs:ecs:cn-hangzhou:*:securitygroup/sg-xxxxxx&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8.4 使用 OIDC 代替长期 AccessKey（高级）&lt;/h4&gt;
&lt;p&gt;GitHub Actions 支持 OIDC 与阿里云 RAM 角色集成，可避免管理 AccessKey Secret，进一步提升安全性。具体配置请参考阿里云官方文档。&lt;/p&gt;
</content:encoded></item><item><title>使用Github Action 定时签到科研通</title><link>https://www.daitcc.top/posts/4f391d11/</link><guid isPermaLink="true">https://www.daitcc.top/posts/4f391d11/</guid><description>这是一个用于科研通（AbleSci）网站的自动签到脚本，支持青龙面板和GitHub Actions双平台运行。每日自动签到，可配置多个账号，签到结果通过多种渠道推送通知。</description><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;科研通自动签到：GitHub Actions 每日定时签到&lt;/h2&gt;
&lt;p&gt;这是一个用于科研通（AbleSci）网站的自动签到脚本，支持&lt;strong&gt;青龙面板&lt;/strong&gt;和&lt;strong&gt;GitHub Actions&lt;/strong&gt;双平台运行。每日自动签到，可配置多个账号，签到结果通过多种渠道推送通知。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;::github{repo=&quot;daitcl/ablesciSign&quot;}&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;✨ 核心功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;🔑 自动登录科研通网站&lt;/li&gt;
&lt;li&gt;📅 每日自动签到获取积分（默认 7:40、21:40 执行）&lt;/li&gt;
&lt;li&gt;👥 支持多账号批量签到&lt;/li&gt;
&lt;li&gt;📊 显示用户信息（用户名、积分、连续签到天数）&lt;/li&gt;
&lt;li&gt;🔔 支持多平台消息通知（Server酱、息知、PushPlus等）&lt;/li&gt;
&lt;li&gt;⚙️ 支持青龙面板和 GitHub Actions 双平台&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;🚀 快速上手&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;准备账号&lt;/strong&gt;：科研通账号（邮箱:密码），多个账号用换行分隔。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择部署平台&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;青龙面板&lt;/strong&gt;：添加仓库、配置环境变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;：Fork 仓库、添加 Secrets、启用工作流。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置通知（可选）&lt;/strong&gt;：获取推送服务的密钥并添加到环境变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待定时触发&lt;/strong&gt;或&lt;strong&gt;手动运行&lt;/strong&gt;测试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;详细步骤请参考下方部署指南。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;📖 部署指南&lt;/h3&gt;
&lt;h4&gt;🐉 1. 青龙面板部署&lt;/h4&gt;
&lt;h5&gt;1.1 添加订阅&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;登录青龙面板，进入 &lt;strong&gt;订阅管理&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;新建订阅&lt;/strong&gt;，填写以下信息：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;名称&lt;/strong&gt;：&lt;code&gt;科研通签到&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型&lt;/strong&gt;：&lt;code&gt;公开仓库&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链接&lt;/strong&gt;：&lt;code&gt;https://github.com/daitcl/ablesciSign.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定时规则&lt;/strong&gt;：&lt;code&gt;40 7,21 * * *&lt;/code&gt;（对应北京时间 7:40 和 21:40）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白名单&lt;/strong&gt;：&lt;code&gt;ablesci.py|sendNotify.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;确定&lt;/strong&gt; 保存，并运行一次订阅拉取脚本。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1.2 配置环境变量&lt;/h5&gt;
&lt;p&gt;进入 &lt;strong&gt;环境变量&lt;/strong&gt; 页面，点击 &lt;strong&gt;新建变量&lt;/strong&gt;，添加以下变量：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量名&lt;/th&gt;
&lt;th&gt;必填&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ABLESCI_ACCOUNTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;科研通账号列表，格式：&lt;code&gt;邮箱1:密码1&lt;/code&gt;（多个账号用换行或分号分隔）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SCKEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Server酱 SCKEY（用于通知）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;XZKEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;息知 XZKEY（用于通知）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUSH_PLUS_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;PushPlus Token（用于通知）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;账号格式示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user1@example.com:password1
user2@example.com:password2
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.3 安装 Python 依赖&lt;/h5&gt;
&lt;p&gt;在青龙面板的 &lt;strong&gt;依赖管理&lt;/strong&gt; 中添加以下 Python 依赖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requests&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beautifulsoup4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;🤖 2. GitHub Actions 部署&lt;/h4&gt;
&lt;h5&gt;2.1 Fork 仓库&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;访问项目主页：&lt;a href=&quot;https://github.com/daitcl/ablesciSign&quot;&gt;daitcl/ablesciSign&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;点击右上角 &lt;strong&gt;Fork&lt;/strong&gt; 按钮，将仓库复制到您的 GitHub 账户。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;2.2 设置 Secrets&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;在您的仓库页面，点击 &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Secrets and variables&lt;/strong&gt; → &lt;strong&gt;Actions&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;New repository secret&lt;/strong&gt;，依次添加以下 Secrets：&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;必填&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ABLESCI_ACCOUNTS&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;SCKEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Server酱 SCKEY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;XZKEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;息知 XZKEY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUSH_PLUS_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;PushPlus Token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：为避免 GitHub Actions 报错，未使用的通知变量可留空或不添加，但若添加则值必须有效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;2.3 激活工作流&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;点击仓库上方的 &lt;strong&gt;Actions&lt;/strong&gt; 选项卡。&lt;/li&gt;
&lt;li&gt;在左侧选择 &lt;strong&gt;AbleSci Auto Sign&lt;/strong&gt; 工作流。&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;Enable workflow&lt;/strong&gt; 启用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;工作流将按照 &lt;code&gt;.github/workflows/ablesciSign.yml&lt;/code&gt; 中定义的 cron 定时执行（默认北京时间 7:40 和 21:40）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;▶️ 3. 手动触发测试&lt;/h4&gt;
&lt;h5&gt;3.1 青龙面板手动运行&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;strong&gt;定时任务&lt;/strong&gt; 列表中找到 &lt;code&gt;科研通签到&lt;/code&gt; 任务，点击右侧的 &lt;strong&gt;运行&lt;/strong&gt; 按钮。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3.2 GitHub Actions 手动触发&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;strong&gt;Actions&lt;/strong&gt; 页面选择 &lt;strong&gt;AbleSci Auto Sign&lt;/strong&gt; 工作流，点击 &lt;strong&gt;Run workflow&lt;/strong&gt; → &lt;strong&gt;Run workflow&lt;/strong&gt; 手动触发一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;🔔 通知推送设置&lt;/h3&gt;
&lt;h4&gt;📡 支持的推送平台&lt;/h4&gt;
&lt;p&gt;脚本支持以下推送服务，可同时配置多个（至少一个有效即可）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server酱&lt;/strong&gt;（&lt;code&gt;SCKEY&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;息知&lt;/strong&gt;（&lt;code&gt;XZKEY&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PushPlus&lt;/strong&gt;（&lt;code&gt;PUSH_PLUS_TOKEN&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;若同时配置多个，脚本会依次尝试发送，任一成功即视为通知成功。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;🔑 获取推送密钥&lt;/h4&gt;
&lt;h5&gt;Server酱（SCKEY）&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://sct.ftqq.com/&quot;&gt;Server酱官网&lt;/a&gt;，使用 GitHub 登录。&lt;/li&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://sct.ftqq.com/sendkey&quot;&gt;发送消息页面&lt;/a&gt;，复制您的 &lt;code&gt;SendKey&lt;/code&gt;（即 SCKEY）。&lt;/li&gt;
&lt;li&gt;添加到环境变量 &lt;code&gt;SCKEY&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;息知（XZKEY）&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://xz.qqoq.net/&quot;&gt;息知官网&lt;/a&gt;，注册或微信扫码登录。&lt;/li&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://xz.qqoq.net/#/admin/key&quot;&gt;密钥管理页面&lt;/a&gt;，点击 &lt;strong&gt;创建密钥&lt;/strong&gt;，填写名称后生成。&lt;/li&gt;
&lt;li&gt;复制生成的密钥（XZKEY），添加到环境变量 &lt;code&gt;XZKEY&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;PushPlus（PUSH_PLUS_TOKEN）&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://www.pushplus.plus/&quot;&gt;PushPlus官网&lt;/a&gt;，微信扫码登录。&lt;/li&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://www.pushplus.plus/push1.html&quot;&gt;一对一推送页面&lt;/a&gt;，复制页面中的 &lt;strong&gt;Token&lt;/strong&gt; 值。&lt;/li&gt;
&lt;li&gt;添加到环境变量 &lt;code&gt;PUSH_PLUS_TOKEN&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;⏰ 定时规则详解&lt;/h3&gt;
&lt;p&gt;脚本默认执行时间（基于 UTC 时间）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;23:40 UTC&lt;/strong&gt; → 对应 &lt;strong&gt;次日北京时间 7:40&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;13:40 UTC&lt;/strong&gt; → 对应 &lt;strong&gt;北京时间 21:40&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如需修改执行时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;青龙面板&lt;/strong&gt;：修改订阅的定时规则或任务的 cron 表达式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;：编辑仓库中的 &lt;code&gt;.github/workflows/ablesciSign.yml&lt;/code&gt; 文件，修改 &lt;code&gt;on.schedule.cron&lt;/code&gt; 字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;❓ 常见问题解答&lt;/h3&gt;
&lt;h4&gt;Q1: 为什么签到失败？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;检查账号密码是否正确，注意邮箱和密码中不要包含空格。&lt;/li&gt;
&lt;li&gt;确保网络能正常访问科研通网站。&lt;/li&gt;
&lt;li&gt;查看运行日志，确认是否是网站改版导致脚本失效，如果是请提交 Issue。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q2: 如何接收通知？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在对应平台的环境变量/Secrets 中配置至少一种通知服务的密钥。&lt;/li&gt;
&lt;li&gt;脚本执行完成后会自动推送签到结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q3: 可以配置多个账号吗？&lt;/h4&gt;
&lt;p&gt;是的，脚本支持多账号。只需在 &lt;code&gt;ABLESCI_ACCOUNTS&lt;/code&gt; 中用换行分隔多个 &lt;code&gt;邮箱:密码&lt;/code&gt; 即可。脚本会依次处理，并汇总通知。&lt;/p&gt;
&lt;h4&gt;Q4: 如何查看签到日志？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;青龙面板&lt;/strong&gt;：在任务日志中查看。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;：在 Actions 页面点击对应工作流运行记录，查看详细日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q5: 需要安装额外的依赖吗？&lt;/h4&gt;
&lt;p&gt;青龙面板需要手动安装 &lt;code&gt;requests&lt;/code&gt; 和 &lt;code&gt;beautifulsoup4&lt;/code&gt;。GitHub Actions 环境已自动安装，无需额外操作。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;📬 反馈与帮助&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;项目 Issues&lt;/strong&gt;：如有问题或建议，欢迎在 &lt;a href=&quot;https://github.com/daitcl/ablesciSign/issues&quot;&gt;GitHub Issues&lt;/a&gt; 提出。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>终端使用代理加速的正确方式</title><link>https://www.daitcc.top/posts/318cf997/</link><guid isPermaLink="true">https://www.daitcc.top/posts/318cf997/</guid><description>本文介绍如何通过设置`http_proxy`和`https_proxy`环境变量，让终端（macOS/Linux/Windows）走代理，从而加速Homebrew、git等命令的下载。</description><pubDate>Wed, 03 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;终端使用代理加速的正确方式&lt;/h1&gt;
&lt;p&gt;我们在终端使用 &lt;code&gt;Homebrew&lt;/code&gt;、&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;npm&lt;/code&gt;、&lt;code&gt;pip&lt;/code&gt; 等命令时，经常因为网络问题而安装失败。尤其是第一次安装 &lt;code&gt;Homebrew&lt;/code&gt;，不少人需要花很长时间解决，心里不知吐槽了多少遍“该死的网络”。&lt;/p&gt;
&lt;p&gt;虽然设置国内镜像确实有用，但镜像有时更新不及时，且没有普适性。今天就介绍一种通用的方法：让终端也走代理。这样无论你用什么包管理器，都能享受代理带来的加速。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;本文以 Shadowsocks 为例，其他代理软件（如 Clash、V2Ray）配置思路完全一致，只需找到对应的 HTTP 代理端口即可。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、macOS &amp;amp; Linux&lt;/h2&gt;
&lt;h3&gt;1. 基础环境变量&lt;/h3&gt;
&lt;p&gt;终端命令行的网络请求默认不会经过代理，我们需要通过设置环境变量来告诉系统使用代理。&lt;/p&gt;
&lt;p&gt;在终端中直接执行以下命令可以&lt;strong&gt;临时&lt;/strong&gt;开启代理（仅对当前终端窗口有效）：&lt;/p&gt;
&lt;p&gt;bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export http_proxy=http://127.0.0.1:1080
export https_proxy=$http_proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;1080&lt;/code&gt; 是 HTTP 代理的默认端口，但你的实际端口可能不同。请打开 Shadowsocks 的&lt;strong&gt;偏好设置 → 高级&lt;/strong&gt;，查看 &lt;strong&gt;HTTP 代理端口&lt;/strong&gt;，将上述命令中的 &lt;code&gt;1080&lt;/code&gt; 替换为你自己的端口。&lt;/p&gt;
&lt;h3&gt;2. 便捷脚本（永久生效）&lt;/h3&gt;
&lt;p&gt;每次都手动敲命令太麻烦，我们可以写两个函数，方便随时开关代理。&lt;/p&gt;
&lt;p&gt;首先根据你使用的 Shell 决定配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行 &lt;code&gt;echo $SHELL&lt;/code&gt; 查看 Shell 类型
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/bin/bash&lt;/code&gt; → &lt;code&gt;~/.bash_profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/bin/zsh&lt;/code&gt; → &lt;code&gt;~/.zprofile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后使用以下命令将脚本追加到对应的配置文件，并立即生效（以下以 &lt;code&gt;.zprofile&lt;/code&gt; 为例，请按实际情况替换）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt;&amp;gt; ~/.zprofile &amp;lt;&amp;lt; &apos;EOF&apos;
function proxy_on() {
    export http_proxy=http://127.0.0.1:1080   # 端口按实际情况修改
    export https_proxy=$http_proxy
    echo -e &quot;终端代理已开启。&quot;
}

function proxy_off() {
    unset http_proxy https_proxy
    echo -e &quot;终端代理已关闭。&quot;
}
EOF

source ~/.zprofile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，你只需要在终端输入 &lt;code&gt;proxy_on&lt;/code&gt; 即可开启代理，输入 &lt;code&gt;proxy_off&lt;/code&gt; 关闭代理。&lt;/p&gt;
&lt;h3&gt;3. 验证代理是否生效&lt;/h3&gt;
&lt;p&gt;执行以下命令，如果返回的 IP 是你代理服务器的 IP，说明代理生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl cip.cc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -I https://www.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果看到 &lt;code&gt;HTTP/2 200&lt;/code&gt; 之类的响应，说明可以正常访问 Google。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：有时 Google 会返回 &lt;code&gt;403&lt;/code&gt;，这是因为 Google 对某些代理 IP 做了限制，可以换用 &lt;code&gt;cip.cc&lt;/code&gt; 验证 IP 归属地。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Windows&lt;/h2&gt;
&lt;p&gt;Windows 下的配置略有不同，主要有两种方法：通过 CMD/PowerShell 设置环境变量，或者使用图形化工具。&lt;/p&gt;
&lt;h3&gt;1. 临时设置（CMD）&lt;/h3&gt;
&lt;p&gt;在命令提示符中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set http_proxy=http://127.0.0.1:1080
set https_proxy=http://127.0.0.1:1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方法只对当前 CMD 窗口有效，关闭后失效。&lt;/p&gt;
&lt;h3&gt;2. 永久设置（系统环境变量）&lt;/h3&gt;
&lt;p&gt;通过 &lt;strong&gt;系统属性 → 高级 → 环境变量&lt;/strong&gt;，为当前用户或系统添加两个变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量名：&lt;code&gt;http_proxy&lt;/code&gt;，值：&lt;code&gt;http://127.0.0.1:1080&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;变量名：&lt;code&gt;https_proxy&lt;/code&gt;，值：&lt;code&gt;http://127.0.0.1:1080&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置后重新打开终端即可生效。&lt;/p&gt;
&lt;h3&gt;3. PowerShell 便捷函数&lt;/h3&gt;
&lt;p&gt;在 PowerShell 中，可以将函数添加到 &lt;code&gt;$PROFILE&lt;/code&gt; 中，实现类似 Linux 的开关功能。&lt;/p&gt;
&lt;p&gt;首先检查 &lt;code&gt;$PROFILE&lt;/code&gt; 文件是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test-Path $PROFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果返回 &lt;code&gt;False&lt;/code&gt;，则创建它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;New-Item -Path $PROFILE -ItemType File -Force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后用记事本打开编辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;notepad $PROFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function proxy_on {
    $env:http_proxy = &quot;http://127.0.0.1:1080&quot;
    $env:https_proxy = &quot;http://127.0.0.1:1080&quot;
    Write-Host &quot;终端代理已开启。&quot;
}

function proxy_off {
    Remove-Item Env:http_proxy -ErrorAction SilentlyContinue
    Remove-Item Env:https_proxy -ErrorAction SilentlyContinue
    Write-Host &quot;终端代理已关闭。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后，重启 PowerShell 或执行 &lt;code&gt;. $PROFILE&lt;/code&gt; 使函数生效。之后就可以使用 &lt;code&gt;proxy_on&lt;/code&gt; 和 &lt;code&gt;proxy_off&lt;/code&gt; 了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、其他代理方式&lt;/h2&gt;
&lt;p&gt;除了设置环境变量，还有一些更灵活的方式可以实现终端代理。&lt;/p&gt;
&lt;h3&gt;1. 使用 Socks5 代理&lt;/h3&gt;
&lt;p&gt;某些代理软件只提供 Socks5 端口，没有开启 HTTP 代理。此时可以用工具将 Socks5 转换为 HTTP，或者直接设置 &lt;code&gt;ALL_PROXY&lt;/code&gt; 变量（某些软件支持 Socks5）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export all_proxy=socks5://127.0.0.1:1080   # 端口为 Socks5 端口
export http_proxy=socks5://127.0.0.1:1080
export https_proxy=socks5://127.0.0.1:1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：并非所有命令行工具都支持 Socks5，例如 &lt;code&gt;curl&lt;/code&gt; 和 &lt;code&gt;wget&lt;/code&gt; 支持，但 &lt;code&gt;git&lt;/code&gt; 需要额外配置。&lt;/p&gt;
&lt;h3&gt;2. 为 Git 单独设置代理&lt;/h3&gt;
&lt;p&gt;如果你只想让 Git 走代理，而其他命令不走，可以单独配置 Git：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# HTTP 代理
git config --global http.proxy http://127.0.0.1:1080
git config --global https.proxy http://127.0.0.1:1080

# Socks5 代理
git config --global http.proxy socks5://127.0.0.1:1080
git config --global https.proxy socks5://127.0.0.1:1080

# 取消代理
git config --global --unset http.proxy
git config --global --unset https.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 使用 proxychains（Linux/macOS）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;proxychains&lt;/code&gt; 是一个强大的工具，可以强制任意命令通过代理，无需修改环境变量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;安装：&lt;code&gt;brew install proxychains-ng&lt;/code&gt; (macOS) 或 &lt;code&gt;sudo apt install proxychains&lt;/code&gt; (Debian/Ubuntu)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置：编辑 &lt;code&gt;/etc/proxychains.conf&lt;/code&gt;，在末尾添加代理信息，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;socks4 127.0.0.1 1080
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用：在需要代理的命令前加上 &lt;code&gt;proxychains4&lt;/code&gt;（或 &lt;code&gt;proxychains&lt;/code&gt;），例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 brew install wget
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 仅对当前命令临时启用代理&lt;/h3&gt;
&lt;p&gt;如果你不想全局开启代理，只想让某一条命令走代理，可以直接在命令前加上环境变量赋值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http_proxy=http://127.0.0.1:1080 https_proxy=http://127.0.0.1:1080 curl cip.cc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式只对紧跟的命令生效，不影响后续命令。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、常见问题&lt;/h2&gt;
&lt;h3&gt;1. 如何获取正确的代理端口？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shadowsocks&lt;/strong&gt;：在客户端设置中查看「HTTP 代理端口」，通常默认为 &lt;code&gt;1080&lt;/code&gt; 或 &lt;code&gt;1087&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clash&lt;/strong&gt;：在配置文件中查看 &lt;code&gt;port&lt;/code&gt;（HTTP 代理端口）和 &lt;code&gt;socks-port&lt;/code&gt;，或者打开 Clash 面板查看。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V2Ray&lt;/strong&gt;：取决于客户端配置，一般也会提供 HTTP 或 Socks 端口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 为什么设置代理后某些命令仍然很慢？&lt;/h3&gt;
&lt;p&gt;可能是 DNS 解析没有走代理。可以尝试设置 &lt;code&gt;ALL_PROXY&lt;/code&gt;，或者使用支持远程 DNS 解析的工具（如 Clash 的 redir-host 模式）。对于 curl 等工具，也可以使用 &lt;code&gt;--socks5&lt;/code&gt; 参数指定 Socks5 代理。&lt;/p&gt;
&lt;h3&gt;3. 使用 Google 验证时返回 403&lt;/h3&gt;
&lt;p&gt;Google 可能会检测代理 IP 并返回 403，这并不代表代理失效。建议使用 &lt;code&gt;cip.cc&lt;/code&gt; 或 &lt;code&gt;ip.sb&lt;/code&gt; 等 IP 查询网站验证。&lt;/p&gt;
</content:encoded></item><item><title>Matlab 离线安装硬件支持包与附加功能的方法</title><link>https://www.daitcc.top/posts/75b07b4e/</link><guid isPermaLink="true">https://www.daitcc.top/posts/75b07b4e/</guid><description>当 Matlab 的附加功能管理器无法正常在线下载或安装硬件支持包时，可以采用以下离线安装方式。本方法通过官方下载工具提前获取安装文件，再从 Matlab 内部进行离线安装。</description><pubDate>Sat, 13 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Matlab 离线安装硬件支持包与附加功能的方法&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;当在线安装无法使用时,通过离线支持包安装附加功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;操作步骤&lt;/h2&gt;
&lt;h3&gt;1. 下载离线包获取工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;访问 &lt;code&gt;MathWorks&lt;/code&gt; 官方支持软件下载页面：
&lt;a href=&quot;https://ww2.mathworks.cn/support/install/support-software-downloader.html?s_tid=srchtitle&quot;&gt;下载地址&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;根据您的操作系统选择对应的版本，下载并运行 &lt;code&gt;SupportSoftwareDownloader.exe&lt;/code&gt;（即下载工具）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 运行下载工具并登录&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;双击 &lt;code&gt;SupportSoftwareDownloader.exe&lt;/code&gt;，如果启动失败（如无响应或报错），可尝试以管理员身份运行，或多次重试。&lt;/li&gt;
&lt;li&gt;成功启动后，界面会要求您登录 &lt;code&gt;MathWorks&lt;/code&gt; 账号。输入已注册的邮箱和密码；若无账号，可点击界面上的注册链接完成注册。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 选择 Matlab 版本&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;登录后，工具会自动检测或要求您选择当前计算机上安装的 &lt;strong&gt;Matlab&lt;/strong&gt; 版本（例如 &lt;strong&gt;R2023a&lt;/strong&gt;、&lt;strong&gt;R2022b&lt;/strong&gt; 等）。请确保选择与您实际版本一致的选项。&lt;/li&gt;
&lt;li&gt;如果版本列表未出现，请返回上一步重新选择，直至正确进入下一步。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 选择需要安装的附加功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;工具会列出所有可用的硬件支持包和附加功能。勾选您需要的项（例如特定型号的硬件支持包、工具箱等）。&lt;/li&gt;
&lt;li&gt;点击&lt;code&gt;下一步&lt;/code&gt;继续。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 指定下载文件夹&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;选择或创建一个本地文件夹，用于存放下载的安装文件。建议选择一个易于访问的位置，例如 &lt;code&gt;D:\Matlab_SupportPackages&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点击&lt;code&gt;开始下载&lt;/code&gt;，等待下载完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 找到 Matlab 离线安装工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;打开 &lt;strong&gt;Matlab&lt;/strong&gt; 的安装目录，找到 &lt;code&gt;install_supportsoftware.exe&lt;/code&gt; 文件。
常见路径为：
&lt;code&gt;Matlab安装根目录\bin\win64\install_supportsoftware.exe&lt;/code&gt;
例如：&lt;code&gt;D:\Matlab\R2023a\bin\win64\install_supportsoftware.exe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果找不到该文件，可在 &lt;strong&gt;Matlab&lt;/strong&gt; 安装目录下搜索文件名 &lt;code&gt;install_supportsoftware&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 复制下载的 &lt;code&gt;archives&lt;/code&gt; 文件夹&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;下载完成后，进入&lt;strong&gt;第 5 步&lt;/strong&gt;指定的文件夹，您会看到一个名为 &lt;strong&gt;&lt;code&gt;archives&lt;/code&gt;&lt;/strong&gt; 的子文件夹。
这个文件夹包含了所有下载的安装文件。&lt;/li&gt;
&lt;li&gt;将整个 &lt;code&gt;archives&lt;/code&gt; 文件夹&lt;strong&gt;复制&lt;/strong&gt;到&lt;strong&gt;第 6 步&lt;/strong&gt;中 &lt;code&gt;install_supportsoftware.exe&lt;/code&gt; 所在的目录（即 &lt;code&gt;bin\win64&lt;/code&gt; 下）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. 运行离线安装&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;bin\win64&lt;/code&gt; 目录下，双击 &lt;code&gt;install_supportsoftware.exe&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;程序会自动识别 &lt;code&gt;archives&lt;/code&gt; 文件夹中的内容，并弹出安装界面。勾选您需要安装的项，按提示完成安装。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9. 验证安装&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;启动 &lt;strong&gt;Matlab&lt;/strong&gt;，在&lt;code&gt;主页&lt;/code&gt;选项卡中点击&lt;code&gt;附加功能&lt;/code&gt; → &lt;code&gt;管理附加功能&lt;/code&gt;，查看已安装的硬件支持包列表，确认安装成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10. 安装其他附加功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如需添加其他硬件支持包或功能，只需重新运行下载工具，将新的文件下载到同一文件夹（覆盖或合并原有的 &lt;code&gt;archives&lt;/code&gt; 文件夹），然后再次执行 &lt;code&gt;install_supportsoftware.exe&lt;/code&gt; 即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;确保 &lt;strong&gt;Matlab&lt;/strong&gt; 版本与下载工具中选定的版本一致，否则可能导致安装失败。&lt;/li&gt;
&lt;li&gt;离线安装过程中，请勿关闭 &lt;strong&gt;Matlab&lt;/strong&gt; 或中断 &lt;code&gt;install_supportsoftware.exe&lt;/code&gt; 的运行。&lt;/li&gt;
&lt;li&gt;若下载工具始终无法正常启动，请检查网络设置、防火墙，或尝试从 &lt;strong&gt;MathWorks&lt;/strong&gt; 官网重新下载该工具。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>配置pcigo-command上传图床为兰空图床</title><link>https://www.daitcc.top/posts/705da22a/</link><guid isPermaLink="true">https://www.daitcc.top/posts/705da22a/</guid><description>本文详细说明了如何使用 picgo-command 配置兰空图床（lskypro），包括获取 token 和相册 ID、安装插件、交互式设置参数等步骤，最终实现通过命令行上传图片到指定相册。</description><pubDate>Sat, 13 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;pcigo-command设置lskypro&lt;/h1&gt;
&lt;h2&gt;1. 准备工作&lt;/h2&gt;
&lt;h3&gt;1.1 Curl的安装与配置&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;安装配置&lt;a href=&quot;https://curl.se/download.html&quot;&gt;Curl 下载地址&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;解压&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下载完成后，我解压到了这样一个目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\bin\curl-8.4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置环境变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新建一个环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;变量名(N): CURL_HOME
变量值(V): D:\bin\curl-8.4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;strong&gt;Path&lt;/strong&gt;中追加这个环境变量即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%CURL_HOME%\bin\
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 获取lsky的信息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 cURL(&lt;strong&gt;推荐&lt;/strong&gt;)获取认证 token 信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST -F &quot;email=admin@mail.com&quot; -F &quot;password=admin&quot; https://test.lsky.com/api/v1/tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：&lt;/p&gt;
&lt;p&gt;此命令仅适用于 V2，V1 用户只需要进入个人设置页面复制、粘贴使用即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;打开终端输入命令&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;D:\&amp;gt;curl -X POST -F &quot;email=admin@mail.com&quot; -F &quot;password=admin&quot; https://test.lsky.com/api/v1/tokens
{&quot;status&quot;:true,&quot;message&quot;:&quot;success&quot;,&quot;data&quot;:{&quot;token&quot;:&quot;1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;保存获取的&lt;code&gt;认证token信息&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：&lt;/p&gt;
&lt;p&gt;通过设置请求 header 标头来验证请求(Bearer Token),所以&lt;code&gt;Bearer&lt;/code&gt;不可少&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;使用cURL(&lt;strong&gt;推荐&lt;/strong&gt;)获取默认上传的相册ID&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl -H &quot;Authorization:Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv&quot; -H &quot;Accept:application/json&quot; https://test.lsky.com/api/v1/albums
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：&lt;/p&gt;
&lt;p&gt;Authorization后面填刚保存的&lt;code&gt;认证token信息&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;打开终端输入命令&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;D:\&amp;gt;curl -H &quot;Authorization:Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv&quot; -H &quot;Accept:application/json&quot; https://test.lsky.com/api/v1/albums
{&quot;status&quot;:true,&quot;message&quot;:&quot;success&quot;,&quot;data&quot;:{&quot;current_page&quot;:1,&quot;data&quot;:[{&quot;id&quot;:3,&quot;name&quot;:&quot;\u65c5\u6e38&quot;,&quot;intro&quot;:&quot;&quot;,&quot;image_num&quot;:0},{&quot;id&quot;:2,&quot;name&quot;:&quot;\u65e5\u5e38&quot;,&quot;intro&quot;:&quot;&quot;,&quot;image_num&quot;:0},{&quot;id&quot;:1,&quot;name&quot;:&quot;\u56fe\u5e8a&quot;,&quot;intro&quot;:&quot;&quot;,&quot;image_num&quot;:0}],&quot;first_page_url&quot;:&quot;http:\/\/test.lsky.com\/api\/v1\/albums?page=1&quot;,&quot;from&quot;:1,&quot;last_page&quot;:1,&quot;last_page_url&quot;:&quot;http:\/\/test.lsky.com\/api\/v1\/albums?page=1&quot;,&quot;links&quot;:[{&quot;url&quot;:null,&quot;label&quot;:&quot;&amp;amp;laquo; \u4e0a\u4e00\u9875&quot;,&quot;active&quot;:false},{&quot;url&quot;:&quot;http:\/\/test.lsky.com\/api\/v1\/albums?page=1&quot;,&quot;label&quot;:&quot;1&quot;,&quot;active&quot;:true},{&quot;url&quot;:null,&quot;label&quot;:&quot;\u4e0b\u4e00\u9875 &amp;amp;raquo;&quot;,&quot;active&quot;:false}],&quot;next_page_url&quot;:null,&quot;path&quot;:&quot;http:\/\/test.lsky.com\/api\/v1\/albums&quot;,&quot;per_page&quot;:40,&quot;prev_page_url&quot;:null,&quot;to&quot;:3,&quot;total&quot;:3}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将返回的参数在&lt;a href=&quot;https://www.json.cn/&quot;&gt;网页&lt;/a&gt;整理之后可得&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;status&quot;: true,
    &quot;message&quot;: &quot;success&quot;,
    &quot;data&quot;: {
        &quot;current_page&quot;: 1,
        &quot;data&quot;: [
            {
                &quot;id&quot;: 3,
                &quot;name&quot;: &quot;旅游&quot;,
                &quot;intro&quot;: &quot;&quot;,
                &quot;image_num&quot;: 0
            },
            {
                &quot;id&quot;: 2,
                &quot;name&quot;: &quot;日常&quot;,
                &quot;intro&quot;: &quot;&quot;,
                &quot;image_num&quot;: 0
            },
            {
                &quot;id&quot;: 1,
                &quot;name&quot;: &quot;图床&quot;,
                &quot;intro&quot;: &quot;&quot;,
                &quot;image_num&quot;: 0
            }
        ],
        &quot;first_page_url&quot;: &quot;http://test.lsky.com/api/v1/albums?page=1&quot;,
        &quot;from&quot;: 1,
        &quot;last_page&quot;: 1,
        &quot;last_page_url&quot;: &quot;http://test.lsky.com/api/v1/albums?page=1&quot;,
        &quot;links&quot;: [
            {
                &quot;url&quot;: null,
                &quot;label&quot;: &quot;&amp;amp;laquo; 上一页&quot;,
                &quot;active&quot;: false
            },
            {
                &quot;url&quot;: &quot;http://test.lsky.com/api/v1/albums?page=1&quot;,
                &quot;label&quot;: &quot;1&quot;,
                &quot;active&quot;: true
            },
            {
                &quot;url&quot;: null,
                &quot;label&quot;: &quot;下一页 &amp;amp;raquo;&quot;,
                &quot;active&quot;: false
            }
        ],
        &quot;next_page_url&quot;: null,
        &quot;path&quot;: &quot;http://test.lsky.com/api/v1/albums&quot;,
        &quot;per_page&quot;: 40,
        &quot;prev_page_url&quot;: null,
        &quot;to&quot;: 3,
        &quot;total&quot;: 3
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：&lt;/p&gt;
&lt;p&gt;返回参数 data.data.name 相册名称 需要经过Unicode转中文&lt;/p&gt;
&lt;p&gt;返回参数 data.data.id   相册自增 ID&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;保存获取的&lt;code&gt;相册ID&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;图床 1
日常 2
旅游 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 安装lsky图床插件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;lsky图床插件&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/picgo-plugin-lankong?activeTab=readme&quot;&gt;官方地址&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装方式
在对应系统的&lt;code&gt;PicGo&lt;/code&gt;程序配置文件路径下执行 &lt;code&gt;npm i picgo-plugin-lankong&lt;/code&gt;，然后重启应用即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 修改配置文件&lt;/h2&gt;
&lt;p&gt;这一步需要找到第1.2步中下载的&lt;code&gt;picgo&lt;/code&gt;二进制文件，不同系统文件名略有不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;windows&lt;/code&gt;系统一般在&lt;code&gt;C:\Users\用户名\AppData\Roaming\picgo\win64\&lt;/code&gt;文件夹，文件名为&lt;code&gt;picgo.exe&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;linux&lt;/code&gt;系统一般在&lt;code&gt;~/.config/Typora/picgo/linux/&lt;/code&gt;文件夹，文件名为&lt;code&gt;picgo&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;接下来执行命令：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;linux&lt;/code&gt;系统，打开终端，在&lt;code&gt;home&lt;/code&gt;目录下执行&lt;code&gt;./.config/Typora/picgo/linux/picgo install lankong&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;windows&lt;/code&gt;系统，打开终端，可以在包含&lt;code&gt;picgo.exe&lt;/code&gt;文件的路径下执行&lt;code&gt;./picgo.exe install lankong&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;下面以&lt;code&gt;windows&lt;/code&gt;系统为例&lt;/strong&gt;
在终端中打开你的&lt;code&gt;picgo.exe&lt;/code&gt;路径，你可以通过&lt;code&gt;picgo.exe -h&lt;/code&gt;来查看所有命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ picgo.exe -h

Usage: picgo [options] [command]

Options:

  -v, --version                 output the version number
  -d, --debug                   debug mode
  -s, --silent                  silent mode
  -c, --config &amp;lt;path&amp;gt;           set config path
  -h, --help                    output usage information

Commands:

  install|add [options] &amp;lt;plugins...&amp;gt;   install picgo plugin
  uninstall|rm &amp;lt;plugins...&amp;gt;            uninstall picgo plugin
  update [options] &amp;lt;plugins...&amp;gt;        update picgo plugin
  set|config &amp;lt;module&amp;gt; [name]           configure config of picgo modules
  upload|u [input...]                  upload, go go go
  use [module]                         use modules of picgo
  init [options] &amp;lt;template&amp;gt; [project]  create picgo plugin&apos;s development templates
  i18n [lang]                          change picgo language
  help [command]                       display help for command
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其中，命令选项如果是用&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;包围起来的为必须输入项，如果是用&lt;code&gt;[]&lt;/code&gt;包围起来的则为可选输入项。 有些命令支持简写，比如&lt;code&gt;picgo upload&lt;/code&gt;可以写为&lt;code&gt;picgo u&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用&lt;code&gt;use&lt;/code&gt;命令为&lt;code&gt;picgo&lt;/code&gt;选择&lt;code&gt;lsky&lt;/code&gt;图床&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe use
? Use an uploader (Use arrow keys)
&amp;gt; lankong
  smms
  tcyun
  github
  qiniu
  imgur
  aliyun
(Move up and down to reveal more choices)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe use
? Use an uploader lankong
? Use a transformer (Use arrow keys)
&amp;gt; path
  base64
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe use
? Use an uploader lankong
? Use a transformer path
? Use plugins (Press &amp;lt;space&amp;gt; to select, &amp;lt;a&amp;gt; to toggle all, &amp;lt;i&amp;gt; to invert selection)
&amp;gt;(*) picgo-plugin-lankong
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe use
? Use an uploader lankong
? Use a transformer path
? Use plugins (Press &amp;lt;space&amp;gt; to select, &amp;lt;a&amp;gt; to toggle all, &amp;lt;i&amp;gt; to invert selection)picgo-plugin-lankong
[PicGo SUCCESS]: Configure config successfully!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;picgo 需要配置文件来启动。当你未指定配置文件的时候，picgo 将会使用默认配置文件来启动。&lt;/p&gt;
&lt;h3&gt;自动生成&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;通常来说你只需要配置 &lt;code&gt;Uploader&lt;/code&gt; 即可，所以你可以通过 &lt;code&gt;picgo set uploader&lt;/code&gt; 来进入交互式命令行，&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe set uploader
? Choose a(n) uploader (Use arrow keys)
&amp;gt; lankong
  smms
  tcyun
  github
  qiniu
  imgur
  aliyun
(Move up and down to reveal more choices)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe set uploader
? Choose a(n) uploader lankong
? Choose a version (Use arrow keys)
  V1
&amp;gt; V2
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe set uploader
? Choose a(n) uploader lankong
? Choose a version V2
? 示例: https://example.com https://test.lsky.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;认证 token 信息&lt;/code&gt;为第&lt;code&gt;1.3节&lt;/code&gt;保存的token值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe set uploader
? Choose a(n) uploader lankong
? Choose a version V2
? 示例: https://example.com test.lsky.com
? 认证 token 信息 Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;albumId&lt;/code&gt;为第&lt;code&gt;1.3节&lt;/code&gt;保存的&lt;code&gt;相册ID&lt;/code&gt; (选择需要默认上传的 这里我选择的是 &lt;strong&gt;图床&lt;/strong&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe set uploader
? Choose a(n) uploader lankong
? Choose a version V2
? 示例: https://example.com test.lsky.com
? 认证 token 信息 Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv
? 选填, V1以及V2使用默认存储策略时请留空 1
? 选填, V2生效 1  
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\AppData\Roaming\picgo\win64&amp;gt;./picgo.exe set uploader
? Choose a(n) uploader lankong
? Choose a version V2
? 示例: https://example.com test.lsky.com
? 认证 token 信息 Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv
? 选填, V1以及V2使用默认存储策略时请留空 1
? 选填, V2生效 1
? set permission private(default)
? 是否忽略证书错误, 如果上传失败提示证书过期请设为true No
? 是否同步删除, 只支持V2 No
[PicGo SUCCESS]: Configure config successfully
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;默认配置文件&lt;/h3&gt;
&lt;p&gt;picgo 的默认配置文件为&lt;code&gt;~/.picgo/config.json&lt;/code&gt;。其中&lt;code&gt;~&lt;/code&gt;为用户目录。不同系统的用户目录不太一样。&lt;/p&gt;
&lt;p&gt;linux 和 macOS 均为&lt;code&gt;~/.picgo/config.json&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;windows 则为&lt;code&gt;C:\Users\你的用户名\.picgo\config.json&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以用&lt;code&gt;记事本&lt;/code&gt;打开&lt;/p&gt;
&lt;p&gt;可以参考如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;picBed&quot;: {
    &quot;uploader&quot;: &quot;lankong&quot;,
    &quot;current&quot;: &quot;lankong&quot;,
    &quot;lankong&quot;: {
      &quot;lskyProVersion&quot;: &quot;V2&quot;,
      &quot;server&quot;: &quot;https://test.lsky.com&quot;,
      &quot;token&quot;: &quot;Bearer 1|CMhnYzsbHiGuX2YDNrMRYJdFRh466wDYzeuz53qv&quot;,
      &quot;strategyId&quot;: &quot;1&quot;,
      &quot;albumId&quot;: &quot;1&quot;,
      &quot;permission&quot;: 0,
      &quot;ignoreCertErr&quot;: false,
      &quot;syncDelete&quot;: false
    },
    &quot;transformer&quot;: &quot;path&quot;
  },
  &quot;picgoPlugins&quot;: {
    &quot;picgo-plugin-lankong&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>基于Fuwari的文章模板简易指南</title><link>https://www.daitcc.top/posts/9446c589/</link><guid isPermaLink="true">https://www.daitcc.top/posts/9446c589/</guid><description>如何使用此模板</description><pubDate>Mon, 01 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;封面图片来源：&lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此博客模板基于 &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; 构建。本指南未提及的内容，可在 &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro 文档&lt;/a&gt; 中找到答案。&lt;/p&gt;
&lt;h2&gt;文章前置参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: 我的第一篇博客文章
published: 2023-09-09
description: &apos;这是我的新 Astro 博客的第一篇文章。
image: &apos;./cover.jpg&apos;
tags: [&apos;Foo&apos;, &apos;Bar&apos;]
category: &apos;前端&apos;
draft: false
lang: &apos;zh-CN&apos;
pinned: false
---
&lt;/code&gt;&lt;/pre&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;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章标题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章发布日期。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的简短描述。显示在首页。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的封面图片路径。&amp;lt;br/&amp;gt;1. 以 &lt;code&gt;http://&lt;/code&gt; 或 &lt;code&gt;https://&lt;/code&gt; 开头：使用网络图片&amp;lt;br/&amp;gt;2. 以 &lt;code&gt;/&lt;/code&gt; 开头：使用 &lt;code&gt;public&lt;/code&gt; 目录中的图片&amp;lt;br/&amp;gt;3. 无上述前缀：相对于 Markdown 文件的路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的标签。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的分类。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果此文仍为草稿，将不会显示。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的语言。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pinned&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章是否置顶。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;文章文件的存放位置&lt;/h2&gt;
&lt;p&gt;你的文章文件应存放在 &lt;code&gt;src/content/posts/&lt;/code&gt; 目录中。你也可以创建子目录，以便更好地组织文章和资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::github{repo=&quot;saicaca/fuwari&quot;}&lt;/p&gt;
</content:encoded></item><item><title>兰空图床配置上传相册</title><link>https://www.daitcc.top/posts/dcd376bb/</link><guid isPermaLink="true">https://www.daitcc.top/posts/dcd376bb/</guid><description>本文介绍了如何修改兰空图床源码，通过在/app/Services/ImageService.php中增加对album_id参数的优先判断，实现了API上传时指定目标相册的功能，若未传递则使用默认相册。</description><pubDate>Mon, 04 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;兰空图床设置上传相册&lt;/h3&gt;
&lt;h4&gt;需求&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在使用 API 上传图片时，希望能够通过参数指定目标相册。同时，Web 上传界面也应支持此功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;解决方法：&lt;/h4&gt;
&lt;p&gt;修改文件 &lt;code&gt;/app/Services/ImageService.php&lt;/code&gt; 第 139 行附近，调整相册分配逻辑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原代码&lt;/strong&gt;（仅使用用户默认相册）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 图片保存至默认相册(若有)
if ($albumId = $user-&amp;gt;configs-&amp;gt;get(UserConfigKey::DefaultAlbum)) {
    if ($user-&amp;gt;albums()-&amp;gt;where(&apos;id&apos;, $albumId)-&amp;gt;exists()) {
        $image-&amp;gt;album_id = $albumId;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改后代码&lt;/strong&gt;（优先使用请求参数中的 &lt;code&gt;album_id&lt;/code&gt;，否则回退到默认相册）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 如果请求中包含 album_id，则优先使用
if ($request-&amp;gt;has(&apos;album_id&apos;)) {
    $image-&amp;gt;album_id = $request-&amp;gt;input(&apos;album_id&apos;);
} else {
    // 图片保存至默认相册(若有)
    if ($albumId = $user-&amp;gt;configs-&amp;gt;get(UserConfigKey::DefaultAlbum)) {
        if ($user-&amp;gt;albums()-&amp;gt;where(&apos;id&apos;, $albumId)-&amp;gt;exists()) {
            $image-&amp;gt;album_id = $albumId;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用方式&lt;/h4&gt;
&lt;p&gt;修改后，在 API 请求的 &lt;code&gt;formdata&lt;/code&gt; 中添加 &lt;code&gt;album_id&lt;/code&gt; 参数即可指定相册。示例（Flutter/Dart）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;formdata = FormData.fromMap({
    &quot;file&quot;: await MultipartFile.fromFile(path, filename: name),
    &quot;strategy_id&quot;: 1,
    &quot;album_id&quot;: 4,      // 指定相册 ID
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;注意事项&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;确保传递的 &lt;code&gt;album_id&lt;/code&gt; 存在且属于当前用户，否则可能报错。&lt;/li&gt;
&lt;li&gt;若请求中未提供 &lt;code&gt;album_id&lt;/code&gt;，则仍按用户默认相册设置处理。&lt;/li&gt;
&lt;li&gt;Web 上传界面的支持需前端配合传递该参数，此修改仅服务端生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过此修改，即可灵活控制图片上传到指定相册。&lt;/p&gt;
</content:encoded></item></channel></rss>