<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on 豆子技术站</title><link>https://blog.91demo.top/posts/</link><description>Recent content in Posts on 豆子技术站</description><generator>Hugo -- 0.155.1</generator><language>zh-cn</language><lastBuildDate>Sun, 24 May 2026 06:40:16 +0800</lastBuildDate><atom:link href="https://blog.91demo.top/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>从 wasm-bindgen 到 C-ABI 写微信小程序WASM：一次真实的 WASM 落地复盘</title><link>https://blog.91demo.top/wx-wasm/</link><pubDate>Sun, 24 May 2026 06:40:16 +0800</pubDate><guid>https://blog.91demo.top/wx-wasm/</guid><description>&lt;blockquote&gt;
&lt;p&gt;在写完 ESP8266 IoT 架构之后，我就在想一件事情：既然代码开源，IoT 网关岂不是谁都能连？&lt;br&gt;
于是我试图在微信小程序端做文章。但开源的小程序不能硬编码密钥，也不能仅靠接口鉴权。&lt;br&gt;
我想到了很早之前预研的 WebAssembly（WASM）。配合后端防护，可以大幅提升攻击者的成本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一背景与目标"&gt;一、背景与目标&lt;/h2&gt;
&lt;p&gt;实现的功能非常简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;传入 &lt;strong&gt;小程序 APPID + 设备 ID + 时间戳&lt;/strong&gt;，使用 &lt;strong&gt;Ed25519&lt;/strong&gt; 签名，返回 Token。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以前在微信小程序调通过 &lt;code&gt;WxWebAssembly&lt;/code&gt;（简称 wxwasm），但当时主要处理 Int 类型，这次需要返回字符串。&lt;/p&gt;
&lt;p&gt;当我拿出以前的预研代码进行修改时，我才意识到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“能用 Rust 写 WASM”&lt;/strong&gt; 和 &lt;strong&gt;“能在微信小程序里稳定运行”&lt;/strong&gt;，完全是两回事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="二第一阶段理想很丰满--rust--wasm-bindgen"&gt;二、第一阶段：理想很丰满 —— Rust + wasm-bindgen&lt;/h2&gt;
&lt;p&gt;最初的环境是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust &lt;strong&gt;1.95&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wasm-bindgen&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wasm-pack&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cargotoml"&gt;Cargo.toml&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;dependencies&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;wasm-bindgen&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0.2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ed25519-dalek&lt;/span&gt; = { &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;2.1&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;features&lt;/span&gt; = [&lt;span style="color:#e6db74"&gt;&amp;#34;rand_core&amp;#34;&lt;/span&gt;] }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;hex&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0.4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="librs"&gt;lib.rs&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[wasm_bindgen]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;generate_token&lt;/span&gt;(appid: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;, device_id: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;, timestamp: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String, JsValue&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Ed25519 签名逻辑
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;编译很顺利（就是有点慢 😭），本地 &lt;code&gt;test.html&lt;/code&gt; 测试一切正常。&lt;/p&gt;</description></item><item><title>谋划已久的蓄力：我的官网第三次重构（Nuxt3 + Naive UI + Nginx</title><link>https://blog.91demo.top/nuxt3-mysite/</link><pubDate>Sun, 17 May 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/nuxt3-mysite/</guid><description>&lt;p&gt;最近，我使用 Nuxt3 全面重构了我的官网。大功告成后经实测发现，首屏加载速度其实变慢了。但奇妙的是，这次重构我却一点都不后悔。&lt;/p&gt;
&lt;p&gt;上一个版本的官网，我是用最纯粹的 HTML+CSS+JS 开发的。说实话，刚写完的时候自己非常满意，纯原生代码，没有任何框架负担。但最近在规划我拥有的数字资产后续的功能时，我的想法发生了一些根本性的转变。我开始意识到，之前把很多工具一股脑塞进微信小程序里，其实并不合理。对于开发者或者需要高频使用不需要账号体系的工具的用户来说，在电脑大屏上操作，远比在手机上点来点去效率高得多，这也是我在小程序上实现没有多少人访问的原因吧。更别说每次改动功能就被小程序那让人头疼的、动不动就被卡住的审核机制了。于是我冒出一个强烈的念头：我要把小程序里适合在电脑上用的工具，全部搬到 Web 网页上来，面向全球用户开放；同时去精简小程序，让它只留下适合移动端且符合小程序场景的即开即用的功能。当我带着这个目标重新去审视我那套 HTML+CSS+JS 的旧代码时，我发现现在这样的写法后期维护起来极其麻烦。最让我抓狂的就是组件复用问题。在旧网站里，像导航栏和页脚这种每个页面都有的东西，要么靠人工全量复制粘贴，要么就直接缺失。每次我想改动一个导航菜单，就要把所有 HTML 文件全部手动改一遍。这种“原始人”一样的维护方式，对于我想规范化后期维护的人来说，不仅工作量巨大，而且写起来让人极度讨厌和烦躁。另一个更深层的坎是：我不希望这个官网仅仅是一个死板的展示页面。在我接下来的蓝图里，这个 Web 平台应该是一个“核心底座”——它能作为数据和逻辑的中心，向上能和小程序联通，向下能跟我的客户端工具做交互，而不是各自为战的孤岛。回看过去，这个官网前后重构了不下三次，但过去的改动大多只是 UI 层面的修修补补。而这一次，也许正是意识的苏醒，让我决定不再无谓地徘徊。为了彻底解决组件复用、多端联通以及降低未来维护成本的痛点，我最终敲定了 Nuxt3 + Naive UI + Nginx 这套架构，并决定长期坚持走下去。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;跨端响应式的 UI 抉择，官网的 UI 必须完美兼容移动端、PC 端和平板端。以前尝试过很多 UI 框架，例如WeUI，Bootstrap等，都不尽如人意，我也不想再投入过多精力去试错。虽然我有丰富的后端管理平台 UI 开发经验，但总觉得那些框架不太适合做门户官网。我需要一款既简单易学、又能满足多端响应式需求的 UI 框架。刚好之前做客户端项目时用过 Naive UI，体验极佳，于是这次便果断选择了它。&lt;/li&gt;
&lt;li&gt;确定使用 Naive UI 后，前端生态自然锁定了 Vue3。虽然原生 JavaScript 写简单页面很方便，但在实现复杂功能时，远不如 Vue3 高效。Vue3 内置了诸多便利功能，尤其是对有 Vue2 基础的我来说，上手极快。我非常享受在同一个 Vue 文件中把逻辑、页面、样式“一把刷”搞定的开发体验。&lt;/li&gt;
&lt;li&gt;选定 UI 和前端底座后，我需要一个强大的开发框架来落地想法。这次重构，我计划编写一系列在线工具来替代微信小程序的部分功能，并计划停用小程序这些功能。经过改造后，我的这些工具将彻底面向全球用户开放，而不仅仅是小程序用户。在开发规范上，我更倾向于有组织、有约束的结构。经过一番调研，我发现当下流行的 Nuxt3 正好完美契合我的诉求。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="为什么选择nuxt3"&gt;为什么选择Nuxt3？&lt;/h2&gt;
&lt;p&gt;比如摆脱复制粘贴：以前使用原生开发网页，HTML 代码难以复用，缺乏组件化概念，每个页面都要复制粘贴相同的导航栏和底部，在没有框架的情况下，这些布局都需要每个页面去赋值。在 Nuxt3 中，我可以直接利用 Vue3 的 Layout（布局）机制完美解决。&lt;/p&gt;
&lt;p&gt;状态管理与声明式渲染：纯 JS 操作 DOM 过于繁琐。对于习惯了后端逻辑的我来说，Vue3 的声明式渲染和响应式数据，写起来比纯单页面或原生 DOM 舒服太多，更符合我的思维习惯。&lt;/p&gt;</description></item><item><title>记一次使用egui 构建的轻量小程序码生成工具</title><link>https://blog.91demo.top/mpcode-egui/</link><pubDate>Tue, 12 May 2026 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/mpcode-egui/</guid><description>&lt;p&gt;最近要使用小程序码生成工具，捡起了以前开发的使用 Rust + native-windows-gui 打造极致轻量的小程序工具。&lt;/p&gt;
&lt;p&gt;我发现这个工具最大的痛点就是每次打开都需要重复输入appid和appsecret。我决定修复它，我没有重新开始该项目，是因为我准备使用最近很流行的egui库。使用这个库的另一个原因是我为开发ESP网关客户端做技术储备。&lt;/p&gt;
&lt;p&gt;在开发的中间，我还想使用iced，但是开发了一段时间放弃了，不是iced不好，是因为我没有精力。它适合长期的大型的项目。我现在想快速开发完成，所以egui的界面我忍受了。&lt;/p&gt;
&lt;p&gt;开发完成的初版界面如下：&lt;br&gt;
&lt;img alt="豆子太阳码管家" loading="lazy" src="https://blog.91demo.top/images/suncode-page.png"&gt;&lt;/p&gt;
&lt;p&gt;它极致轻量，只有一个小巧的运行文件，无需安装臃肿的运行库。响应迅速毫秒级启动，操作如丝般顺滑，没有多余的动效和加载等待。&lt;/p&gt;
&lt;p&gt;下载工具后，放入你要生成小程序码的文件夹中，打开工具，填入小程序参数（AppID、路径等），一键获取高清太阳码，小程序码图片就会输出到本文件夹中。 最为暖心的是，应用重新启动后，不用重新输入appid和appsecret，并且会自动获取token。&lt;/p&gt;
&lt;p&gt;下面看看核心代码片段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// --- 主应用状态 ---
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BeanApp&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store: &lt;span style="color:#a6e22e"&gt;AppConfig&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 保存信息
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; appid_raw: String, &lt;span style="color:#75715e"&gt;// appid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret_raw: String, &lt;span style="color:#75715e"&gt;// appsecret
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; access_token: Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 存储 Token
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; expires_in: &lt;span style="color:#66d9ef"&gt;u64&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 存储过期时间戳 (秒)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output_dir: String, &lt;span style="color:#75715e"&gt;// 小程序码输出目录
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; is_pro: &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 是否开启了高级功能
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; active_tab: &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 0: 单张模式, 1: 批量模式
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; page_path: String, &lt;span style="color:#75715e"&gt;// 页面路径
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scene_value: String, &lt;span style="color:#75715e"&gt;// 场景值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; qr_size: &lt;span style="color:#66d9ef"&gt;u32&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 导出尺寸
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; show_crop_line: &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 是否显示裁剪线
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; preview_texture: Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;TextureHandle&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 预览图句柄
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; status_msg: String, &lt;span style="color:#75715e"&gt;// 状态信息
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; show_password: &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 是否显示密码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx: &lt;span style="color:#a6e22e"&gt;Sender&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;AppMessage&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 发送端
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rx: &lt;span style="color:#a6e22e"&gt;Receiver&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;AppMessage&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 接收端
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ctx: &lt;span style="color:#a6e22e"&gt;egui&lt;/span&gt;::Context, &lt;span style="color:#75715e"&gt;// 保存上下文，用于异步刷新
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; is_loading_token: &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 防止重复请求
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是最核心的内容，egui就靠这个结构体，让UI和业务逻辑进行交互。本质就是UI交互区修改这个结构体中的一些内容，然后调用业务逻辑去处理内容，然后再返回信息到这个结构体，然后UI读取这些值的内容再显示出来。&lt;/p&gt;</description></item><item><title>一次从“全量遍历”到“指针索引+异步刷盘”的性能优化历程</title><link>https://blog.91demo.top/b01-arr-map/</link><pubDate>Thu, 07 May 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/b01-arr-map/</guid><description>&lt;p&gt;豆子域名管家是一个使用Wails3开发的域名和证书检测客户端工具。该工具存储域名的结构体经过几次优化，现在已经进化到如下形式：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;type DomainModel struct {
Domain string `json:&amp;#34;domain&amp;#34;` // 域名信息
Port int `json:&amp;#34;port&amp;#34;` // 端口信息，默认443
UpdatedAt int64 `json:&amp;#34;updatedAt&amp;#34;` // 更新时间戳
NextCheckAt int64 `json:&amp;#34;nextCheckAt&amp;#34;` // 下次检测时间
Whois WhoisModel `json:&amp;#34;whois&amp;#34;` // 域名状态
SSL SSLModel `json:&amp;#34;ssl&amp;#34;` // 证书状态
}
type WhoisModel struct {
Expiry int64 `json:&amp;#34;expiry&amp;#34;` // 域名过期时间
RegisteredAt int64 `json:&amp;#34;registeredAt&amp;#34;` // 域名注册时间
LastCheckAt int64 `json:&amp;#34;lastCheckAt&amp;#34;` // 上次扫描时间
Status string `json:&amp;#34;status&amp;#34;` // active, expired, error
LastError string `json:&amp;#34;lastError&amp;#34;` // 最近的错误
}
type SSLModel struct {
Expiry int64 `json:&amp;#34;expiry&amp;#34;` // 证书过期时间
LastCheckAt int64 `json:&amp;#34;lastCheckAt&amp;#34;` // 上次扫描时间
Status string `json:&amp;#34;status&amp;#34;` // valid, warning, expired, error
LastError string `json:&amp;#34;lastError&amp;#34;` // 最近的错误
Issuer string `json:&amp;#34;issuer&amp;#34;` // 签发者信息，方便排查
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个结构体已经涵盖了前端显示的所有信息。为了管理这些域名记录，我又定义了一个结构体Store，它和文件结构也一一对应。如下：&lt;/p&gt;</description></item><item><title>从单一 TCP 握手到 TCP/UDP 全协议覆盖的实战演进</title><link>https://blog.91demo.top/b01-ssl-check/</link><pubDate>Wed, 06 May 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/b01-ssl-check/</guid><description>&lt;p&gt;豆子域名管家是一个使用Wails3开发的域名和证书检测客户端工具。该工具最初围绕标准的 HTTPS（TCP 443 端口）构建。通过 tls.Dial 建立三次握手，获取 PeerCertificates，然后获取到期时间。&lt;/p&gt;
&lt;p&gt;具体的代码片段如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func checkByTCP(ctx context.Context, domain, addr string) (int64, error) {
dialer := &amp;amp;net.Dialer{Timeout: 5 * time.Second}
// 使用 DialWithDialer 但需配合 context 处理取消
conn, err := tls.DialWithDialer(dialer, &amp;#34;tcp&amp;#34;, addr, &amp;amp;tls.Config{
ServerName: domain,
InsecureSkipVerify: true,
})
if err != nil {
return 0, err
}
defer conn.Close()
// 检查 Context 是否已取消
select {
case &amp;lt;-ctx.Done():
return 0, ctx.Err()
default:
cert := conn.ConnectionState().PeerCertificates[0]
return cert.NotAfter.Unix(), nil
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在运行一段时间后，有用户反应无法检测它的域名证书。经过排查后发现，用户使用的webtransport协议，且仅提供了quic流服务。当使用TCP探测纯 QUIC 服务时会直接报 Connection Refused。所以需要优化探测功能，增加基于UDP的证书提取。随着QUIC(UDP)的兴起以及HTTP/3的普及，越来越多的服务开始提供基于UDP的QUIC协议。这增加了新开实现UDP证书检测需求的迫切性。&lt;/p&gt;</description></item><item><title>一步一步将ESP8266 WIFI 配网搞定并丝滑体验</title><link>https://blog.91demo.top/esp-wifi-config/</link><pubDate>Wed, 29 Apr 2026 22:22:34 +0000</pubDate><guid>https://blog.91demo.top/esp-wifi-config/</guid><description>&lt;p&gt;准备入手ESP网关项目了，手头有一块esp8266的开发小板子，就先从esp8266开始入手。白天搞定了ESP8266 的UDP配网编码和小程序端的控制编码。就一个功能模块给ESP8266配置WIFI网络。这是必须的入门操作，因为WIFI 的SSID和密码不能真的写入到代码中。&lt;/p&gt;
&lt;p&gt;网络配置有很多种方式，我选择了&lt;code&gt;SoftAP配网&lt;/code&gt;，虽然配置有点繁琐，但这是一种很可靠的配网方案，兼容性极高，不依赖手机硬件的特殊协议。以前做过HTTP配网，这是2025年时做的，到现在已经忘的差不多了。现在捡起来再回忆一下。&lt;/p&gt;
&lt;p&gt;准确来说，这不应该算是一个项目，但因为它具有通用性和实用性。我决定把它记录下来，可以方便地应用到我的其它项目中。&lt;/p&gt;
&lt;h2 id="soft配网原理"&gt;Soft配网原理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;初始化模式：ESP8266 启动后进入 WIFI_AP_STA 模式。它会开启一个无密码（或已知密码）的热点（AP），并运行一个轻量级的 Web 服务器（HTTP Server）。&lt;/li&gt;
&lt;li&gt;通道建立：手机通过小程序或系统设置连接到该热点。此时手机与 ESP8266 处于同一个局域网内。&lt;/li&gt;
&lt;li&gt;数据交互：小程序通过 HTTP Post 请求将目标 WiFi 的 SSID 和 Password 发送给 ESP8266 的固定接口（如 /config）。&lt;/li&gt;
&lt;li&gt;校验与切换：ESP8266 收到参数后，尝试作为客户端（STA）连接路由器。如果连接成功，则关闭 AP 热点，保存参数到 Flash；如果失败，则返回并重置到AP状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="esp8266端核心代码arduino-ide和小程序端代码"&gt;ESP8266端核心代码（Arduino IDE）和小程序端代码&lt;/h2&gt;
&lt;p&gt;在Arduino IDE打开项目后，需要安装ESP8266 WebServer库。这个库提供了Web服务，可以接收其它HTTP客户端的连接。&lt;/p&gt;
&lt;p&gt;它的核心是实现了一个非常简单的Web服务，它监听&lt;code&gt;/config&lt;/code&gt;接口并能够接收WIFI信息，以及接收之后可以本地存储，实现动态配置的功能。&lt;/p&gt;
&lt;p&gt;下面是Arduino端的代码片段：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#include &amp;lt;ESP8266WiFi.h&amp;gt;
#include &amp;lt;ESP8266WebServer.h&amp;gt;
// 定义AP热点的名称
const char* ap_ssid = &amp;#34;ESP8266_Config_Device&amp;#34;;
ESP8266WebServer server(80);
void handleConfig() {
// 处理SSID和密码逻辑
}
void setup() {
// 设置为AP+STA模式
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(ap_ssid);
// 注册接口
server.on(&amp;#34;/config&amp;#34;, HTTP_POST, handleConfig);
server.begin();
}
void loop() {
server.handleClient();
if (WiFi.status() == WL_CONNECTED) {
// 如果连接成功，可以根据需要在这里处理业务逻辑
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;完整代码可以从&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;下载。小程序端则充当了HTTP客户端，它用来作为输入WIFI信息的终端。这个界面非常简单，一个WiFi信息提交表单，包含了SSID信息和密码信息，以及一个提交按钮。&lt;/p&gt;</description></item><item><title>对于微信小程序的一些回忆和思考</title><link>https://blog.91demo.top/mp-story/</link><pubDate>Fri, 24 Apr 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/mp-story/</guid><description>&lt;p&gt;有些事，不做笔记真的意识不到已经过去了这么久。&lt;/p&gt;
&lt;p&gt;最近在整理博客内容时，我翻到了&lt;strong&gt;小程序&lt;/strong&gt;的最早版本记录。那是 2018 年，最初的它简陋得甚至有点滑稽：整个页面只有一个按钮，点一下，生成一个随机数。&lt;/p&gt;
&lt;h2 id="一豆子工具小程序"&gt;一、豆子工具小程序&lt;/h2&gt;
&lt;p&gt;这个小程序项目名称“Wander”，它的小程序名称换了很多，只有到微信开始要求备案和实名后，才开始真正的确定下来。&lt;/p&gt;
&lt;p&gt;这几年里，我断断续续的维护着这个项目，是由于自己没事的时候喜欢做一些东西，当忙起来时又会忘记了它，当有新的想法时，会改变它，然后更换微信小程序名称。印象深刻的有几次：1，随机数，纯粹是为了小程序上线，为了学习小程序，做了最简单的示例，然后上线了。2，图片集，制作了一个图片列表，然后展示图片，图片存储在云服务上，那是还是免费，并且提供两个环境，为了学习云存储，云函数。3，找厕所，这是我最费心最耗时最吃力的一次改进，为了学习地图map，最后没有完成放弃了。4，自用工具，音频格式转换，图片格式转换，MD5哈希等小工具，从这之后应该算是入门了吧，毕竟以前是自学，现在因为有了工作经历，感觉自己进步了，思维和技巧不在停留在简单的示例教程上。5，工具+项目，以前自学的一些项目以及现在新做的模块都包含进了这个小程序，工具还是自己在用，项目呢，可以用来分享给有需要的人。&lt;/p&gt;
&lt;p&gt;在这几年里，我在这款小程序上倾注了太多的时间和精力，看起来是总在变化，没有一个明确的目标，但是现在回忆总结一下，还有在一个圆内，只不过半径大了一些而已。它像是我个人开发者生涯的一个“活化石”，记录了我每一个阶段遇到的问题和想出的方案。&lt;/p&gt;
&lt;p&gt;总结了最近几年开发过和上线了的一些内容。现在的“豆子工具【已备案】”已经从当年的随机数生成器，进化成了一个覆盖网络、多媒体、开发调试的全能工具箱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;算法&lt;/strong&gt;：随机数生成，MD5，SHA256，RSA，AES，Base64，URL编码等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络&lt;/strong&gt;：获取公网IP地址和归属地，获取 WIFI 公网地址、云厂商安全组管理、查服务端口、域名证书检测，局域网TCP、UDP、WebSocket调试，ESP网络配置等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多媒体&lt;/strong&gt;：音频格式转换（m4a 转 mp3）、图片格式转换（png 转 webp）、二维码识别与生成，图片拼接等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统功能&lt;/strong&gt;：邮件通知、微信服务通知、微信聊天分享，打开文档，导出文档等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;小程序一直在迭代中，这中间的很多功能因为自身原因或者微信审核政策的原因，遗憾地消失在了版本更迭中。&lt;/p&gt;
&lt;h2 id="二豆子碎片小程序"&gt;二、豆子碎片小程序&lt;/h2&gt;
&lt;p&gt;这个小程序项目名称“Visit”，它的微信小程序名称也是变更了多次，只有在备案时才确定了下来。不过它的内容现在已经融入了豆子工具v11.0.0版本中。只是为了维护方便。&lt;/p&gt;
&lt;p&gt;这个Visit小程序在Wander小程序一年后注册，刚开始做的功能就是浏览 Go Web 框架 Beego 的教程。当时，正在自学 Beego，而 Beego 的网站时常打不开，就想做一个小程序，可以浏览 Beego 内容。刚做出来的一段时间，因为新鲜度小火了一把。&lt;/p&gt;
&lt;p&gt;后来又切换到唐诗主题，当时自己费劲的采集了小学的古诗词作为内容源。印象最深的是添加拼音，当时没有AI，从网上搜索和自己琢磨。最后还是放弃了，一是因为素材的原因，没有解析，没有音频，界面美观一般（回忆来看）。到备案的时候，卡住了，个人主体不允许出现诗词这些关键字。要改名字了，我觉得内容不符合，就换内容了，对于我来说，名字需要和内容匹配，心里才会舒服。当时为了选内容而发愁，因为不想放弃这个小程序。想了几天后，觉得把自己的开发经验做成笔记吧。因为碎片化，所以叫豆子碎片。添加豆子前缀，是因为当时备案时，需要区分小程序名称。觉得豆子比较简单上口。那段时间学了towxml，将内容使用Markdown来编写。&lt;/p&gt;
&lt;p&gt;这之后，虽然也改过游戏关卡，作品集，模块集市。但都是通过Markdown来编写和展示内容。&lt;/p&gt;
&lt;h2 id="三思考现在与未来"&gt;三、思考现在与未来&lt;/h2&gt;
&lt;p&gt;今年已经是2026年，AI相当发达，我以前收录的代码片段和想法思考，AI已经分分钟替代，并且做的更好。现在的小程序在备案和实名后，自己的小程序更是无人问津。到了今天，已经没有精力再去想尝鲜了，为了一个新出的功能或者想法去搞好几天去研究了。总结了小程序最近几年的变更，虽然内容一直在变，但是自己在技术上并没有新的突破，依旧是列表详情布局，依旧是本地和后端联动。以前围绕着微信小程序为中心，当抬高视野时，发现局限性很大，小程序很多内容是不能做的，很多功能是做不了的。当前的豆子工具是功能小程序，已经可用的功能如音频格式转换，图片格式转换继续保留。豆子碎片当前的模块集市依旧保留，它是一个项目列表，可以查看详情并可获取到源码和实用客户端工具。所以可以将豆子碎片和豆子工具的内容合并到一起，减少小程序的维护，可以有更多的精力去做一些其他事情。2026年已经计划在博客中写文章了，已经不再使用小程序记笔记，因为小程序的缺陷很明显，特别对于长篇文章。那么未来就可以这样，首页网站可以做Web实验性项目，博客中写项目实现和想法，豆子工具中做小程序最合适的工具和上架模块源码，公众号主要用来引流和发布一些比较好的博客文章，或者发布一些工具更新，或者一些好的开源项目。以后的内容可以深入一些，实用一些，比如以前的模块，现在可以做一个产品。然后继续新的方向，桌面客户端工具和嵌入式开发尝试，继续深入Rust和Go。&lt;/p&gt;</description></item><item><title>小程序激励广告防刷演进史：从前端校验到“挑战模式”</title><link>https://blog.91demo.top/crypto-link/</link><pubDate>Tue, 07 Apr 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/crypto-link/</guid><description>&lt;p&gt;在开发“模块集市”时，用户通过观看激励广告获取源码下载地址。虽然对接广告并不难，但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调（Server-to-Server），奖励的发放完全依赖前端触发，这给了一些“羊毛党”可乘之机。&lt;/p&gt;
&lt;p&gt;在接口防护的过程中，我经历了四个阶段：&lt;/p&gt;
&lt;h2 id="第一阶段身份鉴权解决匿名刷取"&gt;第一阶段：身份鉴权（解决匿名刷取）&lt;/h2&gt;
&lt;p&gt;我的获取奖励接口是&lt;code&gt;getAdReward&lt;/code&gt;。调用它就可以获取奖励，后台进行发放。最初，由于经验不足，我没有对它做任何防护。这样任何人任何客户端都可以进行访问。了解小程序微信账号体系后，我添加了鉴权，只有携带Token的用户可以访问。这样可以保证只有能够调用wx.login的用户才可以访问。&lt;/p&gt;
&lt;p&gt;这解决了一部分问题，但是又有了新的问题，它防不住“真实用户”，只要用户登录后拦截并解析出接口地址和 Token，就可以跳过广告手动调用了&lt;code&gt;getAdReward&lt;/code&gt;接口，没有观看广告就获取了奖励。&lt;/p&gt;
&lt;h2 id="第二阶段共享密钥加密解决接口暴露"&gt;第二阶段：共享密钥加密（解决接口暴露）&lt;/h2&gt;
&lt;p&gt;为了防止直接调用接口，我引入了对称加密。前端与后端约定一个硬编码的共享密钥，在小程序端和服务端同时使用这个共享密钥加密随机数和时间戳进行校验，匹配则发放奖励。这样可以防止fidder等工具直接拦截调用接口。&lt;/p&gt;
&lt;p&gt;它正常运行了一段时间，我发现又有了新的情况。用户可能通过反编译或者其它方式获取到了共享密钥。这在一段时间内对我造成困扰，除了定期更新共享密钥，我没有好的办法，但更新共享密钥需要升级发布小程序。直到当我了解到小程序提供加密网络通道时，我发现这是一个好的解决方案。&lt;/p&gt;
&lt;h2 id="第三阶段微信加密网络通道解决密钥安全"&gt;第三阶段：微信加密网络通道（解决密钥安全）&lt;/h2&gt;
&lt;p&gt;首先，我们来了解一下加密网络通道，它是微信为小程序提供的一套加密KEY机制，它可以同时在微信小程序端和服务器端获取相同的密钥。由于密钥由微信官方通道生成且动态更新，反编译源码也拿不到密钥。除非攻击者能拦截微信的网络通道，否则无法伪造解密过程。同时，配合时间戳校验，有效防御了初级的重放攻击。这对于我来说，是非常好的利好消息，我不需要在源码中硬编码共享密钥了。&lt;/p&gt;
&lt;p&gt;我们可以参考获取短信验证码，短信验证码之所以安全，是因为应用和短信验证码是两个不相同的通道。同样的这个加密KEY，在小程序获取和服务端获取都是通过微信的网络通道，和自己的应用通道也不是一个。对于我的小程序稍加改造就可以了。在获取奖励的接口中，首先使用&lt;code&gt;wx.getUserEncryptKey&lt;/code&gt;获取微信的加密KEY，然后使用自定义的AES加密方法，将要传递的时间戳和Nonce等参数加密后，将加密数据提交给服务器就可以了。当数据到达服务器，服务器同样调用微信的接口获取微信的加密KEY，然后进行AES解密。这样就可以防止获取共享密钥的弊端。&lt;/p&gt;
&lt;p&gt;好了，我们来看看具体如何使用？为了避免小程序与开发者后台通信时数据被截取和篡改，微信侧维护了一个用户维度的可靠key，用于小程序和后台通信时进行加密和签名。开发者可以分别通过小程序前端和微信后台提供的接口，获取用户的加密 key。&lt;/p&gt;
&lt;p&gt;1，小程序端获取：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const somedata = &amp;#39;xxxxx&amp;#39;
const userCryptoManager = wx.getUserCryptoManager()
userCryptoManager.getLatestUserKey({
success({encryptKey, iv, version, expireTime}) {
const encryptedData = someAESEncryptMethod(encryptKey, iv, somedata)
wx.request({
data: encryptedData,
success(res) {
const decryptedData = someAESDEcryptMethod(encryptKey, iv, res.data)
console.log(decryptedData)
}
})
}
})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，someAESEncryptMethod 和 someAESDEcryptMethod 分别为加解密函数，由开发者自行引入加解密库来实现，基础库暂时不提供加解密能力。&lt;/p&gt;
&lt;p&gt;2，服务端获取：&lt;br&gt;
在开发者服务端，可以调用getUserEncryptKey后台接口获取用户最近三次的key。在获取key的同时，接口会携带version信息，开发者可以比较version版本来选择使用对应的key对数据进行加解密。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -X POST &amp;#34;https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=ACCESS_TOKEN&amp;amp;openid=OPENID&amp;amp;signature=SIGNATURE&amp;amp;sig_method=hmac_sha256&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，openid是用户的openid，signature用sessionkey对空字符串签名得到的结果。即 signature = hmac_sha256(session_key, &amp;ldquo;&amp;quot;)，sig_method为签名方法，固定为hmac_sha256。&lt;/p&gt;
&lt;p&gt;这个是核心，获取激励广告奖励基于它再添加一些参数进行加解密。&lt;/p&gt;
&lt;h2 id="第四阶段后端挑战模式解决重放与逻辑伪造"&gt;第四阶段：后端“挑战模式”（解决重放与逻辑伪造）&lt;/h2&gt;
&lt;p&gt;我在参数中添加的Nonce，它是一个噪音参数，它是小程序端本地生成的，还有时间戳也是本地生成的，增加它们是为了增加解密难度。在实际运行中，我发现硬核玩家会通过修改本地时间戳和 Nonce（随机数）来绕过检测，或者使用相同的参数进行重放攻击，或者不够15s（广告正常播放最短时间）调用多次接口。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我将逻辑升级为“挑战模式”：首先，小程序端在广告开始前进行一次预请求，前端必须先调用 getNonce 接口获取Nonce然后使用这个Nonce进行加密，&lt;code&gt;getNonce&lt;/code&gt;是一个新增接口，用来生成Nonce，并记录生成时间和关联openid，服务端存入缓存（如 Redis），完成服务端打标。当前端再次调用&lt;code&gt;getAdReward&lt;/code&gt;接口时，如果差值小于广告常规时长（如 15s），则判定为非正常观看，直接拒绝。&lt;/p&gt;</description></item><item><title>我是如何设计和实现模块源码集市的？</title><link>https://blog.91demo.top/mod-carrier/</link><pubDate>Thu, 26 Mar 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/mod-carrier/</guid><description>&lt;p&gt;当我开发完51单片机温度采集并转换Modbus RTU后，我一度想分享出去，但是我又不想草率的去分享。我一直在思考，如何能让这些代码发挥更大的价值，而不仅仅是躺在个人仓库里。作为一名摸爬滚打多年的开发者，我手头积累了大量来自实践的代码、模块和解决方案。它们散落在硬盘的各个角落，像一颗颗未经打磨的珍珠。 于是，我萌生了做一个“模块源码集市”小程序的想法——一个可以直接浏览、查阅、并一键下载优质源码的平台。&lt;/p&gt;
&lt;p&gt;经过最近一段时间的集中开发，这个小程序终于和大家见面了。今天，我想和大家分享其背后的设计与思考，希望能为有相似想法的朋友提供一些参考。这个小程序的核心功能简洁而实用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首页全景浏览：所有上架的代码模块都在首页清晰展示，你可以快速了解其名称、简介和分类，对全站资源一目了然。&lt;/li&gt;
&lt;li&gt;沉浸式详情阅读：点击任一模块，即可进入详情页。这里我使用了Markdown渲染引擎，来展示模块的详细介绍、使用说明、技术要点等。Markdown的优雅排版能让技术文档的阅读体验大幅提升。&lt;/li&gt;
&lt;li&gt;一键获取源码：在详情页，点击获取模块源码链接按钮，即可轻松获取模块的完整源码压缩包。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了让这个简单的流程稳定、安全、可扩展，我在几个关键环节做了一些设计和取舍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;动态资源与安全通道源码文件并非直接打包在小程序内，而是存储在后台的MinIO对象存储中。当用户点击下载时，服务端会动态生成一个具有时效性的访问链接返回给小程序。这样做的好处是资源可以随时更新。同时，为了维持后端服务器持续运行，我在列表及详情中接入了少量广告。为了保护下载接口不被恶意刷取，我集成了激励式视频广告。用户观看一则简短的广告，即可解锁下载权限。这不仅是简单的广告接入，更重要的是，我为此构建了一套防刷机制，我使用了微信的加密网络通道，对关键数据进行加密传输和验证，极大地增加了自动化盗刷的成本和难度，保护了服务器资源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;灵活的沟通与通知我深知，一个“活”的产品需要与用户保持沟通。因此，小程序内设置了系统消息中心，我可以在这里向所有用户下发重要的系统公告、模块上新以及更新日志。更重要的是，我为每个模块的上新功能，接入了微信服务通知。当有新的、优秀的代码模块入库时，订阅了的用户会在早上8点后收到一条微信消息提醒，能让你随时跟上“仓库”的更新节奏。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;持续生长的内容生态目前上架的或正在上架的模块，覆盖了我擅长的多个领域：小程序、Go语言后端工具、Rust系统编程实践、Web前端片段、嵌入式软硬件代码，包含音视频、网络、加密等技术要点，形式包括客户端、命令行工具、固件、小程序、网页等。这只是一个开始。我的计划是将其作为一个长期项目，持续将我实践中验证过的、有价值的代码沉淀、整理并开源出来。未来，我还会在项目详情页中，增加关联的公众号技术文章跳转，或是有功能关联的其他小程序跳转，形成一个立体化的技术知识网络。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;坦率地说，这个小程序的开发过程远超我最初的预期。从架构设计到具体实现，尤其是几个关键的技术卡点上，耗费了我大量的精力。&lt;/p&gt;
&lt;p&gt;与广告盗刷的攻防：我的服务器每天都有人在扫描，所以在实现激励广告防刷机制时，我花了大量时间加强防刷机制，其实这个在微信小程序社区中也可以看到很多类似的问题。我这里研究了一种解决方案。就是使用加密网络通道。在对接的过程中，我与各种加密错误、签名错误（如常见的40079错误）作斗争。例如，处理会话密钥时，发现不需要进行Base64编码，而是直接以空字符串的字节形式处理；在URL拼接的细节上，POST与GET方法的误用也会导致失败；而加解密环节更是陷阱重重——加密密钥需要从Base64解码，IV却是一个直接的16位字符串，加密数据则需要从十六进制格式解码……这些细枝末节，任何一个出错都会导致整个流程崩掉，往往需要清空Token重新登录来排查。这个过程让我对网络数据安全有了更深刻的认识。这些错误和经验本想专开一篇文章介绍，但是细想一下，可能一段话就能总结，但是其中的辛酸和感悟无法表达在纸面上。索性后期整理出来开源这个模块，减少其他开发者的一些坎坷。&lt;/p&gt;
&lt;p&gt;存储方案的抉择：在对象存储的选择上，也有一段小插曲。我最熟悉的是Mino，但是在我了解到MinIO版本已归档，便尝试了rustfs等其他方案进行探索。但经过综合对比在可靠性、API友好度以及与现有技术栈的契合度后，最终还是选择了功能完备、文档清晰的MinIO作为存储后端，事实证明这个选择是稳定高效的。&lt;/p&gt;
&lt;p&gt;所幸，在这个艰难的过程中，AI编程助手给予了我巨大的帮助（但是也让我吃了很多苦头，因为它上面的资料都比较旧）。许多代码片段的生成、调试过程中错误信息的解读、API文档的快速查询，都因AI的介入而效率倍增。如果没有它，单靠我自己，恐怕几个月也难见成果，绝无可能在十几天内推动项目成型上线。&lt;/p&gt;
&lt;p&gt;做这个小程序，并非为了追逐热点，这个小程序刚刚上线，还需要时间去细细打磨。开发其初心很简单：整理自己的知识资产，并以一种对开发者最友好、最便捷的方式分享出去。我希望它不仅仅是一个下载站点，更可以成为一个高质量的、经过实战检验的“代码片段集市”和“工具箱”。路漫漫其修远兮。在代码世界的探索中，每个人都是学生，也是老师。我在这里抛砖引玉，期待这些代码模块能真正帮助到在具体问题上寻找解决方案的你。也欢迎你常来看看，这些项目模块，会和我一样，在技术的道路上不断生长。&lt;/p&gt;
&lt;p&gt;扫码体验：&lt;img src="https://blog.91demo.top/images/visit.webp" width="200" alt="豆子碎片小程序"&gt;&lt;/p&gt;</description></item><item><title>在51单片机实现 Modbus RTU 协议过程中的踩坑和思考</title><link>https://blog.91demo.top/c51-sensemodbus/</link><pubDate>Wed, 18 Mar 2026 08:05:33 +0000</pubDate><guid>https://blog.91demo.top/c51-sensemodbus/</guid><description>&lt;p&gt;在上篇原理文章介绍后，我开始实现代码，硬件使用现成的51开发板。原本以为只是简单的串口收发，结果却在 Modbus Poll 软件中疯狂循环 Checksum Error 和 Timeout。&lt;/p&gt;
&lt;p&gt;下面是我在普中科技 51 开发板上，使用STC89C52芯片接入工业标准的 Modbus RTU 协议，从最基础的串口打印到实现动态温度采集，并成功通过 Modbus 协议回传的完整过程。&lt;/p&gt;
&lt;h2 id="环境准备"&gt;环境准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;硬件： 普中 51-A2 开发板 (STC89C52RC)、DS18B20 温度传感器、USB 转串口线。&lt;/li&gt;
&lt;li&gt;软件： Keil uVision5、STC-ISP、Modbus Poll (主机仿真器)、SSCOM 串口调试助手。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="51开发板" loading="lazy" src="https://blog.91demo.top/images/embedded/51board.webp"&gt;&lt;/p&gt;
&lt;h2 id="核心挑战为什么-modbus-poll-总是报-checksum-error"&gt;核心挑战：为什么 Modbus Poll 总是报 Checksum Error？&lt;/h2&gt;
&lt;p&gt;这是我耗时最长的地方，也是我头疼的地方。如果你也遇到数据看着对但校验不过，请检查以下三点：&lt;/p&gt;
&lt;h3 id="查表法的陷阱"&gt;查表法的陷阱&lt;/h3&gt;
&lt;p&gt;因为51单片机的性能问题，我使用了查表法，CRC表中的数据是采摘网上教程的。在解决了之后，发现是自己设置的表中的数值不对。要知道Modbus 的 CRC16 校验非常严苛。为了方便以后的开发者，我把正确的CRC表和获取函数都贴出来。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 高位表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; code aucCRCHi[] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 低位表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; code aucCRCLo[] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x00&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x01&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x03&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x02&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x06&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x07&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x05&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x04&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0xCC&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x0C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x0D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xCD&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x0F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xCF&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xCE&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x0E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x0A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xCA&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xCB&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x0B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC9&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x09&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x08&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xC8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0xD8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x18&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x19&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD9&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x1B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xDB&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xDA&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x1A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x1E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xDE&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xDF&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x1F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xDD&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x1D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x1C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xDC&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x14&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x15&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x17&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x16&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x12&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x13&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x11&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xD0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x10&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0xF0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x30&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x31&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x33&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x32&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x36&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x37&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x35&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x34&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF4&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x3C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xFC&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xFD&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x3D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xFF&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x3F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x3E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xFE&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xFA&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x3A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x3B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xFB&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x39&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF9&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xF8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x38&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x28&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE9&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x29&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xEB&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x2B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x2A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xEA&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xEE&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x2E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x2F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xEF&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x2D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xED&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xEC&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x2C&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0xE4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x24&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x25&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x27&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x26&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x22&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x23&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x21&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x20&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xE0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0xA0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x61&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x63&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x62&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x66&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x67&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x65&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x64&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA4&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x6C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xAC&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xAD&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x6D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xAF&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x6F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x6E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xAE&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xAA&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x6A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x6B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xAB&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x69&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA9&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xA8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x68&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x78&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB9&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x79&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xBB&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x7B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x7A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xBA&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xBE&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x7E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x7F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xBF&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x7D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xBD&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xBC&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x7C&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0xB4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x74&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x75&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x77&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x76&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x72&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x73&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x71&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x70&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0xB0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x50&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x90&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x91&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x51&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x93&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x53&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x52&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x92&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x96&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x56&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x57&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x97&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x55&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x95&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x94&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x54&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x9C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x5C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x5D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x9D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x5F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x9F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x9E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x5E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x5A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x9A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x9B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x5B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x99&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x59&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x58&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x98&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x88&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x48&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x49&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x89&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x4B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x8B&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x8A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x4A&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x4E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x8E&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x8F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x4F&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x8D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x4D&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x4C&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x8C&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0x44&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x84&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x85&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x45&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x87&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x47&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x46&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x86&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x82&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x42&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x43&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x83&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x41&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x81&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x80&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0x40&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 获取CRC
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GetCRC16&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;pData, &lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; len) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; uIndex;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; crch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xFF&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; crcl &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xFF&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; (len&lt;span style="color:#f92672"&gt;--&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uIndex &lt;span style="color:#f92672"&gt;=&lt;/span&gt; crcl &lt;span style="color:#f92672"&gt;^&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;pData&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; crcl &lt;span style="color:#f92672"&gt;=&lt;/span&gt; crch &lt;span style="color:#f92672"&gt;^&lt;/span&gt; aucCRCHi[uIndex];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; crch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; aucCRCLo[uIndex];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;unsigned&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;)(crch &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; crcl);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="字节序与-quantity-的对齐"&gt;字节序与 Quantity 的对齐&lt;/h3&gt;
&lt;p&gt;这里也是一个坑，自己概念不清，在设置quantity时设置错误，错把他们1比1进行换算。还有Modbus 规定 低字节在前，高字节在后。&lt;/p&gt;</description></item><item><title>为豆子域名管家实现版本检测功能的技术实践</title><link>https://blog.91demo.top/ssl-checker/</link><pubDate>Tue, 17 Mar 2026 09:00:00 +0800</pubDate><guid>https://blog.91demo.top/ssl-checker/</guid><description>&lt;p&gt;豆子域名管家是一款基于 Wails 3 开发的超轻量级单文件客户端。它不仅拥有 Naive UI 打造的精致界面，更集成了强大的域名监控能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全方位监测：主面板表格直观展示域名列表、SSL 证书剩余天数、Whois 到期时间。&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;h2 id="为什么需要版本提醒为什么不是自动更新"&gt;为什么需要版本提醒，为什么不是自动更新？&lt;/h2&gt;
&lt;p&gt;在开发 豆子域名管家 时，Wails 3 就提供了自动集成的更新方案，但经过深思熟虑，我选择了“版本提醒 + 手动覆盖”的策略：主要考虑到个人服务器的带宽限制，避免高并发下载造成服务器宕机。单文件二进制直接覆盖即可完成升级，无需复杂的安装程序。&lt;/p&gt;
&lt;p&gt;在之前的版本中，我主要通过公众号发布新版本并提供下载链接进行手动升级。它的问题是用户需要关注公众号并手动前往蓝奏云下载。为了进一步优化体验，我决定在新版本中引入版本对比提醒：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;红点微标提醒：客户端启动后会自动对比本地与云端版本号。若有新版，左下角版本号将出现灵动的红点徽标，提醒而不打扰。&lt;/li&gt;
&lt;li&gt;交互式遮罩弹窗：点击徽标即可弹出基于 Naive UI 开发的漂亮窗口，清晰展示：
&lt;ul&gt;
&lt;li&gt;当前版本 vs 最新版本&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;/li&gt;
&lt;/ul&gt;
&lt;h2 id="版本检测核心实现思路"&gt;版本检测核心实现思路&lt;/h2&gt;
&lt;p&gt;这不仅仅是一个简单的弹窗，背后凝聚了对安全和灵活性的思考。为了确保版本检测既高效又安全，我们采用了以下技术路径：&lt;/p&gt;
&lt;p&gt;1，我没有在前端 JS 中硬编码版本号。使用了Go 后端驱动，客户端通过 Wails 3 的原生桥接功能，调用后端 Go 方法获取本地编译时的版本标识。&lt;/p&gt;
&lt;p&gt;2，防爬虫机制：在向云端请求最新版本信息时，配置了特定的 Custom Header 校验，有效拦截恶意脚本和不必要的扫描，保护服务器资源。&lt;/p&gt;
&lt;p&gt;3，设计了灵活的云端数据结构。我们的版本接口不只是返回一个数字，而是返回一个包含三个核心维度的 JSON 对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最新版本号：用于精准比对。&lt;/li&gt;
&lt;li&gt;下载地址：支持动态变更下载镜像，防止因链接失效导致的无法更新。&lt;/li&gt;
&lt;li&gt;更新日志：让用户第一时间了解修复了哪些 Bug 或新增了哪些功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4，极致的 UI 交互（基于 Naive UI）&lt;br&gt;
当本地版本低于云端时，左下角会亮起精美的红点徽标。点击后的交互逻辑非常人性化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信息全透明：对比当前版本与新版本，展示完整的更新列表和使用指南。&lt;/li&gt;
&lt;li&gt;尊重用户选择：弹窗配备了清晰的“取消/稍后再说”按钮，绝不强制升级。&lt;/li&gt;
&lt;li&gt;快捷操作：提供“复制下载链接”按钮，点击后自动写入剪切板。用户只需打开浏览器粘贴，即可从蓝奏云等高速渠道下载，覆盖即升级。免去了前期的复杂步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="界面功能效果预览"&gt;界面功能效果预览&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;徽标提示&lt;br&gt;
&lt;img alt="alt 徽标提示" loading="lazy" src="https://blog.91demo.top/images/go/vercheck-tips.png"&gt;&lt;/li&gt;
&lt;li&gt;弹窗说明&lt;br&gt;
&lt;img alt="alt 弹窗说明" loading="lazy" src="https://blog.91demo.top/images/go/vercheck-window.png"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="工具如何开始使用"&gt;工具如何开始使用？&lt;/h2&gt;
&lt;p&gt;无论您是拥有几十个域名的运维大拿，还是只有几个小站的个人玩家，豆子域名管家都是您的不二之选。&lt;/p&gt;</description></item><item><title>解决 Cloudreve 无法通过 HTTPS 连接自签名证书 MinIO</title><link>https://blog.91demo.top/cloudreve-s3/</link><pubDate>Wed, 11 Mar 2026 08:00:00 +0800</pubDate><guid>https://blog.91demo.top/cloudreve-s3/</guid><description>&lt;p&gt;这是一次真实事件案例改编。用户A在部署 Cloudreve 存储策略时，集成了用户内网环境下的 MinIO S3 服务。但是当用户上传资料时发现Cloudreve无法工作，无法将资源上传到MinIO中。&lt;/p&gt;
&lt;p&gt;我也是第一次接触Cloudreve，经过搜索发现&lt;a href="https://github.com/cloudreve/cloudreve"&gt;Cloudreve&lt;/a&gt;是一个自托管文件管理与共享系统，支持多存储提供商。那么现在的问题就变成了Cloudreve如何配置使用了自定义证书的S3服务Minio。&lt;/p&gt;
&lt;p&gt;由于用户的Minio服务使用了DDNS映射到公网，我首先想到的是，使用可以通过mc访问到用户的Minio服务。经过初步排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认内网穿透链路正常。&lt;/li&gt;
&lt;li&gt;使用 MinIO 客户端工具 mc 测试，发现必须带上 &amp;ndash;insecure 参数才能正常访问，明确了核心矛盾在于 SSL 证书验证失败。&lt;/li&gt;
&lt;li&gt;尝试将证书放置在自定义目录并使用 mc &amp;ndash;config-dir 指定，可以连接成功，但 Cloudreve 还是无法正常工作，无法直接复用该配置，且即便在同目录下放置证书也未能生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;经过搜索和分析，知道需要将自定义 CA 证书导入系统信任库，才能使 Cloudreve（Go 环境）能够原生识别。那么现在的问题就变成了如何在 CentOS 系统中安装并激活自定义证书？&lt;/p&gt;
&lt;p&gt;经过一番折腾摸索，终于搞定，这里记录一下历程。&lt;/p&gt;
&lt;h3 id="1检查证书是否pem格式"&gt;1，检查证书是否PEM格式？&lt;/h3&gt;
&lt;p&gt;Go 程序通常识别PEM格式的证书。所以需要先检查自定义证书是否是PEM格式？可以使用以下命令检查证书文件（如public.crt）：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat public.crt
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;如果开头是 &amp;mdash;&amp;ndash;BEGIN CERTIFICATE&amp;mdash;&amp;ndash;，则是 PEM 格式。&lt;/li&gt;
&lt;li&gt;如果是一堆乱码，则是 DER 格式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果是DER格式，需要转换为PEM格式，转换的命令如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;openssl x509 -inform der -in public.crt -out public.pem
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2放置证书"&gt;2，放置证书&lt;/h3&gt;
&lt;p&gt;在CentOS Linux中，需要将证书文件放在/etc/pki/ca-trust/source/anchors/目录下，并且后缀必须为.crt。如果文件名为public.pem，需要改为public.crt。这是血的经验和教训。&lt;/p&gt;
&lt;p&gt;首先确认文件是否放置正确？&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ls /etc/pki/ca-trust/source/anchors/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;输出中应包含我们的证书，如果没有，需要重新放置。&lt;/p&gt;
&lt;h3 id="3激活证书"&gt;3，激活证书&lt;/h3&gt;
&lt;p&gt;当放置好证书后，必须进行激活，否则系统不认。可以执行以下命令激活，让系统重新扫描目录并生成全局信任束，依次执行：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;update-ca-trust extract
update-ca-trust
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="4验证证书是否激活成功"&gt;4，验证证书是否激活成功&lt;/h3&gt;
&lt;p&gt;我们需要确认是否激活成功？确认的方法就是检查系统全局证书合集&lt;code&gt;ca-bundle.crt&lt;/code&gt;是否已包含你的证书信息：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tail -n 20 /etc/pki/tls/certs/ca-bundle.crt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以查看最近的20行中是否包含自己的证书，如果还没有成功，则执行命令追加：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat public.crt | sudo tee -a /etc/pki/tls/certs/ca-bundle.crt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最后，使用 grep 再次搜索证书中关键的信息&lt;/p&gt;</description></item><item><title>自制可使用 Modbus 采集的 RS485 温度传感器（原理验证版）</title><link>https://blog.91demo.top/idea-sensetemp/</link><pubDate>Fri, 06 Mar 2026 20:05:33 +0000</pubDate><guid>https://blog.91demo.top/idea-sensetemp/</guid><description>&lt;p&gt;我将使用经典的 STC89C12 单片机作为核心，配合 DS18B20 数字温度传感器和 MAX485 通信芯片，构建一个支持标准 Modbus-RTU 协议的感知节点。&lt;/p&gt;
&lt;h2 id="一核心架构传感器大脑与传声筒"&gt;一、核心架构：传感器、大脑与传声筒&lt;/h2&gt;
&lt;p&gt;这个小模块的本质是一个“翻译官”。它把环境中的物理温度转化为数字信号，再按照工业标准协议通过长线传输给上位机（如 PLC 或电脑）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;感知单元 (DS18B20)： 不同于传统的模拟热敏电阻，它是数字传感器，直接输出 12 位精度的二进制温度数据。&lt;/li&gt;
&lt;li&gt;处理中心 (STC89C12)： 负责按照时序“读”传感器，并把数据存入内存，同时监听串口指令。&lt;/li&gt;
&lt;li&gt;通信接口 (MAX485)： 单片机的 TTL 信号传不远，MAX485 将其转换为差分信号，实现抗干扰的长距离传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二硬件链路设计"&gt;二、硬件链路设计&lt;/h2&gt;
&lt;p&gt;为了简化电路并验证可行性，我们将引脚定义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DS18B20 接口： 连接至 P1.0。由于 1-Wire 总线需要上拉电阻，我们在硬件上需确保 P1.0 与 VCC 之间有一个 4.7kΩ 的电阻，以维持空闲时的高电平。&lt;/li&gt;
&lt;li&gt;MAX485 控制：
&lt;ul&gt;
&lt;li&gt;UART 接口： RXD(P3.0) 接 RO，TXD(P3.1) 接 DI。&lt;/li&gt;
&lt;li&gt;收发切换 (RE/DE)： 连接至 P3.2。RS485 是半双工的，平时 P3.2 置低电平处于“听”模式；当需要回传数据时，将其置高切换为“说”模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;烧录接口： 仅预留 VCC、GND、TXD、RXD 四线接口，用于程序的迭代验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三技术实现思路"&gt;三、技术实现思路&lt;/h2&gt;
&lt;h3 id="a-如何采集-ds18b20"&gt;A. 如何采集 DS18B20？&lt;/h3&gt;
&lt;p&gt;STC89C12 通过 单总线 (1-Wire) 时序 与传感器对话。由于 STC89C12 速度较慢且不支持硬件单总线，我们需要通过精准的软件延时来模拟时序：&lt;/p&gt;</description></item><item><title>为豆子域名管家实现Windows开机自启动功能的技术实践</title><link>https://blog.91demo.top/domain-autostart/</link><pubDate>Sun, 01 Mar 2026 09:00:00 +0800</pubDate><guid>https://blog.91demo.top/domain-autostart/</guid><description>&lt;p&gt;在PC客户端开发中，开机自启动是提升用户体验的重要功能之一。豆子域名管家作为一款Windows平台下的域名管理工具，近期添加了随系统启动功能。本文将详细介绍从技术选型到最终实现的全过程，重点阐述在跨平台库适配失败后，如何针对Windows系统特性实现简洁可靠的自启动方案。&lt;/p&gt;
&lt;h2 id="一技术选型与挑战"&gt;一、技术选型与挑战&lt;/h2&gt;
&lt;h3 id="11-初始方案跨平台库的尝试"&gt;1.1 初始方案：跨平台库的尝试&lt;/h3&gt;
&lt;p&gt;项目初期采用了go-autostart这一流行的Go语言跨平台自启动库。该库设计优雅，理论上支持Windows、macOS、Linux三大主流操作系统。然而在实际集成到wails3项目中时，遇到了编译问题：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sslchecker
.\domainservice.go:1453:11: app.Enable undefined (type *autostart.App has no field or method Enable)
.\domainservice.go:1455:11: app.Disable undefined (type *autostart.App has no field or method Disable)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;经过排查，发现虽然能定位到源码，但由于系统配置或依赖管理问题，无法正确调用库方法。这种问题在Go模块化开发中并不少见，特别是涉及CGO或系统特定依赖时。&lt;/p&gt;
&lt;h3 id="12-平台限制的现实考量"&gt;1.2 平台限制的现实考量&lt;/h3&gt;
&lt;p&gt;进一步分析发现，即使解决编译问题，跨平台方案仍面临以下限制：&lt;/p&gt;
&lt;p&gt;macOS的签名要求：自macOS Catalina以来，苹果加强了应用安全策略。无签名的应用在开机自启动时会被Gatekeeper拦截，除非用户手动进入系统设置&amp;gt;安全性与隐私&amp;gt;通用中点击&amp;quot;仍要打开&amp;quot;。&lt;/p&gt;
&lt;p&gt;Linux的碎片化：不同桌面环境（GNOME、KDE、XFCE等）的自启动机制存在差异，需要适配多种配置方式。&lt;/p&gt;
&lt;p&gt;维护成本：跨平台库在提供便利的同时，也引入了额外的依赖和潜在的兼容性问题。&lt;/p&gt;
&lt;p&gt;考虑到豆子域名管家主要用户群体为Windows用户，且无macOS开发者证书，决定采用专注Windows的轻量化方案。&lt;/p&gt;
&lt;h2 id="二windows自启动实现方案"&gt;二、Windows自启动实现方案&lt;/h2&gt;
&lt;h3 id="21-技术原理"&gt;2.1 技术原理&lt;/h3&gt;
&lt;p&gt;Windows开机自启动主要通过注册表实现。当前用户的自启动项位于：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;系统级的自启动项（需要管理员权限）位于：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于大多数桌面应用，使用用户级注册表即可满足需求，且无需提权操作。&lt;/p&gt;
&lt;h3 id="22-核心实现代码"&gt;2.2 核心实现代码&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// auto_start_windows.go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// +build windows&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;package&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;errors&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;os&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;path/filepath&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;strings&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;golang.org/x/sys/windows/registry&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// AutoStartManager Windows自启动管理器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// NewAutoStartManager 创建自启动管理器实例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;NewAutoStartManager&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;) (&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;os&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Executable&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;errors&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;New&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;获取可执行文件路径失败&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 转换为绝对路径并处理空格&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;filepath&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Abs&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Contains&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34; &amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;`Software\Microsoft\Windows\CurrentVersion\Run`&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// IsAutoStartEnabled 检查是否已启用开机自启动&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;IsAutoStartEnabled&lt;/span&gt;() (&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;OpenKey&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;CURRENT_USER&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;QUERY_VALUE&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Close&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;GetStringValue&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;ErrNotExist&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 对比路径，处理可能的引号差异&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;current&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Trim&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;stored&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Trim&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;EqualFold&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;filepath&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Clean&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt;), &lt;span style="color:#a6e22e"&gt;filepath&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Clean&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;stored&lt;/span&gt;)), &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// SetAutoStart 设置或取消开机自启动&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;SetAutoStart&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;enable&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;OpenKey&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;CURRENT_USER&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;QUERY_VALUE&lt;/span&gt;|&lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;SET_VALUE&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Close&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;enable&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;SetStringValue&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; = &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;DeleteValue&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;ErrNotExist&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 键不存在不算错误&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="23-关键实现细节"&gt;2.3 关键实现细节&lt;/h3&gt;
&lt;p&gt;路径处理：使用os.Executable()获取可执行文件绝对路径，确保在不同工作目录下都能正确运行。&lt;/p&gt;</description></item><item><title>从“半夜巡栏”到“智能换气”：我把黑盒搬进了猪舍</title><link>https://blog.91demo.top/idea-pigsty/</link><pubDate>Fri, 27 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/idea-pigsty/</guid><description>&lt;h2 id="一-那件披在身上的大衣"&gt;一、 那件披在身上的大衣&lt;/h2&gt;
&lt;p&gt;老家人养猪，冬天的半夜，都要披上厚重的大衣，去猪舍看产床温度。猪舍的墙上挂了一个水银温度计。温度低了猪仔会冻死，温度高了会脱水。氨气重了会生病，感觉味道重了，需要手动打开抽风机。这种“靠人巡、靠鼻闻、靠经验”的原始模式，不仅累，而且风险极高。&lt;/p&gt;
&lt;p&gt;供暖烧煤炉，煤炉需要半夜起来加煤，人困得不行；母猪还得使用电热板，电热板是那种电阻丝加热的，没有温控计，只能隔几个小时去关掉，等温度降下去再打开。说到底，还是得靠人去“看着”。&lt;/p&gt;
&lt;p&gt;我想，能不能用技术，把家人从这种重复、枯燥且充满风险的体力劳动中解放出来一点点？不用花太多钱，因为他们会心疼。&lt;/p&gt;
&lt;h2 id="二-攻坚方案黑盒的温控逻辑"&gt;二、 攻坚方案：黑盒的温控逻辑&lt;/h2&gt;
&lt;p&gt;可以看到面临的主要是“环境失控风险”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;温度控制的极端性：依靠烧煤和简易电热板，温度分布不均。半夜人工起夜不仅辛苦，且这种“大跨度”的温差变化会导致猪仔腹泻甚至死亡。&lt;/li&gt;
&lt;li&gt;空气质量的隐形威胁：氨气超标是诱发呼吸道疾病的主因。仅靠“鼻闻”时，空气质量往往已经恶化到危及健康的程度。&lt;/li&gt;
&lt;li&gt;利润被风险蚕食：养猪大头是料钱和药钱，但成活率才是利润的底线。一次深夜的失误，可能让数月的辛苦付诸东流。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;能马上想到的临时解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;恒温自动化（解决“半夜加煤”与“手动插拔”）&lt;br&gt;
温控传感器方案：购买带有探头的温控开关，将电热板连接到温控插座上。&lt;br&gt;
效果：设定好区间（如32-35度），温度低了自动通电，够了自动断电。不仅省去了人工看管，还能通过减少无效耗电抵消设备成本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环境预警系统（解决“鼻闻”与“风险”）&lt;br&gt;
智能监控终端：安装一个集成了温湿度监控与氨气检测的智能传感器，通过 Wi-Fi 或 4G 信号连接手机。&lt;br&gt;
效果：当温度异常或氨气浓度过高时，手机会自动发出警报铃声提醒，避免了老人家整晚不敢闭眼的焦虑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;联动抽风（解决“呼吸道疾病”）&lt;br&gt;
自动排风系统：将原有的抽风机改装为智能联动。&lt;br&gt;
效果：当氨气传感器感应到超标，自动开启抽风机，达标后关闭。这能精准减少热量流失，同时保证猪舍空气清新。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="核心硬件选择务实与成本的平衡"&gt;核心硬件选择：务实与成本的平衡&lt;/h3&gt;
&lt;p&gt;因为猪舍现场环境非常恶劣，湿度，氨气，粉尘等因素，温控开关坏的频率非常高，如果接到料房，又采集不了温度。购买氨气专业设备成本又非常高。现场可以根据温度和通风进行解决，但在冬季，保暖是另一个问题。实际考量后，还是自己动手做比较划算。零部件能够直接买并满足现场需求的，直接购买，满足不了，自己动手制作，需要防腐蚀处理：焊接完成后，用酒精清洗焊渣，然后必须喷涂“三防漆”。外壳选用 IP65 防水接线盒，进线口使用电缆防水密封接头 (PG7)，否则氨气会从缝隙钻进去。线路外层加PVC管，防鼠咬，腐蚀，机械损伤。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;传感器：水银温度计不行，无法采集。工业级的 RS485 温湿度传感器太贵（动辄上百元），购买不锈钢封装好的民用级的DS18B20数字温度传感器，焊接后要密封好，挂在猪舍合适的位置。它通过一根单总线连接，协议简单，成本极低。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主控：具备 NB-IoT 通信功能的物联网核心板或者采用分布式架构，先接一个STM32控制黑盒，然后再连接物联网黑盒，它集成了主控芯片和 NB-IoT 模块，可以直接连接运营商网络。它负责读取传感器、执行逻辑、控制输出，并具备联网能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通信：猪舍没有Wi-Fi，即使有WiFi，也推荐使用RS485线连接，考虑如下：稳定性以及长期收益。布线不方便的地方，可以使用4G或者NBIot模块，用一张物联网卡，可以直接走运营商的网络上传数据，无需在猪舍和住房之间拉网线，年流量费仅需十几元。这是实现“无线化”的关键，需要注意的是，如果在猪舍内加装铁盒，需要接外接天线，防止信号屏蔽。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;报警：前期控制投入，不用手机流量卡和云平台。购买一个433MHz无线门铃，使用黑盒驱动，黑盒使用IP65 防水塑料接线盒，把发射模块接在主控板上。当需要报警时，主控板模拟按下门铃按钮，屋里就会“叮咚”响。这是最直接、最可靠的本地告警。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="第一阶段从定时巡到听铃响"&gt;第一阶段：从“定时巡”到“听铃响”&lt;/h3&gt;
&lt;p&gt;目标：解决“夜间需要频繁固定间隔的去猪舍”的核心痛点，将人工巡检间隔时间拉长到“只在有异常时响应”。&lt;/p&gt;
&lt;h4 id="系统架构与原理"&gt;系统架构与原理：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;感知：主控板 不断读取 DS18B20 的温度值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;决策：我在主控板的固件里写死一段简单的判断逻辑（例如：if (温度 &amp;lt; 18度 或 温度 &amp;gt; 28度)）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行：一旦条件满足，主控板 立即驱动 GPIO 引脚，向 433MHz 发射模块发送一个高电平脉冲信号。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;告警：433MHz 信号被屋里的门铃接收器捕获，触发响铃。屋里的人听到铃声，就知道猪舍温度异常，需要去查看。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="可执行性与落地关键"&gt;可执行性与落地关键：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;硬件连接：仅需将 DS18B20 的数据脚、电源、地线接到主控板对应引脚；将 433MHz 发射模块的信号脚接到另一个 GPIO 引脚。接线简单，无需复杂电路。&lt;/p&gt;</description></item><item><title>水表、电表、热表：一个“黑盒”如何撬动千亿级存量市场中的利旧改造细分蓝海</title><link>https://blog.91demo.top/idea-watermeter/</link><pubDate>Thu, 26 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/idea-watermeter/</guid><description>&lt;h2 id="一-场景从插卡洗澡到手机充值的断层"&gt;一、 场景：从“插卡洗澡”到“手机充值”的断层&lt;/h2&gt;
&lt;p&gt;“洗澡洗一半，卡里没钱了，得湿着身子跑下楼去圈存机充钱。”——这是十年前大学宿舍的常态。&lt;/p&gt;
&lt;p&gt;我参与过那个“刷卡时代”的项目。彼时，每个宿舍楼都有一个弱电井，里面几十个水表通过RS-485总线串联。学生用M1卡洗澡，钱存在卡里，水表是“单机版”。宿管中心不知道哪个宿舍快没水了，只能被动等待报修。&lt;/p&gt;
&lt;p&gt;核心痛点：用户需要手机充值的便捷，但海量的老式水表（及电表、热表）是“数字孤岛”，只有RS-485接口，不具备任何联网能力。整体更换为物联网表成本极高，且施工影响巨大。&lt;/p&gt;
&lt;h2 id="二-方案一个黑盒唤醒沉默的数据"&gt;二、 方案：一个“黑盒”，唤醒沉默的数据&lt;/h2&gt;
&lt;p&gt;我们的方案是添加一个 “黑盒”——一个协议转换网关。它的逻辑很简单：不换表，不改线，只做“翻译官”。&lt;/p&gt;
&lt;p&gt;物理部署：在弱电井的485总线汇接处，并联接入“黑盒”。原有的刷卡系统完全保留，作为保底。&lt;/p&gt;
&lt;h3 id="核心工作"&gt;核心工作：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;采集：黑盒内置高性能Modbus协议栈，主动轮询总线上所有水表，读取余额、用量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转换：将不同品牌、不同协议的水表数据，统一“翻译”成标准的JSON格式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上传：通过4G或网线，将数据通过MQTT协议上传至云平台。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="手机充值闭环"&gt;手机充值闭环：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;学生在小程序支付。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;云端将“为XX房号充值XX元”的指令下发给对应黑盒。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关键一步：黑盒将指令“反向翻译”成目标水表能识别的、符合其私有协议的485报文，完成“写卡”操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统确认后，充值到账。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="三-定位在巨头缝隙中做数字化的梯子"&gt;三、 定位：在巨头缝隙中，做“数字化的梯子”&lt;/h2&gt;
&lt;p&gt;我深知，国家级、城市级的智慧水务/能源项目，是头部玩家的“铁桶阵”，他们依靠专用网络、5G和强大的工程能力构建壁垒。&lt;/p&gt;
&lt;p&gt;“黑盒”的目标，是那片被忽略的“长尾市场”：&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;p&gt;对他们而言，动辄数十万的整体改造方案是不可承受之重。而一个单价数百元、即插即用、半天可部署、不动原有设施的“黑盒”，是他们迈入数字化管理的唯一可行阶梯。&lt;/p&gt;
&lt;h2 id="四-挑战与壁垒并非即插即用的童话"&gt;四、 挑战与壁垒：并非“即插即用”的童话&lt;/h2&gt;
&lt;p&gt;这个方案在逻辑上自洽，但真实的商业落地远非易事，存在多重壁垒：&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;性能与稳定性壁垒：RS-485总线是半双工，挂载几十块表后，轮询周期会拉长，数据实时性下降。网络波动、设备干扰可能导致指令执行失败，需要设计复杂的重试、容错和事务一致性机制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;渠道与商务壁垒：如何触达并说服高校后勤、房东这些分散的客户？如何提供及时可靠的现场支持？这比技术开发更难。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="五-思考技术应俯身解决真问题"&gt;五、 思考：技术应俯身解决真问题&lt;/h2&gt;
&lt;p&gt;从弱电井里满是灰尘的RS-485总线，到云端的MQTT消息；从易丢失的实体卡，到手机里的数字账户。&lt;/p&gt;
&lt;p&gt;“黑盒”的价值，不在于它用了多炫的技术，而在于它用极低的成本，为一个真实、广泛但被忽视的需求，提供了一个可行的解决方案。&lt;/p&gt;
&lt;p&gt;它像一根“数字化的梯子”，让那些无力承担“电梯”费用的用户，也能攀上智能管理的台阶。&lt;/p&gt;
&lt;p&gt;这背后，是对现场通信“脾气”（干扰、雷击、协议冲突）的深刻理解，更是对用户“简单、可靠、别添乱”这一终极诉求的敬畏。&lt;/p&gt;
&lt;p&gt;当巨头们仰望星空，构建未来时，总需要一些人，愿意为角落里那些“老旧笨重”的设备，插上一双通往现代的翅膀。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这条路充满挑战，但正因如此，每一步才都踏在真实的需求之上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="后记与邀请"&gt;后记与邀请&lt;/h2&gt;
&lt;p&gt;如果你正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电&lt;a href="https://afdian.com/a/modujson"&gt;https://afdian.com/a/modujson&lt;/a&gt;​ 持续同步与更新。&lt;/p&gt;</description></item><item><title>从粮仓 RS485 总线到云端 JSON：一个前实施工程师的数字化反思</title><link>https://blog.91demo.top/idea-granary/</link><pubDate>Wed, 25 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/idea-granary/</guid><description>&lt;blockquote&gt;
&lt;p&gt;“以前看粮仓，那是体力活，现在看粮仓，是技术活。”——一位在粮库工作三十年的老保管员&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一那些年我在粮库插钎子"&gt;一、那些年，我在粮库“插钎子”&lt;/h2&gt;
&lt;p&gt;“我年轻时，最怕夏天查粮仓。”老保管员回忆道。三伏天，他得扛着十几斤重、6米长的铁杆，踩着晃晃悠悠的木板，深一脚浅一脚地爬上几米高的粮堆。杆子前端是温度传感器，杆子末端则是一个巴掌大的小型LED显示屏。每到一个检测点，他就得用力把这“铁签子”插进压得结实的粮堆底部，然后低头查看杆子末端的屏幕读数，再抄录到本子上。全部查完、记录完，要爬一整天。粉尘呛人、闷热缺氧，是家常便饭。&lt;/p&gt;
&lt;p&gt;“那时靠的是腿，是眼，是经验。”老保管员说得对。他练就了“一看、二闻、三摸”的本事，但这套经验在几千吨的粮堆面前，总有盲区。一旦某个角落的粮食开始发热霉变，等人工发现时，往往损失已不可避免。&lt;/p&gt;
&lt;p&gt;这不是故事，这是我的朋友——一位粮库测温系统的实施工程师——告诉我的：&lt;/p&gt;
&lt;p&gt;他入行时，老保管员的手艺活已是过去式。他面对的是更“现代”的麻烦：部署一套有线测温系统。&lt;/p&gt;
&lt;p&gt;这套系统的核心，是把带有固定传感器的“测温电缆”像神经一样埋入粮堆，取代人工巡检。而他的工作，就是把这套“神经”铺进去，再把“信号”引出来。&lt;/p&gt;
&lt;p&gt;这活儿，一点不比爬粮堆轻松。&lt;/p&gt;
&lt;p&gt;夏天，粮仓内像个蒸笼，地面温度超过40度。他要在入粮前，或趁着粮堆还不高时，拖着几十米长、拇指粗的测温电缆在仓内布线。电缆里包裹着钢丝，死沉。他得把它沿着预定路径铺好、固定，确保每个传感器都在设计位置。&lt;/p&gt;
&lt;p&gt;这仅仅是开始。真正的考验是“拉线”。&lt;/p&gt;
&lt;p&gt;粮堆深处的测温电缆，其线头最终要汇集到挂在仓库墙壁或柱子上的“测温分机”（采集器）。他需要扛着梯子，在满是灰尘的仓房里，将几十根从粮堆里引出的线，一根根对号入座，拧到分机箱内的接线端子上。接错一根，整个回路的数据都可能乱掉。&lt;/p&gt;
&lt;p&gt;这还没完。仓库里几十台这样的分机，还需要用更粗的“通信总线”（RS-485线）手拉手串联起来，最后拉到机房，接入一台专用电脑。&lt;/p&gt;
&lt;p&gt;这，才是他口中“从杆子末端拉出数据线，连接到远处的分线器上”的真实图景：​ 那不是在粮堆上现插现拉的潇洒，而是在高温、高粉尘环境下，重复千百次的枯燥、负重且必须精准的体力与细心活。线要拉得横平竖直，接头要拧得牢靠，还要防鼠咬、防磨损。&lt;/p&gt;
&lt;p&gt;刚开始，他觉得这工作充满了技术仪式感。干久了，日复一日地扛电缆、爬高、接线、测通断，只剩下一个字的感受：累。一种被庞杂的物理线缆和复杂现场环境反复折磨的、沉甸甸的疲惫。&lt;/p&gt;
&lt;p&gt;然而，正是这种疲惫，让他对“可靠”二字有了刻骨的理解。他见识过雷雨天后烧毁一片的电路板，处理过因一个接头松动导致整仓数据瘫痪的故障，也深知那台机房里孤零零的电脑，就是整个系统最脆弱的“大脑”。&lt;/p&gt;
&lt;h2 id="二技术跃迁从有线到无线"&gt;二、技术跃迁：从“有线”到“无线”&lt;/h2&gt;
&lt;p&gt;我朋友后来参与实施的，正是那套用来取代人工的有线测温系统。它的核心是用预埋测温电缆取代了人工铁钎——技术人员在粮堆中预埋专用电缆，每根电缆上集成数十个温度传感器，像“神经末梢”一样分布。数据通过RS-485总线手拉手串联，即设备像糖葫芦一样串在一条总线上，一个接一个。最终汇总到仓外的监控主机。&lt;/p&gt;
&lt;p&gt;除了传统的平房仓，现代化的粮库越来越多地采用立筒仓（圆仓）。这种仓体高大，粮食像瀑布一样从顶部灌入，依靠底部的锥形漏斗和出粮机实现自动化出粮。&lt;/p&gt;
&lt;p&gt;在这里，测温电缆不再是“蛇形”铺在底部，而是像垂直的琴弦一样，从仓顶的采集器一直垂到仓底。采集器不再藏在灰尘满布的机房，而是直接安装在仓顶的防雨箱里，通过无线模块将数据发送到云端。&lt;/p&gt;
&lt;p&gt;这种结构，让粮仓从“体力活”彻底变成了“自动化工厂”。&lt;/p&gt;
&lt;p&gt;再回到我朋友的测温系统，这解决了人工爬仓的问题，但带来了新的、更复杂的麻烦。&lt;/p&gt;
&lt;p&gt;当时觉得这套系统很先进，但现在回想，它太“重”了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;怕雷击：几百米纵横交错的485总线，成了感应雷的“引雷针”，雷雨季后，分线器经常烧坏一片。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;维护难：专用的监控电脑不能死机，SQL数据库一旦损坏，几年的历史温度数据可能全丢。&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;p&gt;真正的变革，发生在物联网和无线通信普及之后。&lt;/p&gt;
&lt;p&gt;新一代的“黑盒”采集器出现了。它只有巴掌大小，却集成了主控、4G模块和无线传输功能。它直接读取测温电缆的数据，通过MQTT协议将数据打包成JSON格式，一跳直达云平台。&lt;/p&gt;
&lt;p&gt;粮库管理员在办公室，甚至用手机，就能看到整个粮堆的立体温度云图。系统自动分析、自动报警。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;免布线：每个仓门口挂一个4G黑盒，直接对接原有的分线器。省掉了跨仓位、跨建筑的数千米复杂布线工程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;云端大脑：不再需要机房那台脆弱的专用电脑，数据直接上云，永不丢失，随时随地可查。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三黑盒的定位避开红海寻找蓝海"&gt;三、黑盒的定位：避开红海，寻找蓝海&lt;/h2&gt;
&lt;p&gt;在深入了解行业后，我发现粮储智能化其实是一个典型的“双轨制市场”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;大公司的“铁桶阵”：资质与工程的护城河&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;国家级粮储库、大型央企的智能化改造，是标准的“重工程”市场。它被几家头部企业垄断，依靠的是强大的工程化资质（如机电安装、系统集成）和专用设备（如光纤测温、工业级5G网关）。这些方案性能极高，但成本也极其昂贵（单仓改造费用动辄数十万），且施工周期长，需要专业工程队进场。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;我的“黑盒”逻辑：低成本与快速响应&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我开发的这个黑盒项目（基于ESP32等开源硬件），从一开始就没打算去冲击那个“铁桶阵”。我清醒地认识到，在需要军工级可靠性的战略储备库，我的方案无法与那些天价的专用设备竞争。&lt;/p&gt;
&lt;p&gt;黑盒的核心价值在于“降维打击”与“普惠”：&lt;/p&gt;
&lt;p&gt;目标市场：中小型粮库、地方储备点、合作社、个体储粮户。这些场景对成本极度敏感，无法承担大型工程的费用，也无需那么严苛的可靠性。&lt;/p&gt;
&lt;p&gt;核心优势：极致低成本（硬件成本可能只有大公司方案的百分之一）和极速部署（即插即用，一个人半小时就能装好一个仓）。&lt;/p&gt;
&lt;p&gt;价值主张：用“够用”的可靠性，解决“有没有”的问题。对于绝大多数只需要基本温湿度监控、异常报警功能的用户，黑盒是让他们迈入数字化门槛的唯一可行选择。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;结语：让技术回归解决问题的本质&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;技术不应只是大公司的专利和豪华包装下的奢侈品。我的黑盒项目，源于“插钎子”的汗水，目标是证明一件事：在那些大公司不愿弯腰、传统方案贵得用不起的“边缘地带”，廉价的芯片和开源的智慧，同样能默默守护好每一粒粮食。&lt;/p&gt;
&lt;p&gt;这不仅是技术实现，更是一种让技术回归普惠、解决真问题的尝试。&lt;/p&gt;
&lt;h2 id="四我的黑盒从痛点里长出来的解决方案"&gt;四、我的“黑盒”：从痛点里长出来的解决方案&lt;/h2&gt;
&lt;p&gt;“虽然已离开粮储行业多年，但那些亲自插下的钎子，让我深刻理解了RS485总线的‘脾气’、雷击的刁钻、还有现场对‘简单可靠’的极致渴望。”——这种浸入式的场景理解，是我开发这款“黑盒”固件最硬的底气。&lt;/p&gt;
&lt;p&gt;它不是一个实验室里的玩具，而是为了解决 “老旧工业设备低成本上云”​ 这个具体痛点而生的 “数字化插件”​ 。它目前是一个稳定的Modbus/RS485转MQTT网关固件，未来可以扩展为支持多种协议、具备边缘计算能力的核心。&lt;/p&gt;
&lt;p&gt;未来黑盒可以集成LoRa，LoRa 最大的优势在于极强的穿透能力和超长的传输距离，非常适合“密闭、大跨度”的粮仓环境。在粮仓这种密闭、充满金属设备和粮堆的环境下，LoRa 依然表现出色。相比于 4G/NB-IoT 在密闭环境信号较弱的特点，LoRa 的信号能够穿透厚厚的墙体，深入到粮堆内部。配合无线测温探杆，使用电池以及太阳能电池板，彻底免布线。&lt;/p&gt;
&lt;h2 id="关于合作"&gt;关于合作：&lt;/h2&gt;
&lt;p&gt;我个人专注于核心固件的研发、算法优化与开源社区建设。硬件生产与制造，我与专业的工业设计伙伴合作，以确保设备的出厂品质。我相信，专业的人做专业的事，才能把产品做到最好。&lt;/p&gt;
&lt;h2 id="最终结语"&gt;最终结语&lt;/h2&gt;
&lt;p&gt;从“汗滴禾下土”的插钎巡检，到“数据云中走”的智能值守，变的不仅仅是工具，更是守护粮食安全的思维与模式。&lt;/p&gt;
&lt;p&gt;我朋友的故事，可能只是大时代里一个很小的注脚。但如果这个源于亲身痛苦、成于开源技术的“黑盒”项目，能帮助到某个正在为粮仓温控发愁的中小业主，或者给某个在工业物联网领域摸索的开发者一点启发，那么，那些年在粮堆上流过的汗，就算有了超越它本身的价值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;技术，理应如此踏实而有温度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="后记与邀请"&gt;后记与邀请&lt;/h2&gt;
&lt;p&gt;如果你也有过类似的工业现场“血泪史”，或正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电&lt;a href="https://afdian.com/a/modujson"&gt;https://afdian.com/a/modujson&lt;/a&gt;​ 持续同步与更新。&lt;/p&gt;</description></item><item><title>Modbus转MQTT网关固件研发与共创计划</title><link>https://blog.91demo.top/modujson-gateway/</link><pubDate>Tue, 24 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/modujson-gateway/</guid><description>&lt;h2 id="一-项目愿景让设备上云像接线一样简单"&gt;一、 项目愿景：让设备上云，像接线一样简单&lt;/h2&gt;
&lt;p&gt;新的一年，新的计划，我们致力于为工程师、创客及小团队，打造一套开箱即用、稳定可靠的工业物联网数据采集方案。你无需深究复杂的协议栈，只需聚焦业务，即可让传统设备轻松上云。&lt;/p&gt;
&lt;p&gt;本项目的核心价值在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协议翻译官：打造一个高性能的“中间件”，将底层的Modbus协议（支持RS485/TTL）无缝转换为上层的MQTT协议，打通设备与云端的“最后一公里”。&lt;/li&gt;
&lt;li&gt;软硬解耦：不绑定特定硬件。无论是WiFi环境（ESP32）还是4G环境（Cat.1模组），只要具备基本通信能力，软件就能赋予其“智能”，提供极大的硬件选型自由度。&lt;/li&gt;
&lt;li&gt;解决技术断层：解决有业务需求但没有研发实力的小团队（如硬件工程师、创客、小厂），本项目提供了一种低成本解决方案。它只需通用开发板，就能实现数据采集和上云，花小钱办大事。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二-技术架构三位一体的全栈实现"&gt;二、 技术架构：三位一体的全栈实现&lt;/h2&gt;
&lt;p&gt;为确保方案的极致稳定与高度可扩展，我们采用“固件 + 配置工具 + 演示端”的三位一体架构，确保方案的完整性与专业性。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;核心固件（“翻译官” - 逻辑版）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;固件架构：采用分层架构设计，硬件抽象层隔离具体芯片与通信接口（如RS485/TTL），实现核心逻辑与硬件的解耦；协议栈层实现完整的Modbus协议栈（主/从站，RTU/TCP），并进行高效、健壮的JSON封装；服务层内置MQTT客户端，负责可靠的数据上传、断线重连与本地缓存；配置管理层，通过Rust编写的PC工具下发设备配置，实现软件定义功能。&lt;/p&gt;
&lt;p&gt;技术栈：采用C语言开发，优先实现裸机（No OS）逻辑版本，确保在资源受限的MCU上也能稳定运行，降低用户使用门槛。&lt;/p&gt;
&lt;p&gt;功能：负责实时采集Modbus寄存器数据（如电压、电流），并根据配置的倍率进行数据转换，最后通过MQTT协议将数据打包上传。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;PC配置工具（“指挥官” - Rust版）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;技术栈：使用Rust语言开发，利用其内存安全特性，确保配置过程绝对可靠，工具长期运行不崩溃。&lt;/p&gt;
&lt;p&gt;功能：提供图形化界面，用户可轻松配置通信模式、寄存器地址、倍率等参数。支持通过串口（USB转TTL） 进行固件刷新与参数下发，解决现场实施难题，无需复杂的网络配置。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;数据演示端（“仪表盘”）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;技术栈：微信小程序。&lt;/p&gt;
&lt;p&gt;功能：扫码即可查看实时数据曲线与数值面板，直观验证数据上云效果，方便现场调试与演示。&lt;/p&gt;
&lt;h2 id="三-当前状态诚邀你成为共创者"&gt;三、 当前状态：诚邀你，成为“共创者”&lt;/h2&gt;
&lt;p&gt;项目正处于核心研发阶段，当前全力攻克裸机版本，实现通信方式支持WiFi和4G的Modbus RTU（串口）协议的稳定性与性能。它适合简单应用，保证运行稳定。我们相信，最好的产品源于真实场景的千锤百炼。&lt;/p&gt;
&lt;p&gt;未来会研发通信方式支持LoRa、以太网的Modbus TCP，以及FreeRTOS多任务版，用于解决需要同时处理多路数据采集，复杂业务逻辑的高端场景。&lt;/p&gt;
&lt;p&gt;因此，我们发起此次共创计划：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的支持，将直接转化为测试硬件（ESP32、各类Modbus传感器），用于极限环境下的兼容性验证。&lt;/li&gt;
&lt;li&gt;你的设备，可以寄来成为我们的“适配样本”，共同完善设备库。&lt;/li&gt;
&lt;li&gt;你的反馈，将直接塑造产品的下一个版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="四未来承诺"&gt;四、未来承诺&lt;/h2&gt;
&lt;p&gt;当固件通过大量真实场景验证，达到工业级稳定标准后，我们将正式申请软著，并启动授权售卖。所有共创阶段的支持者，都将依据历史贡献，获得丰厚的升级折扣与永久优先支持权。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我入驻了爱发电，更多详情内容请查看：&lt;a href="https://afdian.com/a/modujson"&gt;https://afdian.com/a/modujson&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>基于 Go + 小程序构建“口袋指令中心”，实现远程发布控制</title><link>https://blog.91demo.top/bag-script/</link><pubDate>Wed, 04 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/bag-script/</guid><description>&lt;p&gt;我日常涉及 Hugo 博客发布、客户端打包、Nginx 运维等多种重复性脚本。每次都要 SSH 连服务器并执行命令，操作链过长，也不方便，特别是在身边没有电脑的情况。&lt;/p&gt;
&lt;p&gt;曾经想过使用HTML 远程调用，又担心安全，使用了微信公众号发消息，也特适合这个场景，但是因为微信更新，私信入口现在非常难进入。所以就想能否通过自己的小程序实现远程控制和鉴权。构建一个通用的执行引擎，通过小程序远程触发，且具备零前端修改（免于小程序上线审核）的扩展能力。&lt;/p&gt;
&lt;h2 id="系统架构设计"&gt;系统架构设计&lt;/h2&gt;
&lt;p&gt;为了实现“明天增加脚本，小程序不发版”的目标，采用了“配置驱动” 模式。即“配置在云端，指令在指尖”。通过将业务逻辑（脚本路径与名称）完全从前端小程序中解耦，实现一套代码支持无限扩展的运维能力。&lt;/p&gt;
&lt;h3 id="核心流程"&gt;核心流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;后端 (Go)：维护一个脚本配置列表（数据库或配置文件）。&lt;/li&gt;
&lt;li&gt;前端 (小程序)：启动时请求后端接口，拉取可用脚本列表。&lt;/li&gt;
&lt;li&gt;触发：使用时选择脚本名称，点击执行。&lt;/li&gt;
&lt;li&gt;鉴权：后端校验小程序 OpenID，仅允许本人指令生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="技术实现"&gt;技术实现&lt;/h2&gt;
&lt;h3 id="系统分为三层确保安全性与扩展性的统一"&gt;系统分为三层，确保安全性与扩展性的统一：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;配置层 (Go Config)：在服务器端定义脚本的 ID、名称和实际路径。&lt;/li&gt;
&lt;li&gt;鉴权层 (WeChat Auth)：利用微信小程序 OpenID 建立强一致性的身份白名单。&lt;/li&gt;
&lt;li&gt;展示层 (Mini Program UI)：动态拉取后端配置，仅负责“展示列表”与“触发指令”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="技术实现方案"&gt;技术实现方案&lt;/h3&gt;
&lt;p&gt;A. 后端：动态脚本引擎 (Go)&lt;/p&gt;
&lt;p&gt;后端不再硬编码脚本路径，而是定义一个结构体：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 脚本任务定义&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ScriptTask&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;id&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 前端传递的任务标识&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;name&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 小程序界面显示的文字&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;-&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 实际执行的脚本路径 (对前端保密)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 示例配置（可存放在 JSON 文件或数据库中）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tasks&lt;/span&gt; = []&lt;span style="color:#a6e22e"&gt;ScriptTask&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;hugo-post&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;发布 Hugo 文章&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/scripts/deploy_hugo.sh&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;build-client&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;构建客户端&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/scripts/build_mole_go.sh&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;nginx-restart&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;重启 Nginx 服务&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;systemctl restart nginx&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对外接口定义：&lt;/p&gt;</description></item><item><title>记一次 Windows DLL 动态链接库并供第三方多语言调用</title><link>https://blog.91demo.top/go-dll/</link><pubDate>Thu, 29 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go-dll/</guid><description>&lt;p&gt;我使用go写了一个http转WebSocket服务，这是一个命令行程序，双击可以直接运行，今天，有个新的需求，需要将这个命令行程序转为dll，然后供第三方使用。&lt;/p&gt;
&lt;h2 id="1改造程序"&gt;1，改造程序&lt;/h2&gt;
&lt;p&gt;这个命令行程序很小，核心逻辑就是启动了一个HTTP服务，然后接收连接，然后通过WebSocket将内容转发出去。所以，在调整为dll时，仅仅需要导出两个函数即可。&lt;/p&gt;
&lt;p&gt;1，StartServer，启动http服务，监听端口地址。因为不能阻塞，所以需要在监听服务时使用go方法启动一个携程。&lt;br&gt;
2，StopServer，关闭http服务，需要提供给第三方，当它关闭时，需要调用它，释放监听地址等资源。&lt;/p&gt;
&lt;p&gt;下面开始改造代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;package main
import &amp;#34;C&amp;#34; // 必须导入 C 包
import (
// ... 原有导入 ...
)
// 保持原有的全局变量和函数逻辑 (例如transDataGet, connWs 等)
// 需要注意下面的//export这中间不能有空格，否则无法导出头文件
//export StartServer
func StartServer() {
// 将原本 main 函数里的逻辑放在这里
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// ... 注册路由 ...
r.Run(&amp;#34;127.0.0.1:9988&amp;#34;)
}
// 必须保留一个空的 main 函数
func main() {}
// 其它参考StartServer，如果需要导出，一定要添加//export
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="2编译命令"&gt;2，编译命令&lt;/h2&gt;
&lt;p&gt;我是在windows环境，需要安装cgo环境，我安装了Mingw-w64支持C编译环境，执行以下命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;go build -buildmode=c-shared -o trans.dll main.go
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行后会生成trans.h和trans.dll两个文件。&lt;/p&gt;
&lt;h2 id="3验证dll"&gt;3，验证dll&lt;/h2&gt;
&lt;p&gt;除了一些可以查看dll的工具外，还可以使用第三方语言进行测试。例如我这里使用Python。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import ctypes
import time
lib = ctypes.CDLL(&amp;#34;./trans.dll&amp;#34;)
lib.StartServer()
print(&amp;#34;服务已在后台启动...&amp;#34;)
time.sleep(10) # 模拟第三方程序运行
lib.StopServer()
print(&amp;#34;服务已关闭&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="4再次优化代码"&gt;4，再次优化代码&lt;/h2&gt;
&lt;p&gt;当实际测试的时候，我发现它会阻塞调用者，为了解决这个问题，我们需要再次改造程序。将启动服务改为非阻塞模式。将Gin的启动逻辑放入协程，并利用http.Server提供的Shutdown方法来实现优雅退出。&lt;/p&gt;</description></item><item><title>告别域名过期焦虑：基于 Go + Wails 3 开发“豆子域名管家”，实现批量监测与企微钉钉预警</title><link>https://blog.91demo.top/ssl-checker/</link><pubDate>Wed, 28 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/ssl-checker/</guid><description>&lt;p&gt;我以前曾介绍过，我在微信小程序实现了域名证书监控功能。但是运行一段时间后，发现用的人极少，我在想是否是因为域名以及Webhook隐私的问题。还有就是域名数量太少？&lt;/p&gt;
&lt;p&gt;我就想开发一个客户端工具，前端时间使用wails3开发了内网穿透客户端，正好技术可以复用。经过数月的规划和开发测试，豆子域名管家终于可以使用了。这是一款完全本地运行、支持批量导入管理以及可以企微和钉钉通知的域名证书检测工具。旨在解决域名过期监控难题。&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;/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;钉钉+企微双通道，支持Markdown格式&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;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;p&gt;技术架构亮点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;工具基于Wails3框架构建，采用Vue3+NaiveUI前端技术栈，确保界面简洁美观，交互流畅，支持暗黑/浅色两种主题。&lt;/li&gt;
&lt;li&gt;本地证书检测引擎，直接调用系统网络库进行TLS检测，无需依赖外部API。&lt;/li&gt;
&lt;li&gt;多线程并发处理，使用go的特性批量进行域名检测。&lt;/li&gt;
&lt;li&gt;跨平台兼容：支持Windows、macOS、Linux系统。&lt;/li&gt;
&lt;li&gt;配置持久化，所有配置本地存储，重启后自动恢复。&lt;/li&gt;
&lt;li&gt;支持域名证书检测，域名到期时间检测。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;软件运行界面预览：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;控制面板&lt;br&gt;
&lt;img alt="控制面板" loading="lazy" src="https://blog.91demo.top/images/notes/b01panel.webp"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置页面&lt;br&gt;
&lt;img alt="配置页面" loading="lazy" src="https://blog.91demo.top/images/notes/b01config.webp"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;基本操作指南：&lt;br&gt;
1，导入域名我们需要把要监控的域名按行录入到txt文档中，可以在软件配置界面下载示例模板，当准备完成后，选择刚才的文件，然后验证。没有问题后，点击确认导入即可。&lt;br&gt;
2，配置机器人目前工具支持钉钉机器人和企业微信机器人，支持Markdown格式消息，可以查点击推送预览效果按钮查看推送效果。当输入机器人配置后，可以验证测试，当收到消息后说明配置成功，点击保存即可。可以同时配置企微和钉钉。这样会同时推送两份通知。&lt;br&gt;
3，配置推送通知策略目前工具扫描调度间隔固定24小时，可以配置通知时间，和告警天数间隔。在通知时间，系统会将当天扫描的结果报表推送到机器人。如果用户更新某些域名证书后，可以在监控面板进行手动刷新。想查看效果，可以把通知时间设置为当前时间加几分钟，当到达时间后，将推送报表。确认无误后，可以调整为真正的推送时间。&lt;br&gt;
4，系统托盘运行当完成域名导入和机器人以及通知策略配置后，可以关闭窗口，工具将自动缩放到系统托盘。如果需要退出，需要右键系统托盘退出。注意，如果退出工具，将不能监控域名，因为这是一个本地工具。所以需要工具长期运行。&lt;br&gt;
5，监控面板监控面板的仪表板显示你的域名配置项统计信息，表格显示监视的域名列表。域名可以搜索，刷新以及删除。域名按照过期、告警、正常顺序排序。请查看最前面域名并及时处理。&lt;/p&gt;
&lt;p&gt;工具按照自己的真实需求开发，在发布后，比小程序版本要热闹一些，很多人下载尝试。有的用户尝试之后还给了反馈。&lt;/p&gt;
&lt;p&gt;如果你需要尝试，可以通过下方下载链接。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://91demo.top/b011"&gt;https://91demo.top/b011&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果您有任何问题和建议，欢迎反馈和交流。&lt;/p&gt;</description></item><item><title>基于 Dufs + Mole-go (FRP) 快速搭建高效的内网穿透开发演示环境</title><link>https://blog.91demo.top/mole-devenv/</link><pubDate>Thu, 15 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/mole-devenv/</guid><description>&lt;p&gt;最近开发完 Mole-go，想给它做个网站用来展示和下载。但我这个后端糙汉子，样式真搞不定，求助 AI 调了半天还是差点意思。最头疼的是，手机端调试得一遍遍输 IP，给朋友演示也得发一串 IP 端口，太不专业了！于是我一顿折腾，搞出了这套方案……&lt;/p&gt;
&lt;p&gt;为了解决这些痛点，我摸索出了一套“黄金组合”：Dufs + Mole-go + FRP + Caddy。这套方案打通了从本地到公网域名的全链路，实现了自动 HTTPS、域名访问以及极致的访问体验。&lt;/p&gt;
&lt;h2 id="第一步构建本地内容基石dufs"&gt;第一步：构建本地内容基石（Dufs）&lt;/h2&gt;
&lt;p&gt;一切的起点是本地文件服务。我选择使用 Dufs 作为静态文件服务器。它极其轻量，支持上传、搜索、打包下载甚至 WebDAV，是我演示 Web 应用或分发安装包的首选。&lt;/p&gt;
&lt;p&gt;通过下面简单的命令，就能在本地 5000 端口启动了静态文件及Web服务。虽然此时它还被“困”在局域网内，但它为后续的展示提供了稳固的基础。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;dufs.exe --render-index
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们这个时候将HTML文件放在启动命令时所在文件夹下就可以了。&lt;/p&gt;
&lt;h2 id="第二步突破局域网束缚frp-与-mole-go"&gt;第二步：突破局域网束缚（FRP 与 Mole-go）&lt;/h2&gt;
&lt;p&gt;为了让公网流量能顺利精准触达内网，我采用了经典的 FRP 方案，但在客户端层面，我使用了自己开发的 Mole-go。它需要以下几部分配合，注意，像FRP Server部署一次就可以一直使用了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务端 (FRP Server)：部署在具备公网 IP 的云服务器上，充当流量中转站。这个服务器配置可以很低，因为网站服务在本地电脑，即使本地有数据库，也是消耗的本地主机资源，如果仅作为演示，带宽1MB就可以，这样在公网就可以访问了，非常方便远程调试。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端 (Mole-go)：这是我为 FRP 打造的桌面管理客户端。它封装了 frpc 核心，不仅提供了直观的 UI，还通过系统托盘设计彻底解决了“关闭窗口即断连”的痛点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Mole-go，我可以将本地 5000 端口通过加密隧道安全地映射到云端。它出色的资源管理和连接稳定性，确保了演示过程中即便网络波动，链接依然稳固如初。它的下载地址：&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;，中文名称是豆子内网管家。&lt;/p&gt;
&lt;h2 id="第三步优雅的网关入口caddy"&gt;第三步：优雅的网关入口（Caddy）&lt;/h2&gt;
&lt;p&gt;即便流量已到达公网，我也不希望朋友们通过 http://IP:端口 这种生硬的方式访问。我追求的是“域名+HTTPS”的专业感，这不仅是为了美观，更是为了开发环境需求，比如公众号开发，小程序后端服务等必须 HTTPS 环境。仅需配置一次就可以一直使用了。&lt;/p&gt;
&lt;p&gt;我选择了 Caddy 担任“守门人”。Caddy 的魅力在于其近乎零配置的 自动 HTTPS 功能。我看中了它的简单方便，它非常符合我的场景。在 Caddyfile 中，我只需写下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;example.com {   
reverse_proxy localhost:7000  # 指向 FRP 映射出的本地端口
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;仅需这一行配置，Caddy 就会自动搞定 SSL 证书的申请与续签。当访问者输入域名时，映入眼帘的是受信任的绿色小锁头，所有的复杂端口逻辑都被完美隐藏。当然，如果你需要高性能，或者服务器已经有了Nginx，你也可以直接使用它。&lt;/p&gt;</description></item><item><title>实战笔记：实现一个语音验证码远程呼叫版本</title><link>https://blog.91demo.top/voice-call/</link><pubDate>Tue, 13 Jan 2026 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/voice-call/</guid><description>&lt;p&gt;在实现了主动使用VOIP客户端拨打&lt;code&gt;8000&lt;/code&gt;号码播报语音验证码的功能后，我发现了一个最大的缺点，就是这需要用户主动去操作。这对于想使用API集成的第三方应用来说根本无法实现。&lt;/p&gt;
&lt;p&gt;在思考之后，我决定实现一个可以调用API就呼叫VOIP客户端的版本，当呼叫用户并且用户接通后，自动播报语音验证码的功能。当实现这个功能后，它的好处是显而易见的。比如，可以集成到嵌入式，集成到第三方网站，直接调用API就可以呼叫出去。&lt;/p&gt;
&lt;p&gt;那么该如何实现它呢？&lt;/p&gt;
&lt;h2 id="一-系统原理"&gt;一、 系统原理&lt;/h2&gt;
&lt;p&gt;传统的拨号方案（Dialplan）是静态的，而 ARI 允许我们动态控制。整个“API 触发呼叫并播报”的流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发阶段：第三方系统通过 API 向 Go 服务发送呼叫请求（包含目标 ID 和验证码）。&lt;/li&gt;
&lt;li&gt;呼叫发起（Originate）：Go 服务调用 Asterisk ARI 的 /channels 接口。此时 Asterisk 会尝试向 PJSIP 终端（或通过中继向手机）发起呼叫。&lt;/li&gt;
&lt;li&gt;接通监听（Stasis Start）：一旦用户接起电话，该通道会被移交给一个名为 Stasis 的应用。此时 Go 服务会收到一个“通道已接通”的 WebSocket 事件。&lt;/li&gt;
&lt;li&gt;语音合成与播放：Go 服务识别到接通后，调用播报指令（可以播放预录音文件，或对接 TTS 引擎生成的语音流）。&lt;/li&gt;
&lt;li&gt;挂断处理：播报完毕后，服务发送挂断指令，释放资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二系统架构"&gt;二、系统架构&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[第三方API] --&amp;gt; [Go 后端服务] --(REST API)--&amp;gt; [Asterisk ARI]
| |
(WebSocket) (PJSIP/IMS)
| |
[接通状态回调] &amp;lt;--- [用户终端接听]
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="三-核心代码实现-golang"&gt;三、 核心代码实现 (Golang)&lt;/h2&gt;
&lt;p&gt;这里使用了 GitHub 上的 &lt;code&gt;go-ari&lt;/code&gt; 库。&lt;/p&gt;
&lt;h3 id="1-初始化-ari-客户端"&gt;1. 初始化 ARI 客户端&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;github.com/v5&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;github.com/v5/client/native&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 连接到 Asterisk ARI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;cl&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;native&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Connect&lt;/span&gt;(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;native&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Options&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Application&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;voice-verify&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 必须与 asterisk.conf 配置一致&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Username&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;URL&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;http://localhost:8088/ari&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-实现呼叫并播报语音验证码"&gt;2. 实现呼叫并播报语音验证码&lt;/h3&gt;
&lt;p&gt;这是核心逻辑：接收参数 -&amp;gt; 发起呼叫 -&amp;gt; 监听接通 -&amp;gt; 播放语音。&lt;/p&gt;</description></item><item><title>深度实战：基于 Wails v3 与 Go 打造跨平台 FRP 桌面客户端 Mole-go 的技术架构与原理</title><link>https://blog.91demo.top/mole-develop/</link><pubDate>Sat, 10 Jan 2026 06:00:00 +0800</pubDate><guid>https://blog.91demo.top/mole-develop/</guid><description>&lt;h2 id="一-缘起为什么需要-mole-go"&gt;一、 缘起：为什么需要 mole-go？&lt;/h2&gt;
&lt;p&gt;在开发微信公众号、调试支付接口、以及演示本地开发网站时，或由于服务器资源限制需要在本地部署服务时，frp 是不可或缺的内网穿透神器。然而，原生的 frpc 存在几个显著的痛点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行隐形性差：必须开启命令行窗口，一旦误关服务即中断。&lt;/li&gt;
&lt;li&gt;配置门槛高：新手难以记忆复杂的 .toml 参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决这些问题，我开发了 mole-go。它是一个轻量级、跨平台的桌面客户端，旨在实现 frp 的配置、启动与监控一体化。&lt;/p&gt;
&lt;p&gt;在选择使用哪个语言，哪个库开发时，我尝试了多个，分别为go fyne，rust tauri，rust iced，go wails3。&lt;/p&gt;
&lt;p&gt;放弃Go fyne是因为frp日志列表显示达不到我的要求，放弃rust tauri是因为frp二进制启动和关闭我无法实现，放弃rust iced是因为只开发了部分，界面和进度无法我无法掌控，并且此时我了解到了wails v3版本。我最终选择 Wails v3 则是看中了其原生渲染、系统托盘支持、Go 强力后端以及极小的打包体积。最主要的原因是我熟悉Go，可以用它实现功能。&lt;/p&gt;
&lt;h2 id="二-核心架构go--wails-v3-的化学反应"&gt;二、 核心架构：Go + Wails v3 的化学反应&lt;/h2&gt;
&lt;p&gt;mole-go 采用了经典的“UI-Backend-Service”三层架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wails UI：负责前端展示，通过事件驱动（Event-Driven）与后端交互。&lt;/li&gt;
&lt;li&gt;Go Backend：核心大脑，负责业务逻辑、进程管理与系统级 API 调用。&lt;/li&gt;
&lt;li&gt;frpc 二进制：底层服务，通过 Go 的 embed 特性内嵌到二进制文件中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三-关键实现细节从命令行到图形化的进化"&gt;三、 关键实现细节：从命令行到图形化的进化&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;前端：从“面条代码”到模块化数据驱动&lt;br&gt;
早期版本中，我直接采用 window.startFrp，window.stopFrp这样的写法，导致代码碎片化严重，以及管理app运行状态不方便。在 mole-go 的正式版中，我将其重构为数据驱动模式，类似Vue，由数据驱动界面：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;模块化封装：定义全局 window.App 对象，将数据状态与行为（Methods）统一封装，使代码结构清晰。&lt;/li&gt;
&lt;li&gt;动态 UI 组件：针对 HTTP、TCP、UDP 等不同代理模型，不再机械地堆砌 HTML 片段，而是通过逻辑判断实现“按需渲染”，大大精简了 DOM 结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="2"&gt;
&lt;li&gt;后端：全局实例与事件机制&lt;br&gt;
为了保证服务层（Service）能随时与 UI 通信，我设计了一个全局 App 实例，这样可以方便得调用和管理。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;状态约定：前后端约定一套状态码，通过 Wails v3 的 Events 机制，后端可以主动向前端推送 frpc 的运行状态、日志等信息。&lt;/li&gt;
&lt;li&gt;独立服务层：将 frp 相关逻辑抽离到专门的文件中，通过 Wails 的 Binding 暴露给前端，保持代码的解耦。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="3"&gt;
&lt;li&gt;系统深度集成&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;系统托盘（System Tray）：利用 Wails v3 原生的托盘支持，实现了“关闭即隐藏”逻辑。&lt;/li&gt;
&lt;li&gt;外部链接调用：使用 wails3自带的 Browser.OpenURL 方法，确保点击文档链接时能正确唤起系统浏览器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可参考项目源码。&lt;/p&gt;</description></item><item><title>FRP 图形化管理新方案：基于 Wails 3 的 Mole-go 桌面客户端部署指南</title><link>https://blog.91demo.top/mole-help/</link><pubDate>Wed, 07 Jan 2026 06:00:00 +0800</pubDate><guid>https://blog.91demo.top/mole-help/</guid><description>&lt;p&gt;记录自研 FRP 管理工具 Mole-go 的公版部署与使用过程，探讨如何通过 Go + Wails 3 构建极致简单的内网穿透图形化管理体验。&lt;/p&gt;
&lt;p&gt;在介绍部署高性能的内网穿透开发演示环境时，虽然也提到了部署配置，但是不太详细。这次本文将带你详细的快速完成基于 Mole 的内网穿透服务部署，包含服务端 (frps) 与桌面客户端 (Mole/frpc) 的配置与排查要点。&lt;/p&gt;
&lt;h2 id="-核心概念"&gt;📖 核心概念&lt;/h2&gt;
&lt;p&gt;首先，我们了解几个关键概念，只有了解之后我们才能进行扩展，应用到更多的场景中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;什么是 FRP？&lt;br&gt;
FRP (Fast Reverse Proxy) 是一款高性能的反向代理/内网穿透工具，它通过在公网服务器和内网客户端之间建立隧道，使外网可以访问内网服务（如 NAS、树莓派、开发环境等）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;什么是 Mole？&lt;br&gt;
Mole 是一款基于 wails3 开发的 FRP 桌面客户端，提供图形化管理界面，简化 frpc 的配置与使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;告别繁琐命令行&lt;/li&gt;
&lt;li&gt;支持系统托盘常驻，防止误关窗口导致中断&lt;/li&gt;
&lt;li&gt;支持 HTTP/HTTPS、TCP、UDP 等多种协议&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="-部署前准备"&gt;🛠️ 部署前准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;必须有一台拥有公网 IP 的低配云服务器（例如1核1G内存的&lt;a href="https://www.aliyun.com/minisite/goods?userCode=zrqh6alb"&gt;阿里云&lt;/a&gt;、&lt;a href="https://curl.qcloud.com/XX6que5w"&gt;腾讯云&lt;/a&gt;等），如果你需要提供视频等服务，那么带宽要高，服务器配置视情况进行调高。&lt;/li&gt;
&lt;li&gt;FRP 服务端（frps）建议使用 FRP Releases 的 v0.65.0 或更高版本，可以从&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;下载我已经打包好各平台的服务端。&lt;/li&gt;
&lt;li&gt;Mole 桌面客户端安装包，可以从&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;直接下载，或者从Github下载源码&lt;a href="https://github.com/littletow/mole-go/releases"&gt;littletow/mole-go&lt;/a&gt;编译。&lt;/li&gt;
&lt;li&gt;需要具备基本网络与防火墙管理知识。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="1-服务端配置frps"&gt;1. 服务端配置（frps）&lt;/h2&gt;
&lt;p&gt;在你拥有公网IP的云服务器上部署frps服务端：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;下载并解压 FRP 服务端（以 Linux 为例）&lt;br&gt;
访问 FRP 的 Releases 页面下载对应版本并解压（示例为 v0.65.0 或更高）。&lt;/p&gt;</description></item><item><title>Wails 3 初体验及在FRP管理客户端中的应用</title><link>https://blog.91demo.top/wails3-demo/</link><pubDate>Sat, 01 Nov 2025 07:34:14 +0000</pubDate><guid>https://blog.91demo.top/wails3-demo/</guid><description>&lt;h2 id="为什么选择了wails-3-"&gt;为什么选择了Wails 3 ？&lt;/h2&gt;
&lt;p&gt;Wails 3 最大的改变在于它不再强绑定于某个特定的前端框架，且引入了&lt;strong&gt;多窗口支持&lt;/strong&gt;和&lt;strong&gt;更轻量级的 Runtime&lt;/strong&gt;。它允许你在不启动主窗口的情况下运行后端服务，这正是我们实现“系统托盘”和“后台演示服务”的基础。&lt;/p&gt;
&lt;p&gt;在 v2 中，我们习惯于自动生成的 &lt;code&gt;wailsjs&lt;/code&gt; 文件夹。但在 v3 中，这一逻辑被进一步标准化。&lt;/p&gt;
&lt;p&gt;当你运行开发指令时，Wails 会扫描你的 Go 结构体方法，并将其映射为前端可以调用的 JavaScript 函数。这个过程在 v3 中被称为 &lt;strong&gt;Generate&lt;/strong&gt; 过程。&lt;/p&gt;
&lt;h3 id="wails-3-通信灵魂"&gt;Wails 3 通信灵魂&lt;/h3&gt;
&lt;p&gt;我们在前端调用在后端定义的 HandleConnect 返回自定义结构体，这在 Wails 3 中是前后端通信的灵魂。&lt;/p&gt;
&lt;p&gt;在 Wails 3 开发中，最核心的动作就是：&lt;strong&gt;后端做功，前端表现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当你调用 &lt;code&gt;MoleService.HandleConnect()&lt;/code&gt; 时，Go 后端会产生一个结果。在本项目中，我们需要同时返回一个 &lt;code&gt;Code&lt;/code&gt;（状态码）和一个 &lt;code&gt;Content&lt;/code&gt;（数据内容）。&lt;/p&gt;
&lt;p&gt;为了实现这一点，我们定义了一个结构体：&lt;br&gt;
&lt;code&gt;type Response struct { Code int; Content string }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;虽然 Go 内部使用的是结构体，但前端 JavaScript 只能读懂 JSON 对象。Wails 3 内部会自动帮你完成这个“翻译”过程。&lt;/p&gt;
&lt;p&gt;但是，如果你想让前端看到的字段名是小写的（例如 &lt;code&gt;res.code&lt;/code&gt; 而不是 &lt;code&gt;res.Code&lt;/code&gt;），你必须在 Go 结构体定义时加上“注解”。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Response&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Code&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;code&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Content&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;content&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;记住，所有通过 bindings 调用的 Go 方法，在前端返回的都是一个 Promise 对象。这意味着你必须使用 await 或者 .then() 来接收数据，否则你拿到的将是一个永不开启的“盲盒”。&lt;/p&gt;</description></item><item><title>Markdown 进化论：从小程序文章列表到游戏关卡到时间轴作品集再到项目模块集市</title><link>https://blog.91demo.top/v-article-extend/</link><pubDate>Wed, 03 Sep 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-article-extend/</guid><description>&lt;p&gt;Markdown越来越玩的纯熟，在小程序架构稳定之后，剩下的就是Markdown内容的编辑，我已经不满足于基础的标题和段落，我还会根据适当的场景使用。我必须把常用的Markdown介绍一下，感觉其设计真是奇妙，比如在一行中用三个以上的星号、减号、底线来建立一个分割线，行内不能有其它东西。也可以在星号或减号中间插入空格。这样就是一条分割线。当你完全掌握之后，就会发现我们还可以进行扩展，比如我后面的跳转公众号文章，跳转小程序。&lt;/p&gt;
&lt;p&gt;我们先来快速浏览一下除了标题和段落之外不常用但重要的Markdown语法。不感兴趣可以跳过。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;*斜体文本*
_斜体文本_
**粗体文本**
__粗体文本__
***粗斜体文本***
___粗斜体文本___
# 分割线
***
* * *
*****
- - -
---------
~~删除线~~
&amp;lt;u&amp;gt;下划线文本&amp;lt;/u&amp;gt;
[^脚注，要注明的文本]
# 无序列表
* 第一项
+ 第二项
- 第三项
# 有序列表
1. 第一项
2. 第二项
3. 第三项
# 列表嵌套需要在子列表中的选项签名添加两个或四个空格即可。
1. 第一项：
- 第一项嵌套的第一个元素
- 第一项嵌套的第二个元素
2. 第二项：
- 第二项嵌套的第一个元素
- 第二项嵌套的第二个元素
# 区块
&amp;gt; 最外层
&amp;gt; &amp;gt; 第一层嵌套
&amp;gt; &amp;gt; &amp;gt; 第二层嵌套
# 列表中使用区块，如果列表中要使用区块，请在&amp;gt;前添加四个空格即可。
* 第一项
&amp;gt; 菜鸟教程
&amp;gt; 学的不仅是技术更是梦想
* 第二项
# 链接
[链接名称](链接地址)
&amp;lt;链接地址&amp;gt;
# 图片显示
![alt 属性文本](图片地址)
![alt 属性文本](图片地址 &amp;#34;可选标题&amp;#34;)
![alt 属性文本](图片地址居中#pic_center)
![alt 属性文本](图片地址宽和高 =400x200)
# 只设置宽，会按比例进行缩放
![alt 属性文本](图片地址 =400x)
# 使用 html &amp;lt;img&amp;gt;标签。
&amp;lt;img src=&amp;#34;https://xxx.com/x.png&amp;#34; width=&amp;#34;50%&amp;#34;&amp;gt;
# 制作表格使用|来分割不同的单元格，可以使用-来分割表头和其它行。
| 表头 | 表头 |
| ---- | ---- |
| 单元格 | 单元格 |
| 单元格 | 单元格 |
# 设置表格的对齐方式使用如下符号：
-: 设置内容和标题栏居右对齐。
:- 设置内容和标题栏居左对齐。
:-: 设置内容和标题栏居中对齐。
# Markdown 使用了很多特殊符号来表示特定的意义，如果需要显示特定的符号则需要使用转义字符，Markdown 使用反斜杠转义特殊字符：
**文本加粗**
\*\* 正常显示星号 \*\*
使用 KaTex 或 MathJax 来渲染数学表达式。
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="mardown语法扩展"&gt;Mardown语法扩展&lt;/h3&gt;
&lt;p&gt;当然我还扩展了一些内容，比如链接，我在小程序中支持跳转到公众号文章，以及跳转到小程序。具体的实现就是通过前缀。&lt;/p&gt;</description></item><item><title>万物皆可 Markdown：小程序动态内容渲染通用架构设计</title><link>https://blog.91demo.top/v-article-list/</link><pubDate>Tue, 03 Jun 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-article-list/</guid><description>&lt;p&gt;在学习了微信小程序之后，我使用wxml页面可以做出唐诗的内容了。结合wxss，我可以定义好看的布局。在小程序需要备案时，因为唐诗需要资质，而我个人无法满足这些条件，所以我需要开发新的项目。恰好最近在为我的技术笔记发愁。我希望有一个可以展示我笔记内容的工具。&lt;/p&gt;
&lt;p&gt;我的最初想法非常简单：&lt;br&gt;
1，可以更新文章内容而不需要升级小程序。&lt;br&gt;
2，需要在小程序端进行展示，并且需要美观。&lt;br&gt;
3，能够根据标题或者关键字进行搜索文章内容。&lt;/p&gt;
&lt;p&gt;经过一番研究后，我决定小程序使用原生开发，界面 UI 使用 WeUI，内容渲染使用Markdown。&lt;/p&gt;
&lt;p&gt;小程序页面架构是列表+详情。列表作为首页，首页页面非常简单，一个标题和描述，用来解释小程序干什么？一个搜索框用来搜索内容，三个快捷按钮用于快速查找内容。 这个也是为了小程序审核，避免成为资讯类主题。&lt;/p&gt;
&lt;p&gt;在完成之后，我发现这套架构可以通用，用来渲染动态内容。像列表页面以及WEUI使用，熟悉前端的非常容易理解实现。下面是一些核心介绍，主要介绍Markdown的渲染实现。&lt;/p&gt;
&lt;p&gt;在小程序中，无法直接展示 Markdown 数据，我们需要将 Markdown 转换为 WXML，网上的大神很多，有多款 Markdown 转 WXML 库，我使用的是 &lt;code&gt;TOWXML&lt;/code&gt; 库。我使用Markdown主要是因为更新内容不用升级小程序，这是动态渲染的最大优点。当然它还有其它一些优势，比如容易编辑，支持数学公式，布局美观等。&lt;/p&gt;
&lt;p&gt;下面是Markdown具体实现，不感兴趣可以跳过。可以看技术下面的架构思考。&lt;br&gt;
1，按照 towxml 库的说明文档，我们将编译后的 towxml 复制到我们的项目中，然后在 app.js 文件中引入这个组件。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;App({
towxml: require(&amp;#39;./towxml/index&amp;#39;),
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2，按照首页的步骤，我们添加文章详情页面。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;pages/article/index&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 index.wxml 文件中，我们添加页面布局，这个很简单，显示渲染后的内容即可。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!--pages/article/index.wxml--&amp;gt;
&amp;lt;view class=&amp;#34;page&amp;#34;&amp;gt;
&amp;lt;!--使用towxml--&amp;gt;
&amp;lt;towxml nodes=&amp;#34;{{article}}&amp;#34; /&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;3，注意，我们还需要在 index.json 中添加组件引入。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
&amp;#34;usingComponents&amp;#34;: {
&amp;#34;towxml&amp;#34;: &amp;#34;../../towxml/towxml&amp;#34;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;4，在 js 文件中，我们需要调用 Markdown 数据。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;onLoad(options) {
const _ts = this;
wx.showLoading({
title: &amp;#39;加载中&amp;#39;,
})
httpGet(&amp;#39;/artd&amp;#39;, {
uuid: options.guid,
}).then((res) =&amp;gt; {
const result = res.data;
if (result.code == 1) {
let content = result.data;
let obj = app.towxml(content, &amp;#39;markdown&amp;#39;, {
theme: &amp;#39;light&amp;#39;,
events: {
tap: (e) =&amp;gt; {
console.log(&amp;#39;tap&amp;#39;, e);
}
}
});
_ts.setData({
article: obj,
});
wx.hideLoading({
success: (res) =&amp;gt; { },
})
} else {
wx.hideLoading()
}
}).catch((err) =&amp;gt; {
console.log(err);
wx.hideLoading()
wx.showToast({
title: &amp;#39;网络异常请重试&amp;#39;,
})
})
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们在 onLoad 中加载函数，这样页面启动就会自动网络请求，然后渲染 Markdown 数据。在请求数据时，我们注意到 guid，这个参数是上一个页面即首页，点击文章列表项带过来的数据。 你也可以使用其它参数，它能唯一代表从服务器拉取哪篇文章的Markdown内容。&lt;/p&gt;</description></item><item><title>构建自定义验证码登录系统及研究语音验证码实战</title><link>https://blog.91demo.top/vcode-login/</link><pubDate>Fri, 21 Mar 2025 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/vcode-login/</guid><description>&lt;p&gt;当我使用小程序码登录网站时，我发现了一个问题，在手机端不方便登录。我们都知道手机号验证码可以登录网站，但是我没有资源去实现手机号验证码功能，我使用一个变通的方案，在手机端不使用手机号验证码也能登录。&lt;/p&gt;
&lt;h3 id="小程序实现验证码登录"&gt;小程序实现验证码登录&lt;/h3&gt;
&lt;p&gt;它的核心还是鉴权，我在小程序端制作了一个获取验证码界面，它可以生成模拟编号和验证码。当用户点击获取验证码时，会向后台请求返回编号和验证码，后台这个时候会记录为哪个用户openid生成了哪个验证码。那为什么不单单使用验证码呢？还要再添加一个编号，不麻烦吗？因为单单使用验证码会发生撞车的可能。要知道，验证码一般4位或者6位，很容易被暴力攻击的。&lt;/p&gt;
&lt;p&gt;当用户在网站端输入编号和验证码时，后端会校验是否存在这对编号和验证码，如果校验正确，将取出openid并绑定到sessionID上，然后返回给前端，存入cookie中。可以看看前面的文章，扫描二维码登录，同样的道理。&lt;/p&gt;
&lt;p&gt;除了在小程序生成验证码外，我们还可以在公众号中发送消息获取验证码。它其实也是使用了微信的账号体系。&lt;/p&gt;
&lt;h3 id="公众号实现验证码登录"&gt;公众号实现验证码登录&lt;/h3&gt;
&lt;p&gt;要知道公众号不仅是内容分发平台，更是一个强大的&lt;strong&gt;身份认证中间件&lt;/strong&gt;。我们使用发送消息来获取验证码信息。&lt;/p&gt;
&lt;h4 id="1-握手与回调-webhook"&gt;1. 握手与回调 (Webhook)&lt;/h4&gt;
&lt;p&gt;当你在公众号消息发送验证码三个字时，微信会推送事件到你的服务器。我使用go对接了微信公众号消息。&lt;/p&gt;
&lt;p&gt;要在 Go 中接收微信消息，你必须先在微信公众平台配置一个 &lt;strong&gt;服务器地址 (URL)&lt;/strong&gt;。** 微信会向你的 URL 发送一个 GET 请求，包含签名、随机数等。你必须按照规定的算法计算并返回正确的 &lt;code&gt;echostr&lt;/code&gt;，这被称为“服务器验证”。 验证通过后，每当用户发送消息，微信服务器就会以 POST 方式将消息体推送给你。&lt;/p&gt;
&lt;h4 id="2-解析-xml-数据"&gt;2. 解析 XML 数据&lt;/h4&gt;
&lt;p&gt;与现代 API 不同，微信公众号的推送采用的是 &lt;strong&gt;XML&lt;/strong&gt; 格式。Go 的标准库 &lt;code&gt;encoding/xml&lt;/code&gt; 提供了强大的解析能力：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;WxMsg&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ToUserName&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`xml:&amp;#34;ToUserName&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;FromUserName&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`xml:&amp;#34;FromUserName&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 这就是用户的 OpenID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Content&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`xml:&amp;#34;Content&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 用户发来的文字&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="3-验证码生存逻辑"&gt;3. 验证码生存逻辑&lt;/h4&gt;
&lt;p&gt;当用户发送“验证码”关键字时，我们的 Go 后端会生成一个 4-6 位的随机数和一个编号。并将消息中的 OpenID 与 编号和随机数 存入 Redis，并设置 TTL（如 5 分钟）。然后通过 XML 响应将验证码发回给用户。&lt;/p&gt;
&lt;h4 id="4-身份绑定完成登录"&gt;4. 身份绑定，完成登录&lt;/h4&gt;
&lt;p&gt;我们在Web HTML页面提供了登录表单，提供编号和验证码输入框。用户输入编号和验证码后提交到后端，后端从 Redis 中根据编号和验证码反查 OpenID，若存在且有效，则代表身份验证成功，返回包含身份的TOKEN。&lt;/p&gt;</description></item><item><title>基于 Go + 小程序实现网页端“扫码登录”实战</title><link>https://blog.91demo.top/qrcode-login/</link><pubDate>Sat, 15 Mar 2025 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/qrcode-login/</guid><description>&lt;p&gt;不想使用账号和密码登录，害怕被攻击，也不想做注册等功能；不想使用手机号和验证码登录，没有钱也没有资质去做这些。我想到了二维码，通过二维码进行登录。这里有一个核心的问题还是如何鉴权？&lt;/p&gt;
&lt;p&gt;在了解小程序后，我就决定使用小程序扫描二维码登录，使用小程序自带的微信账户体系完成鉴权。&lt;/p&gt;
&lt;p&gt;我们要知道，二维码（QR Code）是连接物理世界与数字世界的“虫洞”。在登录系统中，它承载着一个临时的&lt;strong&gt;身份信标&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在实现这个Web登录功能的过程中，我们需要：&lt;/p&gt;
&lt;h3 id="1-生成有效二维码"&gt;1. 生成有效二维码&lt;/h3&gt;
&lt;p&gt;这个二维码需要显示在网页上，供用户扫码登录，登录二维码通常包含一个加密的 URL 或一个唯一的 &lt;strong&gt;UUID&lt;/strong&gt;（通用唯一识别码）。&lt;/p&gt;
&lt;p&gt;这个唯一识别码需要具备的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;唯一性：&lt;/strong&gt; 每一对扫描动作都必须对应一个独一无二的 ID。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时效性：&lt;/strong&gt; 二维码必须配合 Redis 设置过期时间（如 2 分钟），逾期自动失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在本项目中，我们将用户的微信 OpenID 作为 Key，生成的验证码作为 Value，使用Redis进行存储：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 存储验证码，并设置 5 分钟过期&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rdb&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Set&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;user:123:code&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;8888&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;time&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Minute&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;Err&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 读取验证码&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;val&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rdb&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;user:123:code&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;Result&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当有了二维码内容后，我们需要工具来生成二维码，在 Go 生态中，我们使用 &lt;code&gt;skip2/go-qrcode&lt;/code&gt; 库来完成像素的绘制：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 生成二维码字节数组&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;png&lt;/span&gt; []&lt;span style="color:#66d9ef"&gt;byte&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;png&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt; = &lt;span style="color:#a6e22e"&gt;qrcode&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Encode&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;91demo.top&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;+&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;sessionID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;qrcode&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Medium&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;256&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为了防止用户伪造扫码请求，二维码里的内容通常是加密的或者是不可预测的长随机数（UUID）。只有真实存在的 ID 才能通过后端的 Redis 校验。&lt;/p&gt;
&lt;h3 id="2-传输二维码"&gt;2. 传输二维码&lt;/h3&gt;
&lt;p&gt;当在服务端生成二维码后，还需要传递给浏览器，让浏览器进行显示，用户才可以扫码。这里有两个方法：1，生成图片文件，然后浏览器下载后显示。2，将图片内容转为Base64字符串传递给浏览器。这里我们选择了后者，我们不希望在用户硬盘上产生大量的临时 .png 文件。&lt;/p&gt;
&lt;p&gt;在Go中可以这样操作：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 转换为前端可直接识别的 Data URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;base64Img&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;data:image/png;base64,&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;base64&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;StdEncoding&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;EncodeToString&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;png&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为了让前端不至于崩溃，后端必须返回统一的格式。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;HandleLogin&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;gin&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Context&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 逻辑处理...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;gin&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;H&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;base64Img&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在前端，我们不再需要引入沉重的第三方库来做简单的请求。浏览器原生提供的 &lt;strong&gt;Fetch&lt;/strong&gt; API 简洁且基于 Promise。&lt;/p&gt;</description></item><item><title>Mosquitto 安全加固从匿名访问到多用户 ACL 细粒度权限控制</title><link>https://blog.91demo.top/mosquitto-config/</link><pubDate>Wed, 01 Jan 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/mosquitto-config/</guid><description>记录 Mosquitto 代理服务的安全配置过程，包括用户认证、动态 ACL 权限控制以及在边缘计算场景下的安全实践。</description></item><item><title>文章上传命令行客户端升级版本使用Cobra支持更多命令</title><link>https://blog.91demo.top/v-upart-cobra/</link><pubDate>Sun, 22 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-upart-cobra/</guid><description>&lt;p&gt;对于添加更多的命令，使用 flag，就有点麻烦了，这次我们使用一个更高级的库 cobra。同时，我们使用 viper 替换 ini 库，这个库可以读取多种格式的配置文件，可以读取环境变量。&lt;/p&gt;
&lt;p&gt;要实现的功能如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上传文章&lt;/li&gt;
&lt;li&gt;文章删除&lt;/li&gt;
&lt;li&gt;更新文章标题&lt;/li&gt;
&lt;li&gt;更新文章关键字&lt;/li&gt;
&lt;li&gt;更新文章内容&lt;/li&gt;
&lt;li&gt;文章公开开关&lt;/li&gt;
&lt;li&gt;文章加锁开关&lt;/li&gt;
&lt;li&gt;根据标题查找文章&lt;/li&gt;
&lt;li&gt;根据关键字查询文章&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在使用 cobra 实现上面的命令。&lt;/p&gt;
&lt;p&gt;首先，我想要的效果如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上传文章 gart upload title keyword filename ispub islock&lt;/li&gt;
&lt;li&gt;删除文章 gart remove uuid&lt;/li&gt;
&lt;li&gt;更新文章标题 gart updatetitle uuid title&lt;/li&gt;
&lt;li&gt;更新文章关键字 gart updatekeyword uuid keyword&lt;/li&gt;
&lt;li&gt;更新文章内容 gart updatecontent uuid filename&lt;/li&gt;
&lt;li&gt;更新文章公开或不公开 gart updatepub uuid ispub&lt;/li&gt;
&lt;li&gt;更新文章加锁或不加锁 gart updatelock uuid islock&lt;/li&gt;
&lt;li&gt;根据标题或关键字查找文章 gart search content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面的 upload,remove,updatetitle,updatekeyword 等，在 cobra 中都是命令。title keyword 等都是参数。&lt;/p&gt;
&lt;p&gt;我在读 cobra 文档后，最疑惑的地方就是标志和参数，这两者最大的区别就是标志需要在命令中添加&amp;ndash;flag 然后是值，而参数是直接跟在命令后，直接就是内容的。标志可以更改程序的行为。&lt;/p&gt;</description></item><item><title>使用Go自制豆子碎片文章上传命令行客户端</title><link>https://blog.91demo.top/v-upart-go/</link><pubDate>Sat, 21 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-upart-go/</guid><description>&lt;p&gt;这是 Golang 实现的上传文章以及管理文章的一个命令行工具。&lt;/p&gt;
&lt;h2 id="项目地址"&gt;项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gitee.com/littletow/upart-go"&gt;https://gitee.com/littletow/upart-go&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="实现一个上传文章的命令行工具"&gt;实现一个上传文章的命令行工具&lt;/h2&gt;
&lt;p&gt;这是一个最初的版本，使用 flag 和 ini 来实现文章上传功能。使用 flag 来解析命令行参数，使用 Ini 配置文件，记录识别码，以及 token。记录 token 的原因是因为每次启动命令行，都需要重新获取 token，为了减少 token 获取次数，在获取到 token 后，同时存储到 Ini 配置文件中。每次命令行启动，优先查看配置文件中的 token。&lt;/p&gt;
&lt;p&gt;开发这个工具主要有以下几个技术要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从命令行获取参数&lt;/li&gt;
&lt;li&gt;从配置文件中读取参数&lt;/li&gt;
&lt;li&gt;读取文件内容&lt;/li&gt;
&lt;li&gt;请求后端接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们通过分析后，需要定义以下几个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;title 标题&lt;/li&gt;
&lt;li&gt;keyword 关键字&lt;/li&gt;
&lt;li&gt;filename MD 文件名&lt;/li&gt;
&lt;li&gt;ispub 是否公开&lt;/li&gt;
&lt;li&gt;islock 是否加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Go 代码定义参数和结构体&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var (
title string
keyword string
filename string
ispub int // 1 pub
islock int // 1 lock
)
func init() {
flag.StringVar(&amp;amp;title, &amp;#34;b&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;文章题目&amp;#34;)
flag.StringVar(&amp;amp;keyword, &amp;#34;k&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;文章关键字&amp;#34;)
flag.StringVar(&amp;amp;filename, &amp;#34;f&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;MD文件&amp;#34;)
flag.IntVar(&amp;amp;islock, &amp;#34;l&amp;#34;, 0, &amp;#34;是否加锁&amp;#34;)
flag.IntVar(&amp;amp;ispub, &amp;#34;p&amp;#34;, 0, &amp;#34;是否开放&amp;#34;)
}
func main(){
flag.Parse()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;init() 在 main 函数前调用。需要在 main 函数中调用 flag.Parse()，这一步非常关键。之后就可以使用变量了。&lt;/p&gt;</description></item><item><title>记一次因直接使用unwrap导致Rust上传工具崩溃问题</title><link>https://blog.91demo.top/upart-rs/</link><pubDate>Mon, 01 Jan 2024 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/upart-rs/</guid><description>&lt;p&gt;最近，更新了上传文章工具 rust 版本，这个版本使用的库为 nwg，即 native-windows-gui 库，该库支持老的 Windows GUI。&lt;/p&gt;
&lt;p&gt;昨天，当我准备在另一台电脑上录制视频时，发现运行不起来，GUI 界面闪现一下，就退出了。以为版本太老，我先升级了 Rust 版本到最新版本（😭，不应该的原因，固定版本反而更可靠稳定一些）。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rustup update
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;升级完成后，cargo clean ,再 cargo run 重新运行。&lt;/p&gt;
&lt;p&gt;郁闷，还是报错。&lt;/p&gt;
&lt;p&gt;将 main.rs 文件中该行注释，&lt;code&gt;#![windows_subsystem = &amp;quot;windows&amp;quot;]&lt;/code&gt;，可以在命令行中查看报错信息，提示是找不到文件。我想了下，只有图片是文件，然后我将图片的路径修改，发现修改之后，编辑器还提示错误，找不到文件。重新改回去后，编辑器提示错误消失。说明不是这里的错误。&lt;/p&gt;
&lt;p&gt;我真的晕了。从网上查找问题原因，没有找到此类问题的解决方法，郁闷。在查找时，发现了另一个 Windows 官方支持的 rust 库，就叫 windows，打算有时间了用这个库重写一下。&lt;/p&gt;
&lt;p&gt;今天，我又查看了下另一台电脑上的这个项目，发现可以正常运行，将 rust 升级到最新版本后，还是可以运行。在看到配置文件后，我才恍然大悟，原来是查找的是这个配置文件。我赶紧扒拉代码确认，发现确实是这个问题。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let conf = Ini::load_from_file(&amp;#34;conf.ini&amp;#34;).unwrap();
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面是使用文件的地方，找不到文件 panic。我这里优化了一下，让错误提醒的更明显一些。这样我就瞬间能知道问题原因了。&lt;/p&gt;
&lt;p&gt;优化后的代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let conf = Ini::load_from_file(&amp;#34;conf.ini&amp;#34;).expect(&amp;#34;please config conf.ini file&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;有些问题其实很简单，但由于时间长了，还是会一头懵，如果报错信息提示完善一点，会很快定位到问题。&lt;/p&gt;</description></item><item><title>使用Rust基于 Native-Windows-GUI 构建的轻量小程序码生成工具</title><link>https://blog.91demo.top/mpcode-nwg/</link><pubDate>Thu, 02 Nov 2023 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/mpcode-nwg/</guid><description>&lt;p&gt;早于官方后台功能的桌面实践：探索使用 Rust + native-windows-gui 打造极致轻量的小程序工具。&lt;/p&gt;
&lt;p&gt;在沉浸于 Wails 3 开发 Mole 客户端的过程中，我不禁想起了我在 2023 年 11 月完成的一个小项目——&lt;strong&gt;mp-qrcode-gen&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虽然现在微信小程序后台已经集成了生成小程序码的功能，但在 2023 年，开发者想要快速、批量或者自定义参数生成小程序码，往往需要自己写脚本。于是，我用 Rust 编写了这款极致轻量的桌面工具。&lt;/p&gt;
&lt;p&gt;&lt;img alt="生成小程序码流程图" loading="lazy" src="https://blog.91demo.top/images/mpcode-rs/mpcode-gen.webp"&gt;&lt;/p&gt;
&lt;h2 id="为什么在-2023-年做这个工具"&gt;为什么在 2023 年做这个工具？&lt;/h2&gt;
&lt;p&gt;当时，自己需要根据页面去生成一些小程序码，线下张贴使用。每次使用服务端修改代码感觉非常不便利。于是做了一个这样的生成小程序码的工具。&lt;/p&gt;
&lt;p&gt;当完成后，我还发了视频号进行了推广，发现很多人还下载使用了。其实运营人员和开发者在准备线下物料（如海报）时，通常会面临一个痛点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;接口调用门槛&lt;/strong&gt;：微信官方提供的是 API 接口，非技术人员无法直接使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数复杂&lt;/strong&gt;：小程序码分为“受限”和“不受限”两种，参数限制各异，手动拼凑 URL 极易出错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量需求&lt;/strong&gt;：线下场景往往需要针对不同页面、不同场景值生成大量图片，网页端操作效率低下。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="技术选型rust--native-windows-gui"&gt;技术选型：Rust + native-windows-gui&lt;/h2&gt;
&lt;p&gt;在那个时期，我并没有选择 Electron 这种庞然大物，而是选择了&lt;strong&gt;Rust&lt;/strong&gt;，保证了极高的运行效率和内存安全。和&lt;strong&gt;native-windows-gui (NWG)&lt;/strong&gt;，这是一个非常纯粹的 Windows 原生 GUI 库。它不包含浏览器内核，直接调用 Windows API，生成的 &lt;code&gt;.exe&lt;/code&gt; 文件体积非常小。选择Rust还有一个原因就是我在学习它，需要一个项目练手。&lt;/p&gt;
&lt;h2 id="核心功能拆解"&gt;核心功能拆解&lt;/h2&gt;
&lt;p&gt;做的工具界面非常简单直观，配置完 &lt;code&gt;AppID&lt;/code&gt; 和 &lt;code&gt;AppSecret&lt;/code&gt; 后，就可以使用它生成小程序码了。点击生成按钮后，工具会直接调用微信接口并处理返回的二进制流，将其保存为本地图片文件。无需打开浏览器，所见即所得。&lt;/p&gt;
&lt;p&gt;根据微信官方的接口，我实现了两种模式，分别为受限模式和不受限模式。&lt;/p&gt;
&lt;h3 id="1-数量受限模式-api-ab"&gt;1. 数量受限模式 (API A/B)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：支持输入完整的、带有超长 URL 参数的页面路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：适用于对码量要求不高（总计 10 万个以内），但需要精准携带复杂参数的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-数量不受限模式-api-c"&gt;2. 数量不受限模式 (API C)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：页面路径较短，但支持独立的 &lt;code&gt;Scene&lt;/code&gt;（场景值）字段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：这是线下物料最常用的模式，可以无限次生成，通过场景值来区分不同的线下投放点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="开源和思考"&gt;开源和思考&lt;/h2&gt;
&lt;p&gt;虽然几个月后，微信官方后台也逐步完善了类似的功能，但 &lt;strong&gt;mp-qrcode-gen&lt;/strong&gt; 的意义在于：&lt;strong&gt;在需求未被完全满足的真空期，个人开发者可以用技术手段快速提供解决方案。&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>构建高可靠 APK 上传工具，彻底终结网络超时与手误引发的生产事故</title><link>https://blog.91demo.top/apkup-rs/</link><pubDate>Mon, 02 Jan 2023 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/apkup-rs/</guid><description>&lt;p&gt;记录一个未公开的 Rust 实战小工具，分享如何通过技术手段解决网络超时与手误操作带来的生产事故。&lt;/p&gt;
&lt;p&gt;在自学 Rust 的那段日子里，我除了开发开源项目，还为日常工作量身定制了一些不公开的内部工具。其中最令我印象深刻的，是一个看似简单的 &lt;strong&gt;APK 文件上传小工具&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虽然它只是一个 Rust + &lt;code&gt;native-windows-gui&lt;/code&gt; (NWG) 编写的单一界面应用，但它解决的两个核心生产问题，至今仍对我有着深远的影响。&lt;/p&gt;
&lt;h2 id="工具画像极简与高效"&gt;工具画像：极简与高效&lt;/h2&gt;
&lt;p&gt;这个工具的界面极其克制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;交互区&lt;/strong&gt;：一个醒目的文件拖拽控件，文案提示“请把 APK 文件拖拽到这里”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表单区&lt;/strong&gt;：版本号、Version Code、更新描述、是否强制升级（开关）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行区&lt;/strong&gt;：一个大大的“上传”按钮。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它的核心逻辑非常纯粹：校验必填项 -&amp;gt; 调用内部上传 API -&amp;gt; 上传文件 -&amp;gt; 返回成功或失败的对话框。&lt;/p&gt;
&lt;h2 id="故事一消失的-60-秒与隐藏的超时限制"&gt;故事一：消失的 60 秒与隐藏的超时限制&lt;/h2&gt;
&lt;p&gt;工具上线初期，一切运行平稳。由于办公室带宽充足，APK 的上传时间通常维持在 40 秒左右。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事故发生&lt;/strong&gt;：&lt;br&gt;
APK 在集成广告后，经常会发生失败的情况，客户端弹出“超时”的报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我首先检查了后端服务，发现后端接口完全没有超时限制，服务器日志显示连接是被客户端主动断开的。&lt;/li&gt;
&lt;li&gt;多次重复上传文件使用有线和无线对比，经排查是文件变大，由于公司无线网络波动，上传速度时好时坏，就会偶尔出现失败的情况。对比排查，发现失败的情况上传时间都超过了 60 秒。&lt;/li&gt;
&lt;li&gt;回到 Rust 代码中，我发现当时为了追求简洁，直接使用了 HTTP 客户端的默认配置。而这个默认配置的 &lt;strong&gt;Timeout 为 60 秒&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;br&gt;
在那个瞬间我意识到，工具不能只考虑“理想状态”。我手动将本地超时时间放宽至 5 分钟，并增加了状态提示。上传时不会再出现超时的情况了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;教训&lt;/strong&gt;：&lt;strong&gt;永远不要依赖默认的超时设置&lt;/strong&gt;，尤其是在处理文件上传这类长耗时操作时，本地的容错范围必须根据业务场景精细化配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="故事二包名校验把人为失误挡在门外"&gt;故事二：包名校验——把人为失误挡在门外&lt;/h2&gt;
&lt;p&gt;这是这个工具最有价值的一次功能进化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事故发生&lt;/strong&gt;：&lt;br&gt;
有一次，同事不小心将应用 A 的打包产物当作应用 B 上传到了官网，导致用户下载后发现软件张冠李戴。这属于严重的生产事故。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查过程&lt;/strong&gt;：&lt;br&gt;
这个客户端软件调用内部API后，会自动将上传的文件名修改为固定的软件下载文件名，显示在网站上。经测试，如果将应用A的打包产物使用应用B上传，在官网就会出现相同的情况。解决这个方法也非常简单，就是上传的时候小心细致一点，应用A使用A客户端软件，应用B使用B客户端软件。但是人总会犯错，尤其是在面对多个长相相似的 &lt;code&gt;.apk&lt;/code&gt; 文件时。通过肉眼观察文件名来区分应用，是极其不可靠的。&lt;/p&gt;</description></item><item><title>实战笔记：实现一个语音验证码自助拨打版本</title><link>https://blog.91demo.top/voice-code/</link><pubDate>Fri, 19 Aug 2022 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/voice-code/</guid><description>&lt;p&gt;自助语音验证码是通过拨打VOIP电话自动获取验证码，验证码可用来豆子笔记网站认证。这个纯粹是自己的兴趣驱动的。掌握了此项技术，你也可以应用于其它地方。&lt;/p&gt;
&lt;p&gt;我是使用Asterisk实现的，Asterisk 是一个流行的开源通信平台，它提供了构建各种通信应用的灵活而强大的解决方案。它被广泛应用于企业内部电话系统、呼叫中心、语音邮件等场景。Asterisk 支持多种通信协议，包括 SIP、H.323、MGCP 和 SCCP，并且能够与传统的 PSTN 线路深度兼容。此外，Asterisk 还支持通过 Inter-Asterisk eXchange (IAX)协议进行语音 IP 传输，允许数据和语音同时在网络上传输。&lt;/p&gt;
&lt;h2 id="自助语音验证码使用说明"&gt;自助语音验证码使用说明&lt;/h2&gt;
&lt;p&gt;你需要准备：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;软电话，或者支持 VOIP 的电话机&lt;/li&gt;
&lt;li&gt;AST 账户，AST 账户从豆子工具小程序中获取，功能菜单为获取 AST 账户。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：该功能目前已经下架，这里仅作为备忘记录。&lt;/p&gt;
&lt;p&gt;首先，使用获取到的账户配置你的电话。然后拨打 8000，即可自动获取验证码。如果您有什么问题，欢迎关注公众号【技术源泉】私信我。&lt;/p&gt;
&lt;h2 id="技术实现"&gt;技术实现&lt;/h2&gt;
&lt;p&gt;它的工作原理是：用户通过 VOIP 电话连接到 Asterisk，当拨打固定的数字 8000时，Asterisk 将调用 AGI 接口获取服务端的API生成验证码，然后将获取到的验证码拆分通过语音文件播报给用户，播报完毕后自动挂断。&lt;/p&gt;
&lt;p&gt;我们根据自助语音验证码原理，将自助验证码实现分解为以下几个技术要点，只要我们解决以下技术要点，就可以实现自助语音验证码。&lt;/p&gt;
&lt;p&gt;技术要点：&lt;/p&gt;
&lt;p&gt;1，如何播放音频文件？音频文件从哪里来？如何播放动态数据？&lt;br&gt;
2，如何存储用户信息？如何使用数据库存储用户信息？&lt;br&gt;
3，如何获取验证码？&lt;/p&gt;
&lt;p&gt;下面内容是我对各个技术点的对应解决方案。&lt;/p&gt;
&lt;p&gt;1，Asterisk 自带 Playback 应用，可以通过它播放音频文件。音频文件需要我们提前录制好，并且转换为对应的音频格式，最简单的方法就是使用手机上的录音机。当在拨号计划中多次执行 Playback 应用，Asterisk 会将音频流自动连接起来。所以我们可以使用循环多次执行 Playback 应用即可。&lt;br&gt;
2，Asterisk 使用配置文件写入 SIP 用户信息，但当写入新的 SIP 用户信息后，需要重新加载配置文件。为了方便和第三方对接，我们推荐使用数据库。Asterisk 支持数据库，并且可以实时获取用户信息。&lt;br&gt;
3，Asterisk 支持 AGI 接口，我们可以使用 AGI 获取第三方应用的验证码，获取后和提前录制好的文件结合起来进行播放。&lt;/p&gt;
&lt;p&gt;当掌握了这些技术点后，我们就可以灵活应用到其它解决方案。&lt;/p&gt;
&lt;p&gt;下面我们来看具体实现：&lt;br&gt;
1，音频文件处理。我们使用手机上的录音机来录制音频文件。Android 录音机录制的音频文件格式为 mp3，如果是 amr 格式，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。IOS 录音机录制的音频文件格式为 m4a，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能也可以在豆子工具中使用。&lt;/p&gt;</description></item><item><title>实战笔记：当我想查端口却没装 Telnet 时，我决定自己写个工具</title><link>https://blog.91demo.top/check-port/</link><pubDate>Mon, 05 Feb 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/check-port/</guid><description>&lt;p&gt;作为开发者，你一定遇到过这种“抓狂”的时刻：&lt;/p&gt;
&lt;p&gt;某次我急需确认服务器上的一个端口是否正常开启，习惯性地打开 Windows 命令行准备敲下 &lt;code&gt;telnet&lt;/code&gt; 命令。结果弹出的却是：“telnet 不是内部或外部命令”。&lt;/p&gt;
&lt;p&gt;由于系统刚重装，这个组件还没勾选。当我从控制面板找到它、安装、甚至可能还要重启系统时，这套复杂的操作让我陷入了反思：&lt;strong&gt;为了检测一个端口，至于这么麻烦吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更进一步想，如果我手头没有电脑，只有一部手机，我该如何快速判断服务器防火墙是不是忘了开？&lt;/p&gt;
&lt;h3 id="痛点环境依赖与生态限制"&gt;痛点：环境依赖与生态限制&lt;/h3&gt;
&lt;p&gt;在实现这个功能之前，我有过两个思考维度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;环境依赖&lt;/strong&gt;：无论是 Windows 的 Telnet 还是 Linux 的 &lt;code&gt;nc&lt;/code&gt;，都依赖当前操作系统的环境配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小程序限制&lt;/strong&gt;：微信小程序虽然有网络请求能力，但它必须配置服务器域名白名单。如果你想直接用小程序前端去探测一个随机的 IP 或端口，微信的底层安全机制是不允许的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="方案go-后端代劳小程序只做遥控器"&gt;方案：Go 后端代劳，小程序只做“遥控器”&lt;/h3&gt;
&lt;p&gt;为了绕过这些限制，我在“豆子工具”里实现了一个&lt;strong&gt;远程端口检测&lt;/strong&gt;功能。它的逻辑非常直接且高效：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端交互&lt;/strong&gt;：用户只需在小程序里输入目标服务器的 &lt;strong&gt;IP/域名&lt;/strong&gt; 和 &lt;strong&gt;端口号&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端核心&lt;/strong&gt;：数据提交到我用 &lt;strong&gt;Go&lt;/strong&gt; 编写的后端服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟连接&lt;/strong&gt;：后端服务代替用户执行 TCP 连接探测。Go 语言的 &lt;code&gt;net.DialTimeout&lt;/code&gt; 在这里非常实用，可以精准控制探测时间，避免由于网络超时导致的页面死等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果反馈&lt;/strong&gt;：后端将“连接成功”或“连接失败”的结果返回给小程序展示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="服务端口检测" loading="lazy" src="https://blog.91demo.top/images/wander/tool-chkport.webp"&gt;&lt;/p&gt;
&lt;h3 id="价值随时随地的运维眼"&gt;价值：随时随地的“运维眼”&lt;/h3&gt;
&lt;p&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;/li&gt;
&lt;li&gt;&lt;strong&gt;零部署成本&lt;/strong&gt;：再也不用担心当前电脑有没有装 Telnet 或 Netcat。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;这里的难点在于服务器端实现，小程序端仅仅提交FORM表单就可以了。下面我们看看服务端的Go的核心实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 检测端口是否被打开？
func IsPortOpened(ip string, port int) bool {
addr := net.JoinHostPort(ip, fmt.Sprintf(&amp;#34;%d&amp;#34;, port))
conn, err := net.DialTimeout(&amp;#34;tcp&amp;#34;, addr, time.Second*3)
if err != nil {
return false
}
if conn != nil {
conn.Close()
return true
}
return false
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里仅能探测TCP端口，UDP端口我还没有思路，等有思路了我再在这篇文章里补充。&lt;/p&gt;</description></item><item><title>实战笔记：为了管好那堆记不住的密码，我给自己写了个全加密密码本</title><link>https://blog.91demo.top/tool-pwbook/</link><pubDate>Thu, 11 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/tool-pwbook/</guid><description>&lt;p&gt;虽然现在的 App 普遍支持手机号或微信一键登录，但对于开发者来说，GitHub、服务器、各类海外服务的登录依然离不开“邮箱+密码”的传统模式。&lt;/p&gt;
&lt;p&gt;我曾经历过一段漫长的“密码管理进化史”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Excel 时代&lt;/strong&gt;：最早用一个加密的 Excel 表格记录，在电脑端还凑合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flutter App 时代&lt;/strong&gt;：为了手机查看方便，我曾写过一个简单的 Flutter App，支持指纹查看，但功能简陋，只有新增功能，连“修改”功能都没有。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;直到有一次，GitHub 要求我重新登录，由于我频繁重置密码且新旧密码不能重复，导致我彻底记混了。在另一台电脑前尝试了无数次失败后，我意识到：&lt;strong&gt;我需要一个足够安全、随时可用、功能完整的个人密码管理工具。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;于是，“豆子工具”里的&lt;strong&gt;小程序版密码本&lt;/strong&gt;诞生了。&lt;/p&gt;
&lt;p&gt;&lt;img alt="我的密码本" loading="lazy" src="https://blog.91demo.top/images/wander/tool-pwbook.webp"&gt;&lt;/p&gt;
&lt;h3 id="1-设计核心绝对的隐私与自由"&gt;1. 设计核心：绝对的隐私与自由&lt;/h3&gt;
&lt;p&gt;在设计之初，我就定下了两个原则：&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;：这是我花费心血最多的地方。主密码（Master Password）不存储在设备上。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一步&lt;/strong&gt;：用主密码解密 &lt;strong&gt;RSA 私钥&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二步&lt;/strong&gt;：用私钥解密 &lt;strong&gt;AES 密钥&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三步&lt;/strong&gt;：用 AES 密钥解密具体的 &lt;strong&gt;JSON 序列化记录&lt;/strong&gt;。&lt;br&gt;
这种混合加密机制确保了即使手机丢失，只要主密码不泄露，数据依然是安全的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-攻克开发中的大山"&gt;2. 攻克开发中的“大山”&lt;/h3&gt;
&lt;p&gt;这个功能的开发过程远比我想象中痛苦。尤其是在小程序环境下处理&lt;strong&gt;文件缓存、二进制流转换和加解密逻辑&lt;/strong&gt;，经常一个 Bug 就要调试好几天。&lt;/p&gt;
&lt;p&gt;中途我好几次想过放弃，觉得用 Excel 也可以凑合。但每次想到反复重置密码的痛苦，还是咬牙坚持了下来。为了稳定性，我将原本不稳定的二进制存储改为了 UTF-8 编码的 JSON 序列化，最终实现了丝滑的操作体验，这都是心血的教训。&lt;/p&gt;
&lt;h3 id="3-功能完善不仅仅是存一下"&gt;3. 功能完善：不仅仅是“存一下”&lt;/h3&gt;
&lt;p&gt;相比之前的 Flutter 版本，这次我补全了所有短板：&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;/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;h3 id="4-字段灵活满足多样需求"&gt;4. 字段灵活，满足多样需求&lt;/h3&gt;
&lt;p&gt;每一条记录都包含了：&lt;strong&gt;标题、URL、用户名、密码、备注&lt;/strong&gt;。&lt;br&gt;
无论是一个服务器的 SSH 密码，还是一个冷门网站的登录信息，都能井井有条地分类存放。&lt;/p&gt;
&lt;h3 id="5-核心代码展示"&gt;5. 核心代码展示&lt;/h3&gt;
&lt;p&gt;纯小程序本地原生实现，首先是密码本初始化，它会根据用户输入的主密码，生成密码本文件。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 第一次使用设置方法
async setupFirstTime(masterPassword, passwordHint) {
try {
// 1. 检查是否已有密码本
if (!passwordManager.hasPasswordBook()) {
console.log(&amp;#39;1. 创建空密码本...&amp;#39;);
const createResult = await passwordManager.createEmptyPasswordBook(masterPassword, passwordHint);
if (createResult.success) {
console.log(&amp;#39;✓&amp;#39;, createResult.message);
} else {
console.log(&amp;#39;创建失败:&amp;#39;, createResult.error);
wx.showToast({
title: createResult.error,
icon: &amp;#39;none&amp;#39;
});
return;
}
}
} catch (err) {
console.log(&amp;#39;创建密码本错误，&amp;#39;, err)
}
// 保存主密码安装和提示
wx.setStorageSync(&amp;#39;pwbookInstall&amp;#39;, new Date().toLocaleString());
wx.setStorageSync(&amp;#39;passwordHint&amp;#39;, passwordHint || &amp;#39;&amp;#39;);
wx.setStorageSync(&amp;#39;pwbookBakTime&amp;#39;, getSecTs());
this.setData({
isFirstTime: false,
isUnlocked: true,
showPasswordDialog: false
});
wx.showToast({
title: &amp;#39;初始化成功&amp;#39;,
icon: &amp;#39;success&amp;#39;
});
this.loadPasswordList();
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;创建密码本文件核心代码：&lt;/p&gt;</description></item><item><title>实战笔记：为了不再漏掉任何一个域名到期提醒，我做了个自动化检测工具</title><link>https://blog.91demo.top/check-domain/</link><pubDate>Wed, 10 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/check-domain/</guid><description>&lt;p&gt;记录如何通过 Go 语言实现多域名 SSL 证书到期自动巡检，并通过钉钉/企业微信机器人实现精准预警。&lt;/p&gt;
&lt;p&gt;在运维工作中，有一类事故极其低级却又杀伤力巨大：&lt;strong&gt;SSL 证书过期。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="痛点被动挨打的救火模式"&gt;痛点：被动挨打的“救火”模式&lt;/h3&gt;
&lt;p&gt;由于业务需要，我手里管理着大量客户的域名。每个客户购买证书的渠道各异（阿里云、腾讯云或其他厂商），证书下来后通过微信或邮件发给运维，再手动配置到服务器上。&lt;/p&gt;
&lt;p&gt;这套流程在客户少的时候还算正常。但随着客户增多，问题出现了：&lt;strong&gt;证书到期时间不一，全靠人工记忆。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;总会有那么一两次，因为忙碌或者交接疏忽，某个域名证书悄悄过期了。直到客户反馈 APP 无法访问、浏览器弹出红色的安全告警，我们才急忙去“救火”。这种被动的局面不仅影响专业度，也给客户带来了实际损失。&lt;/p&gt;
&lt;h3 id="方案主动出击的自动化巡检"&gt;方案：主动出击的自动化巡检&lt;/h3&gt;
&lt;p&gt;为了彻底根治这个“心病”，我决定做一个自动化的域名证书检测工具。&lt;/p&gt;
&lt;p&gt;我的设想很简单：&lt;strong&gt;变“人找信息”为“信息找人”。&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="1-实现原理"&gt;1. 实现原理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置简单化&lt;/strong&gt;：将所有需要监测的域名汇总成一个 &lt;code&gt;txt&lt;/code&gt; 文件，每行一个域名，管理起来极其方便。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心引擎（Go）&lt;/strong&gt;：使用 Go 语言开发后端服务，利用 &lt;code&gt;cron&lt;/code&gt; 库开启定时任务，设定每天固定时间（如凌晨 4 点）执行一次巡检。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测逻辑&lt;/strong&gt;：程序自动循环读取域名列表，通过 TLS 握手获取证书的有效载荷，计算当前的剩余天数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精准预警&lt;/strong&gt;：我设定了一个“7天阈值”。一旦发现有域名将在 7 天内过期，程序会立即将这些域名汇总，并通过企微或者钉钉送达。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2-消息触达为什么选择机器人"&gt;2. 消息触达：为什么选择机器人？&lt;/h4&gt;
&lt;p&gt;在小程序中，邮箱属于敏感隐私资料，审核往往比较严格。为了避开这个麻烦，同时也为了让通知更具实时性，我选择了&lt;strong&gt;钉钉机器人&lt;/strong&gt;和&lt;strong&gt;企业微信机器人&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;管理员或运营人员只需将机器人的 &lt;code&gt;Webhook&lt;/code&gt; 地址配置好，每天早上一上班，就能在手机上收到一份清晰的到期清单。&lt;/p&gt;
&lt;p&gt;&lt;img alt="域名证书检测" loading="lazy" src="https://blog.91demo.top/images/wander/tool-chkdomain.webp"&gt;&lt;/p&gt;
&lt;h3 id="价值买到了最宝贵的时间"&gt;价值：买到了最宝贵的“时间”&lt;/h3&gt;
&lt;p&gt;这个功能上线后，我们最直接的收获就是**“沟通时间”**：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提前预判&lt;/strong&gt;：有了 7 天的缓冲期，运营人员可以气定神闲地与客户沟通续费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提前操作&lt;/strong&gt;：运维人员有了充裕的时间更换新证书，彻底杜绝了“半夜修证书”的尴尬。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;整个域名检测分为两部分：小程序端和服务端，小程序端负责提交域名信息以及Webhook（钉钉、企微）信息。以及查看导入的域名。服务端需要存储域名以及Webhook。并在每天的固定时刻进行域名巡检，当域名证书有效期小于7天时，将发送通知给管理员。&lt;/p&gt;
&lt;p&gt;小程序端主要是常规的信息提交，我们来看下后端的Go代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 域名证书检测计划任务，每天固定6点执行
func CronCertCheck() {
logger.Info(&amp;#34;启动域名证书检测计划任务&amp;#34;)
// 获取用户webhook，有webhook则进行域名检测
webhooks, err := dao.GetAllWebhook()
if err != nil {
logger.Error(&amp;#34;计划任务域名证书检测加载Webhooks错误，&amp;#34;, err)
return
}
// 循环扫描
for _, v := range webhooks {
uid := v.Uid
url := v.Url
ht := v.HookType
// 获取单个用户的域名
dbDomains, err := dao.GetWDomainByUid(uid)
if err != nil {
logger.Error(uid, &amp;#34;计划任务域名证书检测获取域名列表错误，&amp;#34;, err)
continue
}
if len(dbDomains) == 0 {
logger.Error(uid, &amp;#34;计划任务域名证书检测用户域名数量为0&amp;#34;)
continue
}
// 解密URL地址为WEBHOOK
password := fmt.Sprintf(&amp;#34;domain:%d:%d&amp;#34;, uid, ht)
webhook, err := cryptoutil.AesDecryptWithPassword(url, password)
if err != nil {
logger.Error(uid, &amp;#34;计划任务域名证书检测webhook解密失败，&amp;#34;, err)
continue
}
// 获取域名列表，需要去重，用户是可以上传重复域名的。
domains := make([]string, 0)
domainMap := make(map[string]bool, 0)
for _, n := range dbDomains {
name := n.Name
isExist := domainMap[name]
if isExist {
continue
}
domains = append(domains, name)
domainMap[name] = true
}
// 开始批次检测，并将检测结果返回
chkResult := CheckDomainsCert(domains, nil)
// 如果结果不为空，则发送webhook通知
if len(chkResult) &amp;gt; 0 {
notifyCfg := NotifyConfig{
Type: ht,
URL: webhook,
RetryTimes: 3,
RetryDelay: 5,
}
result := NotifyContent{
Title: &amp;#34;以下域名的SSL证书将在7天内过期:&amp;#34;,
Domains: chkResult,
}
go SendWebhookNotificationWithRetry(notifyCfg, result)
}
// 休息1秒
time.Sleep(1 * time.Second)
}
logger.Info(&amp;#34;域名证书检测处理：&amp;#34;, len(webhooks))
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在小程序上线后，用户使用极少，我不知道具体原因，可能是不信任功能，Webhook不想暴露，域名不想暴露？我又开发了客户端工具，纯本地运行，使用Wails3开发，是图形化桌面客户端，域名导入和通知都是在本地运行。并且增加了很多功能。例如支持HTTP/3，支持系统托盘，支持开启启动，支持导入更多域名，支持更多Webhook通知，监控面板更精细等。&lt;/p&gt;</description></item><item><title>实战笔记：为了不再发错下载链接，我给工具箱加了“扫码鉴定”</title><link>https://blog.91demo.top/tool-scan/</link><pubDate>Tue, 09 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/tool-scan/</guid><description>&lt;p&gt;小工具可以解决大问题。在软件发布流程中，最尴尬的事情莫过于：宣传图已经发出去了，用户扫码后却发现链接打不开，或者下载了一个旧版本的安装包。&lt;/p&gt;
&lt;p&gt;我就亲身经历过这么一次“翻车”事故。&lt;/p&gt;
&lt;h3 id="事故现场一个-url-引起的麻烦"&gt;事故现场：一个 URL 引起的麻烦&lt;/h3&gt;
&lt;p&gt;有一次，我在更新软件下载页面后，由于换了一个新的 URL 地址，在生成二维码阶段没有进行严格校验，就直接把二维码发布了出去。&lt;/p&gt;
&lt;p&gt;结果当用户兴冲冲地扫码下载时，发现软件运行异常。我对比了半天才发现，生成二维码时填写的 URL 链接版本号写错了一个字符，导致用户下载到了一个有问题的版本，这是偶然中的极端。&lt;/p&gt;
&lt;p&gt;虽然这只是一个低级错误，但它让我意识到：&lt;strong&gt;在二维码挂载后、正式发布前，必须有一个极简的“内容鉴定”环节。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="痛点如何快速无痛地校验"&gt;痛点：如何快速、无痛地校验？&lt;/h3&gt;
&lt;p&gt;通常我们校验二维码，习惯性地直接拿手机扫一下。但如果二维码指向的是一个大容量的 APP 下载包或复杂的跳转链接，扫码后手机会自动触发下载或进入复杂的网页路径。&lt;/p&gt;
&lt;p&gt;我其实并不想真的下载并安装测试（那是后续的 QA 环节），我只是想&lt;strong&gt;一眼看到二维码里到底写了什么字符&lt;/strong&gt;，确定那个 URL 是不是我想要的那一个。&lt;/p&gt;
&lt;h3 id="实现借力小程序的原生能力"&gt;实现：借力小程序的原生能力&lt;/h3&gt;
&lt;p&gt;既然发现了痛点，解决起来就非常顺手了。得益于微信生态对扫码的深度支持，我在“豆子工具”里实现了一个“二维码识别”功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑&lt;/strong&gt;：直接调用小程序的 &lt;code&gt;wx.scanCode&lt;/code&gt; 接口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能表现&lt;/strong&gt;：不仅支持扫描实物二维码，还支持从手机相册里直接读取生成的二维码图片，还支持小程序码扫描，支持查看页面路径和scene值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果反馈&lt;/strong&gt;：系统不会触发自动跳转，而是将二维码包含的原始文本、URL、一维码内容直接以&lt;strong&gt;纯文本&lt;/strong&gt;的形式展示在屏幕上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="价值多看一眼少出一次错"&gt;价值：多看一眼，少出一次错&lt;/h3&gt;
&lt;p&gt;现在，每当我生成一个新的二维码，无论是用于软件下载、文档分享还是活动跳转，我都会习惯性地用自己的工具“扫一下”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认 URL 参数是否完整。&lt;/li&gt;
&lt;li&gt;确认是否有肉眼难以察觉的拼写错误。&lt;/li&gt;
&lt;li&gt;确认二维码的类型（一维码还是二维码）是否符合预期。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="识别二维码" loading="lazy" src="https://blog.91demo.top/images/wander/tool-scan.webp"&gt;&lt;/p&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;使用小程序纯原生实现，仅仅做一个页面展示即可。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;scanQRCode: function () {
const that = this;
wx.showLoading({
title: &amp;#39;识别中...&amp;#39;,
mask: true,
})
wx.scanCode({
onlyFromCamera: false,
scanType: [&amp;#39;qrCode&amp;#39;, &amp;#39;barCode&amp;#39;, &amp;#39;datamatrix&amp;#39;, &amp;#39;pdf417&amp;#39;, &amp;#39;wxCode&amp;#39;],
success: (res) =&amp;gt; {
wx.hideLoading()
if(res.scanType==&amp;#39;WX_CODE&amp;#39;){
that.setData({
scanResult: res.path
});
}else{
that.setData({
scanResult: res.result
});
}
},
fail: (err) =&amp;gt; {
wx.hideLoading()
console.error(err);
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;需要注意的是，小程序码和其它码的结果字段有点不同，所以需要区分，以便取到正确的值。&lt;/p&gt;</description></item><item><title>实战笔记：从一个按钮到全能生成器，随机数功能的“进化论”</title><link>https://blog.91demo.top/tool-random/</link><pubDate>Sun, 07 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/tool-random/</guid><description>&lt;p&gt;这个工具是我的图腾，还记得我之前提到的吗？2018 年，“豆子工具”的初版只有一个功能：点一下按钮，生成一个随机数。&lt;/p&gt;
&lt;p&gt;那时候的代码逻辑极其简单，甚至算不上一个“工具”。但随着这几年自己在开发、运维过程中不断遇到“需要一个复杂密码”、“需要一个 16 位纯金钥”或者“需要一个全大写 ID”等实际场景，我发现&lt;strong&gt;简单的随机数其实并不简单&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="痛点单一随机数的局限"&gt;痛点：单一随机数的局限&lt;/h3&gt;
&lt;p&gt;早期的随机数功能非常死板，生成的格式往往不是我想要的。每次生成后，我可能还需要手动去改长度、改大小写，甚至手动补上特殊字符。&lt;/p&gt;
&lt;p&gt;作为一个开发者，如果一个工具不能让我“一键到位”，那就是不合格的。&lt;/p&gt;
&lt;h3 id="进化高度自定义的融合模式"&gt;进化：高度自定义的“融合模式”&lt;/h3&gt;
&lt;p&gt;于是，我把这些年所有关于“随机”的需求全部揉碎、重组，将它升级成了一个全能的生成器。&lt;/p&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;ul&gt;
&lt;li&gt;&lt;strong&gt;纯数字&lt;/strong&gt;：适用于验证码类场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯字母&lt;/strong&gt;：适用于临时 ID 或变量命名。&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;ul&gt;
&lt;li&gt;支持结果一键&lt;strong&gt;转大写&lt;/strong&gt;或&lt;strong&gt;转小写&lt;/strong&gt;。这在配置某些特定系统的 API Key 时非常有用，省去了手动切换输入法的麻烦。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我经常使用它生成一些随机数作为密码，密钥。我不用去数长度，或者生成后，我简单调整一下，让它有一点意义，我再去使用。&lt;/p&gt;
&lt;p&gt;它使用小程序本地实现，下面是一段核心代码，描述了如何去实现随机数：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 生成随机字符串
genRndStr(rtype) {
const that = this;
if (utils.isEmpty(that.data.input1)) {
wx.showToast({
title: &amp;#39;内容为空&amp;#39;,
})
return
}
let result = &amp;#39;&amp;#39;;
switch (rtype) {
case 1:
// 纯数字
result = utils.randNumber(that.data.input1);
break;
case 2:
// 纯字母
result = utils.randLetter(that.data.input1);
break;
case 3:
// 字母和数字
result = utils.randStr(that.data.input1);
break;
case 4:
// 含特殊字符
result = utils.randSymbol(that.data.input1);
break;
}
that.setData({
result1: result
})
},
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="思考小功能里的产品观"&gt;思考：小功能里的“产品观”&lt;/h3&gt;
&lt;p&gt;虽然随机数在技术实现上只是简单的字符数组随机采样，但从产品角度看，它体现了一种**“减少用户操作”**的原则。&lt;/p&gt;</description></item><item><title>实战笔记：知识真的就是金钱，聊聊我的局域网调试工具</title><link>https://blog.91demo.top/debug-tcp/</link><pubDate>Sat, 06 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/debug-tcp/</guid><description>&lt;p&gt;在“豆子工具”的所有功能中，&lt;strong&gt;局域网调试工具（TCP/UDP/WebSocket）&lt;/strong&gt; 并不是受众最广的，但它却是我最引以为傲的一个。&lt;/p&gt;
&lt;p&gt;因为它让我真切地体会到了一句话：&lt;strong&gt;知识就是力量，知识就是金钱。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="缘起把调试器揣进口袋"&gt;缘起：把调试器揣进口袋&lt;/h3&gt;
&lt;p&gt;做嵌入式开发或者网络协议调试的人都知道，以往测试局域网通信，必须背着电脑，接上串口线或网线，蹲在机柜旁守着。我当时就在想：既然微信小程序已经开放了网络通信的能力，为什么我不能做一个随身携带的调试器呢？&lt;/p&gt;
&lt;p&gt;于是，我深度调用了小程序的 &lt;code&gt;wx.createTCPSocket&lt;/code&gt;、&lt;code&gt;wx.createUDPSocket&lt;/code&gt; 和 &lt;code&gt;WebSocket&lt;/code&gt; API，在“豆子工具”里构建了一个完整的网络测试模块。它支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;十六进制（Hex）与 ASCII 码切换&lt;/li&gt;
&lt;li&gt;实时数据日志展示&lt;/li&gt;
&lt;li&gt;局域网内稳定的数据收发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="TCP调试工具" loading="lazy" src="https://blog.91demo.top/images/wander/tool-tcp.webp"&gt;&lt;/p&gt;
&lt;h3 id="变现从技术分享到第一桶金"&gt;变现：从技术分享到“第一桶金”&lt;/h3&gt;
&lt;p&gt;工具做成后，我并没有藏着掖着，而是把实现原理和核心逻辑整理成文，发布到了&lt;strong&gt;微信开发者社区&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;没过多久，一位用户通过那篇文章联系到了我。他在工业自动化领域遇到了一个难题：需要一套能够定制化采集设备数据的微信小程序工具，而协议正是基于 &lt;strong&gt;Modbus/TCP&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于我的“豆子工具”已经打好了坚实的底层基础，我对原有的 TCP 调试模块进行了针对性的修改，迅速就适配出了满足他实际业务场景的采集方案。&lt;/p&gt;
&lt;p&gt;虽然这笔订单带来的“第一桶金”数额并不算惊人，但它对我的意义非凡。它验证了一个逻辑：&lt;strong&gt;当你把一个细分领域的工具做到极致，并愿意分享出去时，价值自然会找上门来。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;这个工具是纯小程序实现，它调用微信小程序的网络API。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;doConnServer() {
const that = this;
wx.getNetworkType({
success(res) {
const networkType = res.networkType
if (networkType !== &amp;#39;wifi&amp;#39;) {
wx.showToast({
title: &amp;#39;请打开WIFI&amp;#39;,
})
return
}
that.connTcp();
}
});
},
connTcp() {
const that = this;
if (!utils.isEmpty(t)) {
t.offMessage()
t.offConnect()
t.offError()
t.offClose()
t.close()
t = null;
that.setData({
isConn: false,
tips: &amp;#34;关闭成功&amp;#34;
})
wx.showToast({
title: &amp;#39;关闭成功&amp;#39;,
})
return
} else {
let ip = that.data.ip;
let port = that.data.port;
if (!utils.isValidIP(ip)) {
wx.showToast({
title: &amp;#39;无效IP地址&amp;#39;,
})
return
}
if (!utils.isPrivateIP(ip)) {
wx.showToast({
title: &amp;#39;请使用私有IP&amp;#39;,
})
return
}
if (utils.containInvalidPort(port)) {
wx.showToast({
title: &amp;#39;非法端口&amp;#39;,
})
return
}
t = wx.createTCPSocket()
t.onError(that.tErr)
t.onConnect(that.tConn)
t.onMessage(that.tMsg)
t.onClose(that.tClose)
t.connect({
address: ip,
port: port,
timeout: 3,
})
}
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这是部分核心TCP代码，它是TCP创建连接的核心代码。完整代码请从下面地址中的豆子碎片小程序内下载：&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;&lt;/p&gt;</description></item><item><title>实战笔记：为了省下服务器流量费，我给小程序加上了 Webp 转换</title><link>https://blog.91demo.top/image-convert/</link><pubDate>Fri, 05 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/image-convert/</guid><description>&lt;p&gt;在“流量贵如油”的今天，作为一名自己买服务器、撸代码的站长，如何给服务器“减负”是每天都要思考的必修课。&lt;/p&gt;
&lt;h3 id="痛点博客网站的流量杀手"&gt;痛点：博客网站的流量杀手&lt;/h3&gt;
&lt;p&gt;我有一个运行多年的个人博客。在分析服务器日志时发现，最大的流量开销并不是文字，而是文章里那些高清的图片。&lt;/p&gt;
&lt;p&gt;虽然 JPG 和 PNG 已经很普及，但随着屏幕分辨率越来越高，原图动辄几 MB，对于按带宽付费或者流量计费的服务器来说，这都是白花花的银子。&lt;/p&gt;
&lt;h3 id="方案webp-格式的降维打击"&gt;方案：Webp 格式的“降维打击”&lt;/h3&gt;
&lt;p&gt;现在主流浏览器（Chrome, Safari, Edge 等）已经全面支持 &lt;strong&gt;Webp&lt;/strong&gt; 格式。相比于传统的 JPG 和 PNG，Webp 可以在保证肉眼看不出画质损失的前提下，将文件体积&lt;strong&gt;压缩 30% 到 70%&lt;/strong&gt;。这是一个非常惊人的数据。这意味着原本 100GB 的图床流量，换成 Webp 后可能只需要 30GB。&lt;/p&gt;
&lt;p&gt;为了方便处理博客素材，我决定把这个需求也集成到“豆子工具”里。&lt;/p&gt;
&lt;p&gt;&lt;img alt="图片格式转换" loading="lazy" src="https://blog.91demo.top/images/wander/image-convert.webp"&gt;&lt;/p&gt;
&lt;h3 id="实现基于-google-官方工具链"&gt;实现：基于 Google 官方工具链&lt;/h3&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;：在服务端安装了 Google 官方出品的 &lt;code&gt;webp&lt;/code&gt; 命令行工具链（&lt;code&gt;cwebp&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端调度&lt;/strong&gt;：依然由 &lt;strong&gt;Go&lt;/strong&gt; 语言担任“指挥官”，接收小程序上传的 PNG/JPG 原图，调用外部 &lt;code&gt;cwebp&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;：现在我写每一篇博客前，都会先用小程序把配图过一遍，转成 Webp 后再上传。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是核心代码片段的注解，这是小程序端：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;page-meta root-font-size=&amp;#34;system&amp;#34; /&amp;gt;
&amp;lt;view class=&amp;#34;page&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;page__hd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;page__title&amp;#34;&amp;gt;图片格式转换&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;page__desc&amp;#34;&amp;gt;可将图片转换为WebP格式，用于Web项目。&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;page__bd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cells weui-cells_radio&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell weui-cell_uploader&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell__bd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__hd&amp;#34;&amp;gt;
&amp;lt;view aria-role=&amp;#34;option&amp;#34; class=&amp;#34;weui-uploader__overview&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__title&amp;#34;&amp;gt;图片信息&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__tips&amp;#34;&amp;gt;
请选取5M内的图片，目前仅支持一张
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__bd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__files&amp;#34; id=&amp;#34;uploaderFiles&amp;#34;&amp;gt;
&amp;lt;block wx:for=&amp;#34;{{files}}&amp;#34; wx:key=&amp;#34;*this&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__file&amp;#34; bindtap=&amp;#34;previewImage&amp;#34; id=&amp;#34;{{item}}&amp;#34;&amp;gt;
&amp;lt;image class=&amp;#34;weui-uploader__img&amp;#34; src=&amp;#34;{{item}}&amp;#34; mode=&amp;#34;aspectFill&amp;#34; /&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/block&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__input-box&amp;#34;&amp;gt;
&amp;lt;view aria-role=&amp;#34;button&amp;#34; aria-label=&amp;#34;选择图片&amp;#34; class=&amp;#34;weui-uploader__input&amp;#34; bindtap=&amp;#34;chooseImage&amp;#34;&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view wx:if=&amp;#34;{{headImageExist}}&amp;#34;&amp;gt;
&amp;lt;image style=&amp;#34;width: 200px; height: 200px; background-color: #eeeeee;&amp;#34; mode=&amp;#34;aspectFit&amp;#34; src=&amp;#34;{{headImage}}&amp;#34; bindtap=&amp;#34;previewMakeImage&amp;#34;&amp;gt;&amp;lt;/image&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view wx:else&amp;gt;&amp;lt;text&amp;gt;转换完成的图片会展示在这里&amp;lt;/text&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;button-sp-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_primary weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;imageConvert&amp;#34;&amp;gt;开始转换&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_warn weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;saveToPhotosAlbum&amp;#34;&amp;gt;保存到相册&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面是界面骨架，现在说下它的灵魂。首先通过小程序的上传文件API，wx.uploadfile将图片文件上传到服务器，然后使用Go调用cwebp进行格式转换，转换成功后，下载文件到小程序本地，然后删除服务器端的文件。&lt;/p&gt;</description></item><item><title>实战笔记：我把 FFmpeg 搬进小程序，搞定了音频格式转换</title><link>https://blog.91demo.top/audio-convert/</link><pubDate>Thu, 04 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/audio-convert/</guid><description>&lt;p&gt;在“豆子工具”众多的功能里，&lt;strong&gt;音频转换（m4a 转 mp3）&lt;/strong&gt; 是我使用频率最高、也最具有“个人救赎”色彩的一个。&lt;/p&gt;
&lt;h3 id="起因被软件更新背刺后的郁闷"&gt;起因：被软件更新“背刺”后的郁闷&lt;/h3&gt;
&lt;p&gt;这个功能的由来非常接地气：有一段时间，我需要频繁地将苹果手机录音产生的 &lt;code&gt;m4a&lt;/code&gt; 格式文件转换成 &lt;code&gt;mp3&lt;/code&gt;，因为当时某个必须使用的业务软件只认 &lt;code&gt;mp3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那时候我找遍了各种转换工具。最后发现某款主流音乐软件自带的转换功能挺好用。然而，好景不长，在一次软件自动更新后，这个功能竟然被砍掉了。我去搜老版本安装包，却发现根本找不到安全的下载路径。&lt;/p&gt;
&lt;p&gt;那种“被绑架”的无奈感，相信每个工具控都深有体会。&lt;/p&gt;
&lt;h3 id="进阶大名鼎鼎的-ffmpeg"&gt;进阶：大名鼎鼎的 ffmpeg&lt;/h3&gt;
&lt;p&gt;郁闷之后，我转向了技术人的终极方案——&lt;strong&gt;ffmpeg&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;命令行虽然硬核，但确实强大到无以复加。一条简单的指令就能解决所有问题：&lt;br&gt;
&lt;code&gt;ffmpeg -i input.m4a output.mp3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用了很长一段时间的命令行后，新的问题又来了，一次在外面，突然用户需要转换音频，我总不能随时随地都带着电脑吧？于是，我动了把 ffmpeg 搬进“豆子工具”的心思。&lt;/p&gt;
&lt;h3 id="实现极简架构下的随时随地"&gt;实现：极简架构下的“随时随地”&lt;/h3&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;code&gt;m4a&lt;/code&gt; 文件，上传至服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端处理&lt;/strong&gt;：后端使用 &lt;strong&gt;Go&lt;/strong&gt; 语言接收文件，通过 &lt;code&gt;exec&lt;/code&gt; 模块调用服务器系统环境中的 &lt;code&gt;ffmpeg&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;：转换生成的文件在用户下载后会立即从服务器删除，既保护了隐私，也完全不占用宝贵的服务器存储空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是核心代码片段的注解，这是小程序端：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;page-meta root-font-size=&amp;#34;system&amp;#34; /&amp;gt;
&amp;lt;view class=&amp;#34;page&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-form&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__text-area&amp;#34;&amp;gt;
&amp;lt;h2 class=&amp;#34;weui-form__title&amp;#34;&amp;gt;音频格式转MP3&amp;lt;/h2&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__desc&amp;#34;&amp;gt;因微信限制，保存文件即为分享文件，可通过微信文件传输助手下载文件。目前仅支持以下格式（M4A、WAV、AMR）文件转换为MP3文件。&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__control-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cells__group weui-cells__group_form&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cells weui-cells_radio&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell__hd&amp;#34;&amp;gt;&amp;lt;label class=&amp;#34;weui-label&amp;#34;&amp;gt;文件&amp;lt;/label&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell__bd&amp;#34;&amp;gt;
&amp;lt;input class=&amp;#34;weui-cell__control weui-cell__control_flex weui-input&amp;#34; type=&amp;#34;text&amp;#34; placeholder=&amp;#34;请选取5M内的音频&amp;#34; placeholder-class=&amp;#34;weui-input__placeholder&amp;#34; value=&amp;#34;{{filename}}&amp;#34; /&amp;gt;
&amp;lt;view aria-role=&amp;#34;button&amp;#34; class=&amp;#34;weui-cell__control weui-btn weui-btn_default weui-vcode-btn&amp;#34; bindtap=&amp;#34;chooseMedia&amp;#34;&amp;gt;选择&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view wx:if=&amp;#34;{{audioExist}}&amp;#34;&amp;gt;
&amp;lt;audio name=&amp;#34;{{filename}}&amp;#34; src=&amp;#34;{{filepath}}&amp;#34; id=&amp;#34;myAudio&amp;#34; controls&amp;gt;&amp;lt;/audio&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view wx:else&amp;gt;&amp;lt;text&amp;gt;选择音频文件后可以在这里试听&amp;lt;/text&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view wx:if=&amp;#34;{{makeAudioExist}}&amp;#34;&amp;gt;
&amp;lt;audio name=&amp;#34;{{makeAudioName}}&amp;#34; src=&amp;#34;{{makeAudioPath}}&amp;#34; id=&amp;#34;myAudio&amp;#34; controls&amp;gt;&amp;lt;/audio&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view wx:else&amp;gt;&amp;lt;text&amp;gt;转换完成的文件会展示在这里&amp;lt;/text&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__opr-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;button-sp-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_primary weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;audioConvert&amp;#34;&amp;gt;开始转换&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_warn weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;saveFile&amp;#34;&amp;gt;保存文件&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面是界面骨架，现在说下它的灵魂。首先通过小程序的上传文件API，wx.uploadfile将音频文件上传到服务器，然后使用Go调用ffmpeg进行格式转换，转换成功后，下载文件到小程序本地，然后删除服务器端的文件。&lt;/p&gt;</description></item></channel></rss>