<?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>豆子技术站</title><link>https://blog.91demo.top/index.html</link><description>Recent content on 豆子技术站</description><generator>Hugo -- 0.155.1</generator><language>zh-cn</language><lastBuildDate>Tue, 07 Apr 2026 07:22:34 +0000</lastBuildDate><atom:link href="https://blog.91demo.top/index.xml" rel="self" type="application/rss+xml"/><item><title>小程序激励广告防刷演进史：从前端校验到“挑战模式”</title><link>https://blog.91demo.top/web/crypto_link.html</link><pubDate>Tue, 07 Apr 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/web/crypto_link.html</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/web/carrier.html</link><pubDate>Thu, 26 Mar 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/web/carrier.html</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>日志查询常用方法</title><link>https://blog.91demo.top/wiki/searchlog.html</link><pubDate>Thu, 19 Mar 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/searchlog.html</guid><description>&lt;p&gt;查询日志里调用最多的5个接口名称&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;grep -o &amp;#39;path: [^ ]*&amp;#39; golog.log | awk &amp;#39;{print $2}&amp;#39; | sort | uniq -c | sort -nr | head -n 5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查询接口调用最多的5个IP&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;grep &amp;#34;path: /getUserById&amp;#34; golog.log | grep -o &amp;#39;ip: [0-9.]*&amp;#39; | awk &amp;#39;{print $2}&amp;#39; | sort | uniq -c | sort -nr | head -n 5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;统计每分钟发送量（Top 分钟）&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# cut -c 7-22: 提取 2026-03-19T00:19 这部分
grep &amp;#34;APP 消息&amp;#34; your_log.log | cut -c 7-22 | sort | uniq -c | sort -nr | head -n 10
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;统计每秒发送量（最频繁的秒）&lt;br&gt;
截取到第 19 位（即 &amp;hellip;00:19:41）：&lt;/p&gt;</description></item><item><title>在51单片机实现 Modbus RTU 协议过程中的踩坑和思考</title><link>https://blog.91demo.top/embedded/sensemodbus.html</link><pubDate>Wed, 18 Mar 2026 08:05:33 +0000</pubDate><guid>https://blog.91demo.top/embedded/sensemodbus.html</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/go/btaddvercheck.html</link><pubDate>Tue, 17 Mar 2026 09:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/btaddvercheck.html</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/devops/cloudreves3.html</link><pubDate>Wed, 11 Mar 2026 08:00:00 +0800</pubDate><guid>https://blog.91demo.top/devops/cloudreves3.html</guid><description>&lt;h2 id="背景与问题描述"&gt;背景与问题描述&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/cloudreve/cloudreve"&gt;Cloudreve&lt;/a&gt;是一个自托管文件管理与共享系统，支持多存储提供商。&lt;/p&gt;
&lt;p&gt;在部署 Cloudreve 存储策略时，我们需要集成内网环境下的 MinIO S3 服务。由于 MinIO 部署在内网并配合内网穿透暴露至公网，且使用了自签名证书，我们在配置完成后发现客户端无法正常上传文件。&lt;/p&gt;
&lt;h3 id="初步排查"&gt;初步排查：&lt;/h3&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;h3 id="最终方案"&gt;最终方案：&lt;/h3&gt;
&lt;p&gt;将自定义 CA 证书导入系统信任库，使 Cloudreve（Go 环境）能够原生识别。&lt;/p&gt;
&lt;h2 id="解决方案在-centos-系统中安装并激活自定义证书"&gt;解决方案：在 CentOS 系统中安装并激活自定义证书&lt;/h2&gt;
&lt;h3 id="1检查证书是否pem格式"&gt;1，检查证书是否PEM格式？&lt;/h3&gt;
&lt;p&gt;Go 程序通常识别PEM格式的证书。&lt;br&gt;
使用以下命令检查证书文件（如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;需要转换为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;</description></item><item><title>微信小程序数据分析指南（用户行为埋点）</title><link>https://blog.91demo.top/wiki/mpanalysis.html</link><pubDate>Fri, 06 Mar 2026 20:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/mpanalysis.html</guid><description>&lt;p&gt;数据分析是微信小程序优化和运营的重要手段。通过用户行为埋点，开发者可以深入了解用户的使用习惯和行为模式，从而进行精准的优化和推广。本文将介绍微信小程序的数据分析方法，特别是用户行为埋点的实施步骤。&lt;/p&gt;
&lt;h2 id="什么是用户行为埋点"&gt;什么是用户行为埋点&lt;/h2&gt;
&lt;p&gt;用户行为埋点是指在小程序的关键节点上埋入数据采集代码，记录用户的行为数据。通过埋点数据，开发者可以分析用户的行为轨迹，了解用户在小程序中的操作习惯和偏好。&lt;/p&gt;
&lt;h2 id="1-埋点规划"&gt;1. 埋点规划&lt;/h2&gt;
&lt;h3 id="确定分析目标"&gt;确定分析目标&lt;/h3&gt;
&lt;p&gt;在进行埋点之前，首先要明确数据分析的目标。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户路径分析：了解用户在小程序中的访问路径。&lt;/li&gt;
&lt;li&gt;功能使用分析：分析用户对各个功能的使用频率和效果。&lt;/li&gt;
&lt;li&gt;活动效果分析：评估营销活动的效果，了解用户的参与情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="设计埋点方案"&gt;设计埋点方案&lt;/h3&gt;
&lt;p&gt;根据分析目标，设计详细的埋点方案，包括以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;埋点位置：确定在小程序的哪些页面和操作上埋点。&lt;/li&gt;
&lt;li&gt;埋点事件：定义具体的埋点事件，如页面访问、按钮点击、表单提交等。&lt;/li&gt;
&lt;li&gt;埋点参数：确定需要采集的参数，如用户 ID、时间戳、页面名称、按钮名称等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-实施埋点"&gt;2. 实施埋点&lt;/h2&gt;
&lt;h3 id="使用微信数据分析工具"&gt;使用微信数据分析工具&lt;/h3&gt;
&lt;p&gt;微信提供了数据分析工具，可以方便地进行埋点和数据采集。&lt;/p&gt;
&lt;h4 id="引入数据分析-sdk"&gt;引入数据分析 SDK&lt;/h4&gt;
&lt;p&gt;在小程序的 &lt;code&gt;app.js&lt;/code&gt; 文件中引入微信数据分析 SDK：&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-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;App&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;onLaunch&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; () {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 引入腾讯分析 SDK
&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;mta&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;mta-wechat-analysis&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 初始化 SDK
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;mta&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;App&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;appID&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;YOUR_APP_ID&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;eventID&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;YOUR_EVENT_ID&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;autoReport&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;statParam&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ignoreParams&lt;/span&gt;&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:#a6e22e"&gt;launched&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="埋点示例"&gt;埋点示例&lt;/h4&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-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Page&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;onLoad&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&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;mta&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Page&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;init&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;handleButtonClick&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&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;mta&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Event&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stat&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;button_click&amp;#34;&lt;/span&gt;, { &lt;span style="color:#a6e22e"&gt;button_name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;start_button&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="使用第三方数据分析工具"&gt;使用第三方数据分析工具&lt;/h3&gt;
&lt;p&gt;除了微信数据分析工具，开发者还可以使用第三方数据分析工具（如 Google Analytics、Mixpanel 等）进行埋点和数据分析。&lt;/p&gt;</description></item><item><title>自制可使用 Modbus 采集的 RS485 温度传感器（原理验证版）</title><link>https://blog.91demo.top/embedded/sensetemp.html</link><pubDate>Fri, 06 Mar 2026 20:05:33 +0000</pubDate><guid>https://blog.91demo.top/embedded/sensetemp.html</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>以动态验证码构建QQ社群防护网：从固定答案到小程序智能管理的安全升级</title><link>https://blog.91demo.top/wiki/qqgrptech.html</link><pubDate>Sun, 01 Mar 2026 20:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/qqgrptech.html</guid><description>&lt;p&gt;在运营技术社群时，一个核心矛盾始终存在：既要开放接纳真正的同行者，又要坚决拦截泛滥的广告与营销机器人。传统的QQ群“固定答案”验证方式，在初期简单有效，但随着时间推移，其静态特性成为了最大的安全漏洞——答案一旦被爬取或泄露，广告机器人便可大规模、自动化地入侵，使群管理陷入疲于应付的被动局面。&lt;/p&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;ul&gt;
&lt;li&gt;
&lt;p&gt;可被遍历破解：如果放在公开页面（如引导页 91demo.top），机器人脚本可以爬取答案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无用户成本：如果设置场景答案（如：“我们的工具叫什么？”），人工获取答案可以零成本，批量QQ号码申请加群。&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;h3 id="1-验证流程动态化与成本化"&gt;1. 验证流程动态化与成本化&lt;/h3&gt;
&lt;p&gt;我们不再让答案“躺”在某个公开页面。流程变为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引导：用户从 91demo.top或其它入口获知，需通过微信小程序获取一次性的进群答案。&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;入群：用户在QQ加群申请中，手动输入或粘贴此动态码完成验证。&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;h3 id="2-管理后台终端化与敏捷化"&gt;2. 管理后台终端化与敏捷化&lt;/h3&gt;
&lt;p&gt;解决了用户端的问题，管理员端同样需要解放。传统修改群答案的操作非常低效，需要修改代码重新发布或者打开管理后台进行设置。&lt;/p&gt;
&lt;p&gt;现在我们的解决方案是：将答案设置功能集成到同一个微信小程序的管理后台。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;移动管理：管理员只需在手机上打开小程序的管理员页面，即可随时、随地生成并设置新的QQ群验证答案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;敏捷响应：一旦发现异常或出于定期更新的安全考虑，管理员能在1分钟内完成答案的全局更新，让所有旧答案立即失效，实现对安全威胁的分钟级响应。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;操作闭环：整个“风险感知 -&amp;gt; 决策 -&amp;gt; 执行”的安全管理闭环，在移动端瞬间完成，极大地提升了社群的主动防御能力。&lt;/p&gt;
&lt;h2 id="三技术实现思路简述"&gt;三、技术实现思路简述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;小程序端：作为核心交互界面，负责展示信息、调用广告组件、并向服务器发起获取动态码的请求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器端：维护验证逻辑。当收到小程序端“广告播放完成”的回调后，结合时间戳、用户临时标识等生成当前有效的QQ群答案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理后台：在小程序中为管理员提供专属界面，调用API通知服务器更新QQ群验证答案，保持信息一致性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;QQ群设置：答案更新后，管理员仍需在QQ群设置中填入新答案，想过自动化设置QQ群答案方案（有封号风险，放弃）。但由于管理后台在手机端，这一步操作变得非常便捷。&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;对管理员：实现了安全策略的敏捷部署与极致便捷的移动化运维。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过“动态验证码”提高攻击成本，再通过“小程序管理后台”降低防御成本，这一升一降之间，我们不仅有效地屏蔽了广告骚扰，更构建了一个具备持续进化能力的社群安全运营体系。技术的价值，在于优雅地解决真实世界的问题，这便是这一实践最好的注脚。&lt;/p&gt;</description></item><item><title>获取QQ进群验证答案</title><link>https://blog.91demo.top/wiki/qqgrpanswer.html</link><pubDate>Sun, 01 Mar 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/qqgrpanswer.html</guid><description>&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;【技术源泉-豆子工具交流群】群号：309409711&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="技术源泉-豆子工具交流群" loading="lazy" src="https://blog.91demo.top/images/qqtechtool.webp"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;【Minio技术学习交流群】群号：1006992606&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Minio技术学习交流群" loading="lazy" src="https://blog.91demo.top/images/qqminio.webp"&gt;&lt;/p&gt;
&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;ol&gt;
&lt;li&gt;
&lt;p&gt;扫描微信小程序码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成验证步骤：系统会引导你观看一段激励广告（通常约15-30秒）。广告播放完毕后，你会自动获得唯一的进群答案（一串数字验证码）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;申请加群：回到QQ群搜索界面，找到我们的群（群号/群名通常会与本站相关），在申请验证答案栏中，输入你刚刚获得的那串答案，并提交申请。&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;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;li&gt;
&lt;p&gt;感谢你的理解、支持与这短短的等待。我们期待在门后与你相遇，一起分享知识，解决问题，共同成长。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>为豆子域名管家实现Windows开机自启动功能的技术实践</title><link>https://blog.91demo.top/go/addautostart.html</link><pubDate>Sun, 01 Mar 2026 09:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/addautostart.html</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/embedded/pigsty.html</link><pubDate>Fri, 27 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/embedded/pigsty.html</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/embedded/watermeter.html</link><pubDate>Thu, 26 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/embedded/watermeter.html</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/embedded/granary.html</link><pubDate>Wed, 25 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/embedded/granary.html</guid><description>记录一段从工业现场布线到云端协议设计的实战经历，反思物联网落地中的协议选择、稳定性保障与系统架构思维。</description></item><item><title>Modbus转MQTT网关固件研发与共创计划</title><link>https://blog.91demo.top/embedded/modujson.html</link><pubDate>Tue, 24 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/embedded/modujson.html</guid><description>记录自研 Modbus-RTU 转 MQTT 网关固件的全过程，探讨工业总线协议与云端交互的架构设计及共创思路。</description></item><item><title>3分钟搞定：用 Headless 模式优雅自动开启 VirtualBox 开发环境</title><link>https://blog.91demo.top/wiki/autostartvm.html</link><pubDate>Wed, 11 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/autostartvm.html</guid><description>&lt;h2 id="前言消失的30秒与开发者的尊严"&gt;前言：消失的“30秒”与开发者的尊严&lt;/h2&gt;
&lt;p&gt;作为开发者，我每天开机后的第一个动作就是：打开 VirtualBox UI -&amp;gt; 选中虚拟机 -&amp;gt; 点击启动 -&amp;gt; 等待窗口弹出 -&amp;gt; 最小化窗口。&lt;/p&gt;
&lt;p&gt;这套动作耗时约 30 秒，虽然微不足道，但这种重复的机械劳动是消磨创造力的元凶。今天，我决定拔掉这颗“硌脚的沙子”，用最优雅的方式让开发环境随系统静默启动。&lt;/p&gt;
&lt;h2 id="技术核心什么是-headless-模式"&gt;技术核心：什么是 Headless 模式？&lt;/h2&gt;
&lt;p&gt;通常我们启动虚拟机都会弹出一个窗口，但在服务器环境下，我们只需要它的后台服务（如 SSH、Web Server）。VirtualBox 提供的 headless 模式可以实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无窗口运行：不占用任务栏，像原生系统服务一样。&lt;/li&gt;
&lt;li&gt;低资源占用：省去了图形界面的显存开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="第一步编写自动化脚本-bat"&gt;第一步：编写自动化脚本 (.bat)&lt;/h3&gt;
&lt;p&gt;为了避免开机瞬间磁盘 IO 占用过高导致启动失败，我们在脚本中加入了 10 秒延迟。AutoStartDev.bat的脚本如下:&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@echo off
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;title Dev-Server Delayed Starter
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#f92672"&gt;[&lt;/span&gt;SYSTEM&lt;span style="color:#f92672"&gt;]&lt;/span&gt; System initialized...
&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;:: Wait &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; system stability
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#f92672"&gt;[&lt;/span&gt;WAIT&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Waiting &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; seconds to ensure system stability...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;timeout /t &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; /nobreak
&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;echo &lt;span style="color:#f92672"&gt;[&lt;/span&gt;SYSTEM&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Starting &lt;span style="color:#e6db74"&gt;&amp;#34;dev-server&amp;#34;&lt;/span&gt; in headless mode...
&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;:: Run VirtualBox command
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;:: Ensure VBoxManage is in your System PATH
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VBoxManage startvm &lt;span style="color:#e6db74"&gt;&amp;#34;dev-server&amp;#34;&lt;/span&gt; --type headless
&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; %errorlevel% equ &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#f92672"&gt;========================================&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#f92672"&gt;[&lt;/span&gt;SUCCESS&lt;span style="color:#f92672"&gt;]&lt;/span&gt; VM &lt;span style="color:#e6db74"&gt;&amp;#34;dev-server&amp;#34;&lt;/span&gt; is now RUNNING.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#f92672"&gt;========================================&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timeout /t &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#f92672"&gt;[&lt;/span&gt;ERROR&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Failed to start VM.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#f92672"&gt;[&lt;/span&gt;TIP&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Check your VM name or VirtualBox installation.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pause
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;dev-server是虚拟机的名称，请修改为你自己的虚拟机名称。&lt;/p&gt;</description></item><item><title>Wails 3 进阶实战：基于 Go 语言实现 frp 自动化管理客户端的代码深度解析</title><link>https://blog.91demo.top/go/mole-summary.html</link><pubDate>Wed, 11 Feb 2026 10:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/mole-summary.html</guid><description>&lt;h2 id="一-项目设计核心思想"&gt;一、 项目设计核心思想&lt;/h2&gt;
&lt;p&gt;本项目的核心定位是内网穿透的一键化管理。参考 ngrok 的服务模式，通过自建 frp 服务器 提供稳定中转，将复杂的配置封装在 Wails 客户端中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商业闭环：通过微信小程序激励视频获取连接权限（2小时有效期）。&lt;/li&gt;
&lt;li&gt;用户体验：一键连接、自动分配二级域名、配置持久化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二-前端架构原生-js-的三维交互"&gt;二、 前端架构：原生 JS 的三维交互&lt;/h2&gt;
&lt;p&gt;为了保持轻量，前端放弃了重量级框架，采用原生 JS 与 Wails 运行时通信。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制面板 (Dashboard)：状态驱动 UI。涉及扫码弹窗逻辑、广告验证状态机。&lt;/li&gt;
&lt;li&gt;配置页面 (Settings)：表单处理。重点在于 Local Port 的保存与通过 Wails Bind 将数据下发给 Go 后端。&lt;/li&gt;
&lt;li&gt;运行日志 (Logs)：虚拟黑屏终端。难点在于实时流式展示后端 frpc 吐出的日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三-后端核心技术要点"&gt;三、 后端核心技术要点&lt;/h2&gt;
&lt;p&gt;将按照应用生命周期逻辑，对以下模块进行深度归纳：&lt;br&gt;
a. 应用原生窗口定义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构化管理：AppManager 模式&lt;/li&gt;
&lt;/ol&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;// AppManager 统一管理应用和窗口&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;AppManager&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;App&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;application&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;App&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;MainWindow&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;application&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Window&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;var&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;manager&lt;/span&gt; = &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AppManager&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;采用了 AppManager 结构体来统一持有 App 实例和 MainWindow 实例。&lt;br&gt;
知识点：在 Wails 3 中，不再像 v2 那样通过上下文（ctx）传递，而是鼓励通过对象持有的方式管理窗口引用。这方便了后续在任何 Service 中通过 manager.MainWindow 直接操控窗口（如置顶、隐藏、发送事件）。&lt;/p&gt;</description></item><item><title>网络协议新纪元：基于 Go 语言实现支持 Ed25519 加密的 QUIC 高性能通信实战</title><link>https://blog.91demo.top/go/quicgo.html</link><pubDate>Tue, 10 Feb 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/go/quicgo.html</guid><description>&lt;h2 id="1-为什么是-quic从-tcp-的瓶颈说起"&gt;1. 为什么是 QUIC？从 TCP 的瓶颈说起&lt;/h2&gt;
&lt;p&gt;手机在 Wi-Fi 和 4G/5G 之间切换（即“切网”）导致 TCP 连接断开，是移动互联网开发中的经典痛点。&lt;/p&gt;
&lt;p&gt;TCP 连接是基于源 IP、源端口、目的 IP、目的端口这“四元组”来标识的。当你从 Wi-Fi 切换到 5G 时，手机的 IP 地址发生了变化，旧的四元组立即失效，TCP 必须重新进行三次握手建立新连接，正在传输的数据（如视频缓冲、下载）就会中断。&lt;/p&gt;
&lt;p&gt;QUIC 引入了 Connection ID 的概念。它不依赖于底层 IP 地址。只要 CID 不变，即便你的 IP 从 A 变成了 B，服务端依然能通过 CID 认出：“噢，你还是刚才那个客户端！”。这样对于业务层完全无感知，数据传输无缝继续。这就是所谓的 “连接迁移 (Connection Migration)”。&lt;/p&gt;
&lt;p&gt;除了切网，QUIC 在弱网（丢包率高、延迟高）下更强的原因在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改进的拥塞控制： QUIC 在应用层实现，可以更激进地进行丢包恢复。&lt;/li&gt;
&lt;li&gt;无队头阻塞： 在 TCP 中，丢一个包全家等死；在 QUIC 中，你刷朋友圈的图丢了一个包，不会影响你接收聊天消息的流。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-go-库quic-go-简介"&gt;2. go 库&lt;a href="https://github.com/quic-go/quic-go"&gt;quic-go&lt;/a&gt; 简介&lt;/h2&gt;
&lt;p&gt;这是Go的库，在 Go 语言世界里，quic-go 是事实上的标准实现。它不仅完整实现了 IETF QUIC 协议，还提供了类标准库 net 的简洁接口。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connection (连接)： 代表两个端点之间的 UDP 隧道。&lt;/li&gt;
&lt;li&gt;Stream (流)： 连接内部的逻辑通道，双向且独立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="3-证书准备使用-openssl-生成-ed25519-证书"&gt;3. 证书准备：使用 OpenSSL 生成 Ed25519 证书&lt;/h2&gt;
&lt;p&gt;为了极致的性能与安全，弃用了传统的 RSA，选择 Ed25519 算法。它的签名速度更快，密钥更短。&lt;/p&gt;</description></item><item><title>从 mdBook 到 Hugo：一位后端开发者的博客架构演进与思考</title><link>https://blog.91demo.top/wiki/mdbook2hugo.html</link><pubDate>Tue, 10 Feb 2026 10:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mdbook2hugo.html</guid><description>&lt;p&gt;在数字内容创作的旅程中，工具的选择往往折射出创作者在不同阶段的诉求。从最初的小程序“豆子碎片”，到尝试使用 &lt;a href="https://rust-lang.org/zh-CN/"&gt;Rust&lt;/a&gt; 生态的 &lt;a href="https://rust-lang.github.io/mdBook/"&gt;mdBook&lt;/a&gt;，再到最终回归并定于 &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;，这不仅是技术栈的迁移，更是我对内容分发、用户体验以及商业化潜力深度思考后的结果。&lt;/p&gt;
&lt;h2 id="1-痛定思痛告别移动端封闭生态的束缚"&gt;1. 痛定思痛：告别移动端封闭生态的束缚&lt;/h2&gt;
&lt;p&gt;最初，我将大量的笔记和技术心得打造成了一个名为“豆子碎片”的小程序。初衷是利用移动端的便捷性，但随着内容的积累，弊端逐渐显现。&lt;/p&gt;
&lt;p&gt;&lt;img alt="豆子碎片小程序" loading="lazy" src="https://blog.91demo.top/images/notes/mdlist.webp"&gt;&lt;br&gt;
最直观的痛点在于&lt;strong&gt;性能与体验&lt;/strong&gt;。小程序在渲染长篇幅的技术文章，尤其是包含大量代码片段的内容时，卡顿感非常明显。更重要的是，作为一名开发者，我发现小程序在内容搜索与社交分享上存在天然的屏障。技术内容应当是开放的，它需要被搜索引擎检索，也需要方便地在网页端被阅读和引用。&lt;/p&gt;
&lt;p&gt;&lt;img alt="豆子碎片小程序" loading="lazy" src="https://blog.91demo.top/images/notes/mdshow.webp"&gt;&lt;br&gt;
为了打破这种孤岛状态，我决定回归网站博客。&lt;/p&gt;
&lt;h2 id="2-抉择为什么不是-mdbook"&gt;2. 抉择：为什么不是 mdBook？&lt;/h2&gt;
&lt;p&gt;在回归 Web 的第一站，我首先关注到了 mdBook。作为一个偏爱简洁风格的开发者，mdBook 这种类似文档流的展示方式起初非常吸引我。然而，在深入使用并尝试进行定制化开发时，我遇到了一些瓶颈。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;扩展性的局限&lt;br&gt;
mdBook 虽然在文档编写上非常纯粹，但在作为通用博客平台的扩展性上显得略为乏力。由于它主要为 Rust 文档设计，当我试图通过编写插件来处理一些特殊逻辑时（例如将我在小程序中定义的私有格式 &lt;code&gt;type|url|params&lt;/code&gt; 自动还原为标准 URL 链接），开发过程并不如预期般顺遂。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;技术栈的契合度&lt;br&gt;
作为一名后端开发者，我对 Go 语言 拥有更高的熟悉度。在折腾 mdBook 插件遇到阻碍后，我意识到：用熟不用生不仅是开发经验，更是提升生产力的核心。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="3-回归-hugo灵活性与商业化的平衡"&gt;3. 回归 Hugo：灵活性与商业化的平衡&lt;/h2&gt;
&lt;p&gt;最终选择 Hugo，是我在权衡了扩展性、性能与长期维护成本后的决定。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;强大的扩展能力与 Go 生态&lt;br&gt;
Hugo 作为基于 Go 语言构建的静态网站生成器，其渲染速度堪称业界天花板。更重要的是，它的模板系统极其灵活。对于我之前在小程序中定义的自定义链接格式，我可以通过 Hugo 的 Shortcodes 或正则表达式替换轻松实现自动化转换。这种“随心所欲”的控制感，是后端开发者最看重的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;布局与精力的分配&lt;br&gt;
在经历了几年的技术折腾后，我悟出了一个道理：开发者的时间应该花在内容创作上，而不是无休止地调整 CSS 布局。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我选择了 PaperMod 主题，因为它精准地命中了我所有的痛点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;极致简洁：没有冗余的装饰，让读者聚焦于文字。&lt;/li&gt;
&lt;li&gt;响应式设计：完美适配手机端阅读，填补了告别小程序后的移动端体验。&lt;/li&gt;
&lt;li&gt;易于 SEO：内置了完善的 SEO 结构，为后续的流量增长打下基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="4-商业化的考量为了-google-adsense"&gt;4. 商业化的考量：为了 Google AdSense&lt;/h2&gt;
&lt;p&gt;之所以选择 Hugo 而非继续留在 mdBook，还有一个非常现实的原因——流量主申请与广告布局。&lt;/p&gt;</description></item><item><title>全栈实战：基于 Go + 小程序构建“口袋指令中心”，实现远程发布控制</title><link>https://blog.91demo.top/go/bagscript.html</link><pubDate>Wed, 04 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/bagscript.html</guid><description>这是一个非常典型的 “运维自动化 + 私有控制台” 的场景。通过微信小程序作为安全鉴权层，利用 Go 后端作为执行引擎，构建了一个属于自己的“口袋指令中心”。</description></item><item><title>实战：为 Hugo 博客开发一个公网 IP 探测 Shortcode</title><link>https://blog.91demo.top/wiki/mypubip.html</link><pubDate>Tue, 03 Feb 2026 10:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mypubip.html</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;工具说明&lt;/strong&gt;：在进行内网穿透（如调试 &lt;code&gt;mole-go&lt;/code&gt;）或配置服务器白名单时，频繁查询公网 IP 是刚需。为了告别繁琐的登录和搜索，我开发了这个集成在博客中的实时探测组件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="-公网-ip-探测"&gt;🌐 公网 IP 探测&lt;/h2&gt;
&lt;div
style="background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 15px; margin: 20px 0; display: flex; align-items: center; justify-content: space-between;"&gt;
&lt;div&gt;
&lt;strong style="color: var(--primary);"&gt;🌐 当前公网 IP：&lt;/strong&gt;
&lt;code id="ip-display" style="font-size: 1.1em; background: none; padding: 0;"&gt;正在探测...&lt;/code&gt;
&lt;/div&gt;
&lt;button id="copy-btn" onclick="copyIP()"
style="background: var(--primary); color: #fff; border: none; border-radius: 4px; padding: 5px 12px; cursor: pointer; font-size: 12px; display: none;"&gt;
复制
&lt;/button&gt;
&lt;/div&gt;
&lt;script&gt;
function fetchIP() {
fetch('https://91demo.top/api/myip')
.then(response =&gt; response.json())
.then(data =&gt; {
const ipSpan = document.getElementById('ip-display');
ipSpan.innerText = data.ip;
document.getElementById('copy-btn').style.display = 'inline-block';
})
.catch(() =&gt; {
document.getElementById('ip-display').innerText = '获取失败，请检查网络';
});
}
function copyIP() {
const ip = document.getElementById('ip-display').innerText;
navigator.clipboard.writeText(ip).then(() =&gt; {
const btn = document.getElementById('copy-btn');
const originalText = btn.innerText;
btn.innerText = '已复制！';
setTimeout(() =&gt; { btn.innerText = originalText; }, 2000);
});
}
document.addEventListener('DOMContentLoaded', fetchIP);
&lt;/script&gt;
&lt;hr&gt;
&lt;h3 id="-核心实现逻辑"&gt;🛠️ 核心实现逻辑&lt;/h3&gt;
&lt;p&gt;这个工具基于 Hugo 的 &lt;strong&gt;Shortcode&lt;/strong&gt; 功能实现，采用了异步加载技术，确保不会影响页面的首屏渲染速度。&lt;/p&gt;</description></item><item><title>Go 语言进阶实战：编写高性能 Windows DLL 动态链接库并供第三方多语言调用</title><link>https://blog.91demo.top/go/godll.html</link><pubDate>Thu, 29 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/godll.html</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>巧用Nginx重定向解决云存储文件地址变更的痛点</title><link>https://blog.91demo.top/wiki/nginx301.html</link><pubDate>Thu, 29 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/nginx301.html</guid><description>&lt;p&gt;在移动应用分发、文件共享等场景中，我们常常会遇到这样的困境：云存储平台（如阿里云OSS、腾讯云COS、蓝奏云等）上传的文件地址一旦生成就无法修改，但客户端版本频繁迭代时，每次更新都需要重新生成下载链接和二维码。用户端需要不断更新访问入口，体验极差。我就碰到了此类问题，在上一篇文章，我介绍了自己的客户端，并分享了下载地址。但我发现当我需要重新升级版本时，已经生成的地址将无法变更。在我的文章中，我直接使用了文件下载地址。这就让我急需解决如何实现&amp;quot;一次分发，永久可用&amp;quot;的优雅方案？Nginx反向代理配合301重定向，正是解决这一痛点的利器。&lt;/p&gt;
&lt;h2 id="一问题根源云存储的地址锁定机制"&gt;一、问题根源：云存储的地址锁定机制&lt;/h2&gt;
&lt;p&gt;大多数云存储服务采用&amp;quot;对象存储&amp;quot;模式，文件上传后生成固定URL，该地址包含存储桶名称、地域、文件路径等不可变信息。这种设计虽然保证了数据一致性，却带来了分发难题：每次版本更新，开发者必须重新上传文件、生成新地址、制作新二维码，用户需要重新扫描或获取新链接。对于频繁迭代的应用，这种重复劳动不仅效率低下，更可能因用户未及时更新而影响使用体验。&lt;/p&gt;
&lt;h2 id="二解决方案nginx重定向的架构设计"&gt;二、解决方案：Nginx重定向的架构设计&lt;/h2&gt;
&lt;p&gt;核心思路是地址解耦——将&amp;quot;物理存储地址&amp;quot;与&amp;quot;用户访问入口&amp;quot;分离。通过Nginx服务器作为中间层，用户始终访问固定域名，Nginx根据配置将请求重定向到实际的云存储地址。当文件更新时，只需修改Nginx配置中的目标地址，用户端的访问入口保持不变。&lt;/p&gt;
&lt;p&gt;具体架构如下：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;用户 → 固定域名/短链接 → Nginx服务器（301重定向） → 云存储实际地址&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这种设计实现了三个关键目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入口稳定性：用户扫描的二维码或保存的链接永久有效&lt;/li&gt;
&lt;li&gt;维护灵活性：后台地址变更只需修改Nginx配置，无需重新分发&lt;/li&gt;
&lt;li&gt;成本可控性：Nginx只做重定向，流量消耗极低，普通云服务器即可承载&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三技术实现从配置到部署"&gt;三、技术实现：从配置到部署&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;环境准备购买一台云服务器（1核1G配置即可），安装Nginx，将自有域名解析到服务器IP。推荐申请SSL证书启用HTTPS。&lt;/li&gt;
&lt;li&gt;Nginx配置示例&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
listen 80;
server_name your-domain.com;
# 方案一：直接重定向
location /download/app.apk {
return 301 https://bucket.oss-cn-region.aliyuncs.com/v2.0/app.apk;
}
# 方案二：通配符匹配（更灵活）
location ~ ^/app/(.*)$ {
rewrite ^/app/(.*)$ https://new-bucket.oss-cn-region.aliyuncs.com/$1 permanent;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;重定向类型选择&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;301永久重定向：搜索引擎会更新索引，客户端会缓存重定向结果，适合生产环境&lt;/li&gt;
&lt;li&gt;302临时重定向：每次请求都会重定向，适合测试阶段频繁变更的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="4"&gt;
&lt;li&gt;二维码生成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将固定地址（如https://your-domain.com/app/v2.0.apk）生成二维码，用户扫描即可下载。后续版本更新时，只需修改Nginx配置中的目标地址，二维码无需重新制作。&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;li&gt;兼容性好：支持各种客户端（浏览器、APP、微信等）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实施建议&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试先行：在测试环境验证重定向逻辑，确保目标地址正确&lt;/li&gt;
&lt;li&gt;监控告警：配置Nginx日志监控，及时发现重定向失败问题&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;Nginx重定向方案将复杂的地址管理问题转化为简单的配置变更，实现了&amp;quot;一次分发，永久可用&amp;quot;的理想状态。这种解耦思维不仅适用于文件分发场景，在API网关、微服务路由等场景中同样具有借鉴意义。对于中小型项目而言，这是成本最低、效果最直接的解决方案，值得每一位开发者掌握。&lt;/p&gt;
&lt;p&gt;最后推荐一下我的豆子域名管家客户端，这个客户端可以解决域名证书到期忘记更换证书的问题。我是为了解决这个问题而想出来的这个方案。这是我的新的下载地址：&lt;a href="https://lab.91demo.top/b011"&gt;https://lab.91demo.top/b011&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这样当我修改下载地址时，我不用再变更这个地址，比如我最近刚刚优化了域名扫描逻辑，上传了新的版本，它生成了一个新的下载地址。&lt;/p&gt;
&lt;p&gt;当然这个方案还是有一点瑕疵，它需要每次更新都登录服务器修改nginx配置文件，这对于非技术人员非常不方便。后期我还有再次进行优化，我将使用小程序完成新地址的更换。大致思路如下：开发一个轻量的Go服务，提供接口给小程序提交新的地址，然后保存编号和新地址到数据库，例如上面的b011是编号，它对应新的下载URL地址。然后使用Nginx代理这个服务，当用户访问b011时查询它的URL地址，然后返回301和新的地址。这样就不用每次都登录服务器修改Nginx配置，在手机上也方便操作，提供了极大的便利性，也可以交给非技术人员进行维护。&lt;/p&gt;</description></item><item><title>告别域名过期焦虑：基于 Go + Wails 3 开发“豆子域名管家”，实现批量监测与企微钉钉预警</title><link>https://blog.91demo.top/go/sslchecker.html</link><pubDate>Wed, 28 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/sslchecker.html</guid><description>&lt;p&gt;域名过期导致业务中断、流量缺失、品牌受损的案例比比皆是。手动记录域名的到期时间？是否会忘记，漏看？&lt;/p&gt;
&lt;p&gt;我以前曾介绍过，我在微信小程序实现了域名证书监控功能。但是担心隐私，功能限制。这次我带来了豆子域名管家，本地化运行。&lt;/p&gt;
&lt;p&gt;经过数月的规划和开发测试，豆子域名管家终于可以使用了。这是一款完全本地运行、支持批量导入管理以及可以企微和钉钉通知的域名证书检测工具。旨在解决域名过期监控难题。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;传统方式&lt;/th&gt;
&lt;th&gt;豆子域名管家&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;运行方式&lt;/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;工具按照自己的真实需求开发，如果你需要尝试，可以通过下方下载链接。目前仅提供了Windows版本。&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/devops/mole-devenv.html</link><pubDate>Thu, 15 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/devops/mole-devenv.html</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 端口启动了服务。虽然此时它还被“困”在局域网内，但它为后续的展示提供了稳固的基础。&lt;/p&gt;
&lt;h2 id="第二步突破局域网束缚frp-与-mole-go"&gt;第二步：突破局域网束缚（FRP 与 Mole-go）&lt;/h2&gt;
&lt;p&gt;为了让公网流量能精准触达内网，我采用了经典的 FRP 方案，但在客户端层面，我使用了自己开发的 Mole-go。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务端 (FRP Server)：部署在具备公网 IP 的云服务器上，充当流量中转站。这个服务器配置可以很低，网站服务都在本地电脑，如果本地有数据库，也非常方便调试。&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;/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 证书的申请与续签。当访问者输入域名时，映入眼帘的是受信任的绿色小锁头，所有的复杂端口逻辑都被完美隐藏。&lt;/p&gt;</description></item><item><title>深度解析：基于 Asterisk 与 SIP 协议的私有化语音验证码呼叫原理</title><link>https://blog.91demo.top/go/vcode-call.html</link><pubDate>Tue, 13 Jan 2026 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/go/vcode-call.html</guid><description>基于 Go-ARI 与 Asterisk 实现自动化语音验证码外呼系统</description></item><item><title>深度实战：基于 Wails v3 与 Go 打造跨平台 FRP 桌面客户端 Mole-go 的技术架构与原理</title><link>https://blog.91demo.top/go/mole-intro.html</link><pubDate>Sat, 10 Jan 2026 06:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/mole-intro.html</guid><description>深入解析 Wails v3 框架下的桌面应用开发，探讨如何集成 FRP 内网穿透核心逻辑、实现 Go 与前端的高效交互及多平台打包实践。</description></item><item><title>把项目做成像 frp 那样的多平台 Release</title><link>https://blog.91demo.top/wiki/ghaction.html</link><pubDate>Wed, 07 Jan 2026 22:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/ghaction.html</guid><description>&lt;p&gt;&lt;a href="https://github.com/littletow/mole-go"&gt;Mole 项目&lt;/a&gt;已经开源，打算把仓库在 GitHub Release 页面中发布各个平台的安装包/二进制（像 frp 那样），已经完成脚本，可参考.github/workflows/release.yml，这个用来记录实现过程与遇到的坑，便于自己回顾。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="大致流程"&gt;大致流程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;查阅 GitHub Actions、goreleaser、actions/create-release、actions/upload-release-asset 等资料。&lt;/li&gt;
&lt;li&gt;在仓库中写一个 matrix workflow，在 Windows / macOS / Ubuntu runner 上分别构建并打包。&lt;/li&gt;
&lt;li&gt;修复构建中出现的依赖与环境差异问题。&lt;/li&gt;
&lt;li&gt;将构建产物上传为 Release asset（先查找/创建 release，再上传各平台包）。&lt;/li&gt;
&lt;li&gt;调试并保证上传在 Release 页面能看到各平台包。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="今天遇到并解决的主要问题按流程顺序"&gt;今天遇到并解决的主要问题（按流程顺序）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Ubuntu 图形库版本不一致&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题：教程写的是 libwebkit2gtk 4.0，但在新版 Ubuntu runner 上只能安装 4.1。&lt;/li&gt;
&lt;li&gt;处理：在 CI 的 apt 安装里兼容 4.0 和 4.1（尝试 4.1，或用 &lt;code&gt;||&lt;/code&gt; 逻辑兼容两种包名）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wails3 安装地址写错导致安装失败&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题：&lt;code&gt;go install&lt;/code&gt; 用错了模块路径，安装一直失败。&lt;/li&gt;
&lt;li&gt;处理：修正为官方地址（例如 &lt;code&gt;github.com/wailsapp/wails/v3/cmd/wails3@latest&lt;/code&gt;），并确保 &lt;code&gt;GOPATH/bin&lt;/code&gt; 在 PATH 中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wails3 的 &lt;code&gt;-platform&lt;/code&gt; / &lt;code&gt;-o&lt;/code&gt; 参数在我的环境不可用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题：官方文档的参数在实际运行时报错或不生效（不能指定输出名）。&lt;/li&gt;
&lt;li&gt;处理：不完全依赖 CLI 的跨平台参数，在对应 runner 上运行构建命令并从 &lt;code&gt;bin/&lt;/code&gt; 下输出文件打包；如有必要向 upstream 提交 issue。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;macOS 的平台特有代码需要 build tag，而不是运行时判断&lt;/p&gt;</description></item><item><title>FRP 图形化管理新方案：基于 Wails 3 的 Mole-go 桌面客户端部署指南</title><link>https://blog.91demo.top/devops/mole-help.html</link><pubDate>Wed, 07 Jan 2026 06:00:00 +0800</pubDate><guid>https://blog.91demo.top/devops/mole-help.html</guid><description>记录自研 FRP 管理工具 Mole-go 的公版部署与使用过程，探讨如何通过 Go + Wails 3 构建极致简单的内网穿透图形化管理体验。</description></item><item><title>git开发常用命令</title><link>https://blog.91demo.top/wiki/git.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/git.html</guid><description>&lt;p&gt;如果你要开始项目开发，看看如下 Git 常用命令对你绝对有帮助。&lt;/p&gt;
&lt;p&gt;在每次开发新功能时，拉出一个新分支，分支名称为开发的功能名称，格式为 func-xxxx。当开发的功能完成并测试没有问题后，合并到主分支。&lt;/p&gt;
&lt;p&gt;当在开发新功能时，如果出现 bug，能够马上解决的，直接在主分支上修改，如果需要时间长，则新创建一个分支，格式为 bug-xxxx。当开发的功能完成并测试没有问题后，合并到主分支。&lt;/p&gt;
&lt;p&gt;1，开发分支。&lt;br&gt;
切换到 dev 分支，进行代码开发。开发完毕后，合并到主分支。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git checkout dev
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2，合并到主分支。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git checkout master
git merge dev
git push origin master
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;3，主分支添加 tag，并发布到线上。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git checkout master
git tag -a v1.0.0 -m &amp;#34;V1.0版本&amp;#34;
git push origin v1.0.0
git push --tags
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;4，删除分支&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git branch -d 分支名称
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;5，设置用户名和邮箱&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git config --global user.name 你的用户名
git config --global user.email 你的邮箱
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="打标签"&gt;打标签&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# 列出标签
git tag
# 列出特定版本标签
git tag -l &amp;#34;v1.8.5*&amp;#34;
# 创建标签
git tag -a v1.4 -m &amp;#34;my version 1.4&amp;#34;
# 显示标签
git show v1.4
# 根据提交打标签
git tag -a v1.2 9fceb02
# 推送标签
git push origin v1.5
# 删除标签
git tag -d v1.4
# 检出标签，浏览使用
git checkout v1.4
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Markdown 语法教程</title><link>https://blog.91demo.top/wiki/markdown.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/markdown.html</guid><description>&lt;p&gt;我们使用 Markdown 作为我们内容的格式，所以学习 Markdown 就显得很必要了。&lt;/p&gt;
&lt;p&gt;Markdown 是一种轻量级标记语言，后缀为.md。因我常用 Markdown 来写一些内容，记录下来常用语法格式。方便自己查找使用，也增加自己的一些流量。Markdown 可以用来撰写电子书，文章，博客等。介绍标题、段落的使用。&lt;/p&gt;
&lt;h2 id="标题"&gt;标题&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="段落"&gt;段落&lt;/h2&gt;
&lt;p&gt;Markd 段落没有特殊的格式，直接编写文件就好，段落的换行使用两个以上空格加上回车。也可以使用一个空行来表示重新开始一个段落。&lt;/p&gt;
&lt;h3 id="字体"&gt;字体&lt;/h3&gt;
&lt;p&gt;使用如下格式分别表示斜体，粗体，粗斜体。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;*斜体文本*
_斜体文本_
**粗体文本**
__粗体文本__
***粗斜体文本***
___粗斜体文本___
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="分割线"&gt;分割线&lt;/h3&gt;
&lt;p&gt;你可以在一行中用三个以上的星号、减号、底线来建立一个分割线，行内不能有其它东西。你也可以在星号或减号中间插入空格。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;***
* * *
*****
- - -
---------
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="删除线"&gt;删除线&lt;/h3&gt;
&lt;p&gt;如果段落上的文字要添加删除线，只需要在文字的两端加上两个波浪线~~即可。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;~~删除线~~
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="下划线"&gt;下划线&lt;/h3&gt;
&lt;p&gt;下划线可以通过 HTML 的&lt;u&gt;标签来实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;u&amp;gt;下划线文本&amp;lt;/u&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="脚注"&gt;脚注&lt;/h3&gt;
&lt;p&gt;脚注是对文本的补充说明。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[^要注明的文本]
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="列表"&gt;列表&lt;/h2&gt;
&lt;p&gt;支持有序列表和无序列表。&lt;/p&gt;
&lt;p&gt;无序列表使用星号、加号、减号作为列表标记，这些标记后面要添加一个空格，然后再填写内容。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;* 第一项
* 第二项
* 第三项
+ 第一项
+ 第二项
+ 第三项
- 第一项
- 第二项
- 第三项
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;有序列表使用数字并加上.号来表示。&lt;/p&gt;</description></item><item><title>豆子碎片的故事</title><link>https://blog.91demo.top/wiki/visit-story.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/visit-story.html</guid><description>&lt;p&gt;豆子碎片小程序很早就注册了，刚开始做的功能就是浏览 Go Web 框架 Beego 的教程。当时，正在自学 Beego，而 Beego 的网站时常打不开，就想做一个小程序，可以浏览 Beego 内容。当时看到了 towxml 组件，正好可以实现 Markdown 转 Wxml，于是就将 Beego 的 内容文档写成 Markdown 格式，做成了小程序。小程序的项目名叫 visit，就是浏览的意思。&lt;/p&gt;
&lt;p&gt;当时小程序还是使用的云环境，云环境免费，并提供 dev 和 pro 两个环境。将 Beego 的 Markdown 文件存在云存储中。使用云函数调用数据。在小程序端使用 towxml 解析 Markdown 数据。当时发布小程序后，还是很受欢迎的。&lt;/p&gt;
&lt;p&gt;中间歇了一段时间，没有更新文章，也没有维护小程序，小程序日浏览人数就下降了。云环境收费后，就彻底不管了。&lt;/p&gt;
&lt;p&gt;后来，买了一台云服务器，为了练习自己所学的 Golang 语言，就将小程序重新拾起来。新做的项目内容是浏览唐诗，使用 Gin 框架进行后端开发，小程序端的布局和现在的豆子碎片布局差不多，只有一个首页，首页包含标题、一个搜索框、几个快捷按钮。项目很快完成，将小学和初中的唐诗、宋词、古诗经都录入了数据库，维护了很长一段时间。小程序上线后，每天也有几个人访问。在录入高中的唐诗内容时，就无法坚持下去了，高中的唐诗等内容都比较长，页面排版非常不美，所以高中的内容就没有录入。小程序在展示唐诗内容时，刚开始使用 Markdown 展示。后来想添加拼音，就将 Markdown 去掉，手撸 wxml，最后，拼音也添加了，但是费了很大劲，自己不擅长前端，美工。本还计划添加录音，但实在没有素材，真正做起来，费时费力，没有精力和动力就搁置了。&lt;/p&gt;
&lt;p&gt;2024 年微信平台要求小程序都要备案。我有三个小程序，本想都放弃了，但后来，在工作中要用到音频格式转换，在小程序端实现操作起来是真的很方便，省时省力，不需要开电脑，也可以在任何时间操作。所以计划留下一两个吧。在备案的时候，注销了一个，剩下了两个，目前还在使用和维护，一个是豆子工具，用来制作工具，平时我也在用，一个是豆子碎片，记录技术经验和代码片段。在备案豆子碎片小程序时，小程序原名称叫找唐诗，提交备案后，微信备案客服人员说唐诗这两字不让用，让换个名称。在换名称的时候，我就在想这个小程序要不要放弃？不放弃做什么内容？已经有一个豆子工具小程序做工具了，没有必要再做一个工具类小程序吧。一直没有想好，等微信备案人员二次打电话时（真的很负责），才确定下来，做笔记类应用，记录日常开发经验和技巧以及代码片段，本想叫代码碎片，为了和豆子工具保持一致，小程序名称就叫豆子碎片，寓意像玩积木一样，学了本小程序内容，你也可以制作一款你想要的小程序。&lt;/p&gt;
&lt;p&gt;豆子碎片项目已经开源，&lt;a href="https://github.com/littletow/visit"&gt;项目地址&lt;/a&gt;：https://github.com/littletow/visit&lt;/p&gt;
&lt;p&gt;豆子碎片目前没有实名认证，如果你喜欢就收藏一下。豆子碎片面对的人群是有技术门槛的，后续看情况再进行实名认证。&lt;/p&gt;
&lt;p&gt;豆子碎片的文章也已经在我公众号发布。有兴趣可以查看一下，&lt;a href="https://mp.weixin.qq.com/s/KoWnv1EejAeu210UkRej1g"&gt;一个程序员之小程序的成长与蜕变&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;想要查看豆子碎片，可以扫下面的小程序码：&lt;/p&gt;
&lt;p&gt;&lt;img alt="豆子碎片" loading="lazy" src="https://ant.91demo.top/imgs/visit.webp#pic_center"&gt;&lt;/p&gt;</description></item><item><title>豆子碎片小程序开发实践：从核心功能到技术实现</title><link>https://blog.91demo.top/wiki/visitdev.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/visitdev.html</guid><description>&lt;p&gt;前面介绍了如何学习豆子碎片小程序，今天本文将介绍包括核心功能实现、技术方案对比和业务逻辑落地等方面。豆子碎片小程序是一款小程序技术和经验等内容展示类的小程序，主要用于展示 Markdown 文件解析后的内容。&lt;/p&gt;
&lt;h2 id="核心功能实现"&gt;核心功能实现&lt;/h2&gt;
&lt;p&gt;豆子碎片小程序的核心功能主要包括以下几个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据下载与本地缓存&lt;/strong&gt;：每次小程序启动时，从服务器下载最新的 Markdown 文件，并缓存到本地，以便后续使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markdown 解析与渲染&lt;/strong&gt;：使用 towxml 组件解析下载的 Markdown 文件，并渲染成 wxml 文件，以便在小程序中展示。&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;h2 id="技术方案对比"&gt;技术方案对比&lt;/h2&gt;
&lt;p&gt;在实现豆子碎片小程序的过程中，我们对多种技术方案进行了对比，最终选择了以下技术方案：&lt;/p&gt;
&lt;h3 id="数据下载与本地缓存"&gt;数据下载与本地缓存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案一：每次启动时都从服务器下载数据&lt;/strong&gt;：这种方案实现简单，但会增加网络请求次数，影响小程序的启动速度和用户体验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案二：使用本地缓存&lt;/strong&gt;：在小程序启动时，先检查本地缓存是否有最新数据，如果有则直接使用，否则从服务器下载数据并缓存到本地。最终我们选择了这种方案，以提高小程序的启动速度和用户体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="markdown-解析与渲染"&gt;Markdown 解析与渲染&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案一：自定义解析器&lt;/strong&gt;：自行开发 Markdown 解析器，解析 Markdown 文件并生成 wxml 文件。这种方案实现复杂，维护成本高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案二：使用现有组件&lt;/strong&gt;：我们对比了 wxParse 组件和 towxml 组件，最终选择使用开源的 towxml 组件进行 Markdown 解析和渲染，它组件成熟，易于集成和使用。最终我们选择了这种方案，以简化开发流程并提高开发效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="首页布局与搜索"&gt;首页布局与搜索&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案一：手动实现布局和搜索功能&lt;/strong&gt;：自行编写代码实现首页布局和搜索功能，灵活性高，但开发成本较高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案二：使用微信小程序提供的组件&lt;/strong&gt;：使用微信小程序提供的 UI 组件，如&lt;code&gt;&amp;lt;view&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;等，实现首页布局和搜索功能，并使用 weui 样式。最终我们选择了这种方案，以简化开发工作并提高开发效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="业务逻辑落地"&gt;业务逻辑落地&lt;/h2&gt;
&lt;p&gt;下方只展示了代码片段，详情请查看 visit 项目。&lt;/p&gt;
&lt;h3 id="数据下载与本地缓存-1"&gt;数据下载与本地缓存&lt;/h3&gt;
&lt;p&gt;在小程序的 &lt;code&gt;app.js&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-javascript" data-lang="javascript"&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;async&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;login&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;that&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tzms&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;utils&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getTodayZeroMsTime&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:#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;that&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;logDevInfo&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 检查Git服务是否正常？
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;isAlive&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;that&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;chkServerAlive&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;url,&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;that&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;globalData&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;url&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;isAlive&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;that&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;uptVer&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;tzms&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;that&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;onLogin&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;onLaunch&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; () {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;startTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Date.&lt;span style="color:#a6e22e"&gt;now&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;globalData&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;startTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;startTime&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;login&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loginTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Date.&lt;span style="color:#a6e22e"&gt;now&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;launchTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loginTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;startTime&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`app onLaunch: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;launchTime&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; ms`&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="首页布局与搜索-1"&gt;首页布局与搜索&lt;/h3&gt;
&lt;p&gt;在首页的 &lt;code&gt;index.wxml&lt;/code&gt; 文件中，我们实现了上中下布局和搜索框：&lt;/p&gt;</description></item><item><title>将数组按照时间重新降序排序</title><link>https://blog.91demo.top/wiki/sortbytime.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/sortbytime.html</guid><description>&lt;p&gt;在写笔记列表页面时，发现从服务器获取的笔记列表包含最新更新时间 lastts，当我们想将最新更新的条目展示在最上端时，我们需要排序数组，将数组按照时间重新排序，排序规则为最新的时间条目放在最前面。&lt;/p&gt;
&lt;p&gt;这样我们就可以在笔记列表中让用户看到最新更新的内容了。&lt;/p&gt;
&lt;p&gt;我们需要将其封装为函数，方便调用。&lt;/p&gt;
&lt;p&gt;函数代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 按照更新时间排序的函数
sortNotesByLastTs: function(notes) {
return notes.slice().sort((a, b) =&amp;gt; {
// 将时间字符串转为时间戳比较
const timeA = new Date(a.lastts).getTime();
const timeB = new Date(b.lastts).getTime();
// 降序排列（最新的在前）
return timeB - timeA;
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当运行之后，发现时间戳格式和服务器返回格式不一致，我们需要重新调整函数，服务器返回格式 YYYY-MM-DD HH:mm:ss，本地函数使用的时间格式 ISO 8601 格式。&lt;/p&gt;
&lt;p&gt;调整后的代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 按照更新时间排序的函数（适配&amp;#34;YYYY-MM-DD HH:mm:ss&amp;#34;格式）
sortNotesByLastTs: function(notes) {
return notes.slice().sort((a, b) =&amp;gt; {
// 解析自定义时间格式
const parseTime = (timeStr) =&amp;gt; {
if (!timeStr) return 0;
// 替换空格为T，使其符合ISO格式
const isoFormat = timeStr.replace(&amp;#39; &amp;#39;, &amp;#39;T&amp;#39;);
return new Date(isoFormat).getTime() || 0;
};
const timeA = parseTime(a.lastts);
const timeB = parseTime(b.lastts);
// 降序排列（最新的在前）
return timeB - timeA;
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;推荐还是调整服务器端，让返回的时间格式转为 ISO 8601 格式。&lt;/p&gt;</description></item><item><title>解析Protobuf文件并转为JSON格式</title><link>https://blog.91demo.top/wiki/pb2json.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/pb2json.html</guid><description>&lt;p&gt;我们知道 Protobuf 文件可以减少体积，方便存储和传输，但在获取文件后，我们还需要将其转换为 JSON 格式，以适配应用进行处理数据。&lt;/p&gt;
&lt;p&gt;下面的代码是在微信小程序中使用，将 pb 文件转换为 JSON，然后再使用。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 小程序根目录执行安装（需开启 npm 支持）
// npm install protobufjs
// 创建 proto 文件 /proto/item.proto
/*
syntax = &amp;#34;proto3&amp;#34;;
message DataItem {
string id = 1;
string name = 2;
string kw = 3;
bool lock = 4;
string category = 5;
string label = 6;
int32 grade = 7;
}
message DataArray { repeated DataItem items = 1; }
*/
// 在 app.js 中引入
const protobuf = require(&amp;#39;protobufjs&amp;#39;);
// 页面 JS 文件
Page({
data: { items: [] },
onLoad() {
this.loadProtoData();
},
async loadProtoData() {
try {
// 1. 下载二进制文件
const { tempFilePath } = await this.downloadFile();
// 2. 读取文件内容
const arrayBuffer = await this.readFile(tempFilePath);
// 3. 加载 Proto 定义
const root = await protobuf.load(&amp;#39;/proto/item.proto&amp;#39;);
const DataArray = root.lookupType(&amp;#39;DataArray&amp;#39;);
// 4. 解析数据
const decoded = DataArray.decode(new Uint8Array(arrayBuffer));
// 5. 转换格式
const result = decoded.items.map(item =&amp;gt; ({
id: item.id || &amp;#39;&amp;#39;,
name: item.name || &amp;#39;&amp;#39;,
kw: item.kw || &amp;#39;&amp;#39;,
lock: typeof item.lock === &amp;#39;boolean&amp;#39; ? item.lock : false,
category: item.category || &amp;#39;&amp;#39;,
label: item.label || &amp;#39;&amp;#39;,
grade: item.grade || 1
}));
this.setData({ items: result });
} catch (err) {
console.error(&amp;#39;解析失败:&amp;#39;, err);
}
},
downloadFile() {
return new Promise((resolve, reject) =&amp;gt; {
wx.downloadFile({
url: &amp;#39;https://your-domain.com/data.bin&amp;#39;,
success: resolve,
fail: reject
});
});
},
readFile(tempFilePath) {
return new Promise((resolve, reject) =&amp;gt; {
wx.getFileSystemManager().readFile({
filePath: tempFilePath,
encoding: &amp;#39;binary&amp;#39;,
success: res =&amp;gt; resolve(res.data),
fail: reject
});
});
}
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当文件非常大时，解析速度会有点慢啊，为了更进一步的提高速度，我们可以使用 WebAssembly，这里是一个 Wasm 示例：&lt;/p&gt;</description></item><item><title>删除JSON数组对象中的某个字段</title><link>https://blog.91demo.top/wiki/removejsonfield.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/removejsonfield.html</guid><description>&lt;p&gt;我需要将 JSON 字符串中的一个对象属性删除，如果手动删除，又累又无聊。&lt;/p&gt;
&lt;p&gt;一个 Python 脚本搞定。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import json
def remove_filename_and_save(input_path, output_path):
&amp;#34;&amp;#34;&amp;#34;
从JSON数组中移除所有对象的filename属性并保存到新文件
Args:
input_path: 输入JSON文件路径
output_path: 输出JSON文件路径
&amp;#34;&amp;#34;&amp;#34;
try:
# 读取输入JSON文件
with open(input_path, &amp;#39;r&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
data = json.load(f)
# 检查是否是数组
if not isinstance(data, list):
print(&amp;#34;错误：输入JSON不是数组对象&amp;#34;)
return False
# 处理每个对象
processed_data = []
for item in data:
if isinstance(item, dict):
# 创建新对象，排除filename属性
new_item = {k: v for k, v in item.items() if k != &amp;#39;filename&amp;#39;}
processed_data.append(new_item)
else:
processed_data.append(item) # 非字典元素保持不变
# 写入输出文件
with open(output_path, &amp;#39;w&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
json.dump(processed_data, f, ensure_ascii=False, indent=2)
print(f&amp;#34;成功处理并保存到 {output_path}&amp;#34;)
return True
except FileNotFoundError:
print(f&amp;#34;错误：输入文件 {input_path} 未找到&amp;#34;)
return False
except json.JSONDecodeError:
print(f&amp;#34;错误：输入文件 {input_path} 不是有效的JSON格式&amp;#34;)
return False
except Exception as e:
print(f&amp;#34;处理文件时出错: {e}&amp;#34;)
return False
if __name__ == &amp;#34;__main__&amp;#34;:
# 文件路径配置
input_json = &amp;#34;/data/data.json&amp;#34; # 输入文件路径
output_json = &amp;#34;/data/data-temp.json&amp;#34; # 输出文件路径
# 执行处理
if remove_filename_and_save(input_json, output_json):
print(f&amp;#34;操作成功完成，结果已保存到 {output_json}&amp;#34;)
else:
print(&amp;#34;操作失败，请检查错误信息&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以基于该脚本进行扩展。&lt;/p&gt;</description></item><item><title>使用towxml组件在小程序中渲染Markdown内容</title><link>https://blog.91demo.top/wiki/articlepage.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/articlepage.html</guid><description>&lt;p&gt;今天我们要讲到含金量非常高的技术了，将 Markdown 内容渲染为小程序页面，这是豆子碎片文章页面。&lt;/p&gt;
&lt;p&gt;在微信小程序中，渲染 Markdown 内容可以使文章展示更加美观和统一。本文将通过一个完整的示例，演示如何从服务器下载 Markdown 内容，并使用 towxml 组件在小程序端渲染为页面。&lt;/p&gt;
&lt;h2 id="目录"&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;需求分析&lt;/li&gt;
&lt;li&gt;页面布局&lt;/li&gt;
&lt;li&gt;下载 Markdown 内容&lt;/li&gt;
&lt;li&gt;使用 towxml 渲染 Markdown&lt;/li&gt;
&lt;li&gt;代码与预览效果对比&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="1-需求分析"&gt;1. 需求分析&lt;/h2&gt;
&lt;p&gt;在本次实战中，豆子碎片文章页面需要实现以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从服务器下载 Markdown 内容&lt;/li&gt;
&lt;li&gt;使用 towxml 组件将 Markdown 内容渲染为页面&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-页面布局"&gt;2. 页面布局&lt;/h2&gt;
&lt;p&gt;豆子碎片文章页面需要包含以下几个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标题区域，用于展示文章标题&lt;/li&gt;
&lt;li&gt;内容区域，用于展示渲染后的 Markdown 内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="示例代码"&gt;示例代码&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-json" data-lang="json"&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;&amp;#34;pages&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;pages/index/index&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;pages/article/article&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;window&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;navigationBarBackgroundColor&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;#ffffff&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;navigationBarTextStyle&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;black&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;navigationBarTitleText&amp;#34;&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;/code&gt;&lt;/pre&gt;&lt;/div&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-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;article-title&amp;#34;&lt;/span&gt;&amp;gt;{{title}}&amp;lt;/&lt;span style="color:#f92672"&gt;view&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/towxml/entry.wxml&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;template&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;is&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;entry&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;{{...articleContent}}&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;view&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;container&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;padding&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&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;article-title&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;24&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-weight&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;bold&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-bottom&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&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;h2 id="3-下载-markdown-内容"&gt;3. 下载 Markdown 内容&lt;/h2&gt;
&lt;p&gt;通过微信小程序的&lt;code&gt;wx.request&lt;/code&gt;方法，从服务器下载 Markdown 内容。&lt;/p&gt;</description></item><item><title>优化内容类小程序快速审核通过</title><link>https://blog.91demo.top/wiki/visitauth.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/visitauth.html</guid><description>&lt;p&gt;我们已经知道如何上架，但是针对内容类小程序，我们还需要再特别注意一些内容。&lt;/p&gt;
&lt;p&gt;引用真实案例！对于内容展示类的小程序，根据微信小程序的审核规则，&lt;strong&gt;个人主体的小程序不能涉及“资讯类”内容&lt;/strong&gt;（如直接展示文章列表、新闻聚合等），否则会被要求以企业主体重新提交。我是技术类文章展示小程序，即使这样，也被拒多次，以下是针对这一问题的优化方案，既能规避审核风险，又能合理引导用户访问技术文章：&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="核心原则"&gt;&lt;strong&gt;核心原则&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内容不应牵涉政治、时事热点、色情暴力等信息&lt;/strong&gt;，这个无需多说。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首页不直接展示文章内容列表&lt;/strong&gt;，避免被判定为“资讯类”。这是资讯类默认布局，容易违反。&lt;/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;/ol&gt;
&lt;hr&gt;
&lt;p&gt;在确定核心原则之后，以下几个推荐布局可以避免判定为资讯类布局。&lt;/p&gt;
&lt;h4 id="方案-1搜索--分类入口--专题推荐"&gt;&lt;strong&gt;方案 1：搜索 + 分类入口 + 专题推荐&lt;/strong&gt;&lt;/h4&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-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&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 style="color:#75715e"&gt;&amp;lt;!-- 搜索框（保持顶部） --&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;search-box&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;placeholder=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;搜索技术文章&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToSearch&amp;#34;&lt;/span&gt; &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 style="color:#f92672"&gt;&amp;lt;/view&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;&amp;lt;!-- 快捷分类入口（工具按钮） --&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;category-buttons&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;button&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToCategory&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-category=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;前端&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;前端技术&lt;span style="color:#f92672"&gt;&amp;lt;/view&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;button&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToCategory&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-category=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;后端&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;后端架构&lt;span style="color:#f92672"&gt;&amp;lt;/view&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;button&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToCategory&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-category=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;数据库&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;数据库优化&lt;span style="color:#f92672"&gt;&amp;lt;/view&amp;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;&amp;lt;/view&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;&amp;lt;!-- 专题推荐（以“技术专题”形式包装） --&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;special-topics&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;text&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;section-title&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;精选技术专题&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;topic&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToTopic&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;image&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/images/topic1.png&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mode=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;aspectFill&amp;#34;&lt;/span&gt; &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 style="color:#f92672"&gt;&amp;lt;text&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;topic-title&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;前端性能优化实战&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;/view&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;topic&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToTopic&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;image&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/images/topic2.png&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mode=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;aspectFill&amp;#34;&lt;/span&gt; &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 style="color:#f92672"&gt;&amp;lt;text&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;topic-title&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;高并发系统设计指南&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;/view&amp;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;&amp;lt;/view&amp;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;&amp;lt;/view&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h4 id="方案-2搜索--问题解答入口--技术资源导航"&gt;&lt;strong&gt;方案 2：搜索 + 问题解答入口 + 技术资源导航&lt;/strong&gt;&lt;/h4&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-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&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 style="color:#75715e"&gt;&amp;lt;!-- 搜索框 --&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;search-box&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;placeholder=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;输入技术问题关键词&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToSearch&amp;#34;&lt;/span&gt; &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 style="color:#f92672"&gt;&amp;lt;/view&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;&amp;lt;!-- 技术问题快速入口（做成问答功能） --&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;qa-section&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;text&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;section-title&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;常见技术问题&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;qa-item&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToArticle&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;101&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;text&amp;gt;&lt;/span&gt;如何解决 Vue 3 响应式丢失问题？&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;/view&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;qa-item&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToArticle&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;102&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;text&amp;gt;&lt;/span&gt;MySQL 索引优化最佳实践&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;/view&amp;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;&amp;lt;/view&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;&amp;lt;!-- 技术资源导航（强调工具属性） --&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;resource-nav&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;nav-card&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToPage&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-page=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;opensource&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;image&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/images/opensource.png&amp;#34;&lt;/span&gt; &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 style="color:#f92672"&gt;&amp;lt;text&amp;gt;&lt;/span&gt;开源项目推荐&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;/view&amp;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;&amp;lt;view&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;nav-card&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bindtap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;navigateToPage&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data-page=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;&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 style="color:#f92672"&gt;&amp;lt;image&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/images/tools.png&amp;#34;&lt;/span&gt; &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 style="color:#f92672"&gt;&amp;lt;text&amp;gt;&lt;/span&gt;在线开发工具&lt;span style="color:#f92672"&gt;&amp;lt;/text&amp;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;&amp;lt;/view&amp;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;&amp;lt;/view&amp;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;&amp;lt;/view&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h3 id="关键审核规避策略"&gt;&lt;strong&gt;关键审核规避策略&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;避免“文章列表”字眼&lt;/strong&gt;：&lt;/p&gt;</description></item><item><title>在列表中搜索符合条件的内容</title><link>https://blog.91demo.top/wiki/searchkw.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/searchkw.html</guid><description>&lt;p&gt;当我们创建笔记索引内容后，如何搜索是个关键的问题，目前的设计就是先搜索 name，看名称中是否包含关键字，然后查找 kws 数组，查看数组中是否有关键字，最后查看 tags 数组中是否有关键字？查到以后取出来，返回数组列表。&lt;/p&gt;
&lt;p&gt;数据的结构如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[{
&amp;#34;id&amp;#34;:&amp;#34;id1&amp;#34;,
&amp;#34;name&amp;#34;:&amp;#34;小星星&amp;#34;,
&amp;#34;kws&amp;#34;:[&amp;#34;爱&amp;#34;,&amp;#34;崇拜&amp;#34;],
&amp;#34;tags&amp;#34;:[&amp;#34;想法&amp;#34;,&amp;#34;感情&amp;#34;],
}]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果使用 JS 来实现，那么代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/**
* 搜索笔记
* @param {Array} notes 笔记数组
* @param {String} keyword 搜索关键词
* @returns {Array} 匹配的笔记数组
*/
function searchNotes(notes, keyword) {
if (!keyword || typeof keyword !== &amp;#39;string&amp;#39;) {
return []; // 如果关键词无效，返回空数组
}
// 统一转换为小写以便不区分大小写搜索
const lowerKeyword = keyword.toLowerCase();
return notes.filter(note =&amp;gt; {
// 检查名称是否包含关键词
const nameMatch = note.name &amp;amp;&amp;amp; note.name.toLowerCase().includes(lowerKeyword);
// 检查kws数组是否包含关键词
const kwsMatch = Array.isArray(note.kws) &amp;amp;&amp;amp;
note.kws.some(kw =&amp;gt; kw &amp;amp;&amp;amp; kw.toLowerCase().includes(lowerKeyword));
// 检查tags数组是否包含关键词
const tagsMatch = Array.isArray(note.tags) &amp;amp;&amp;amp;
note.tags.some(tag =&amp;gt; tag &amp;amp;&amp;amp; tag.toLowerCase().includes(lowerKeyword));
// 如果任一条件匹配，则包含该笔记
return nameMatch || kwsMatch || tagsMatch;
});
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里支持多维度搜索，同时搜索名称、关键词和标签三个字段，当搜索时，先将关键字转为小写，这样比较时不区分大小写。为了性能优化，使用 filter 和 some 方法，减少不必须要的循环。&lt;/p&gt;</description></item><item><title>在小程序文章页面动态添加广告</title><link>https://blog.91demo.top/wiki/dynaddad.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/dynaddad.html</guid><description>&lt;p&gt;这是一项含金量非常高的技能，在页面中动态添加广告，可以增加广告曝光量和提高点击率。&lt;/p&gt;
&lt;p&gt;小程序使用 Markdown 来编写文章内容，存储在服务器或者后端对象存储，小程序打开时，会从服务器拉取文章索引，然后展示文章列表页面给用户浏览。当用户点击列表上的某篇文章时，小程序下载该篇文章的 Markdown 内容，并在小程序端使用 towxml 组件进行解析渲染，该组件可以将 Markdown 内容渲染成 WXML 文件内容。&lt;/p&gt;
&lt;p&gt;文章页面集成了流量主原生模板广告。起初是在页面顶端显示，直接在 article.wxml 页面顶部添加模板广告代码，就可以集成广告了，非常的简单。当前文章页面布局如下：从上到下广告组件、Markdown 文章内容、页面底部说明。但是当实际上线后，发现这样的广告曝光量虽然非常高，但是页面整体看起来非常的不舒服，不美观。然后将广告位置调整到文章底部，因为文章底部和页面底部之间有自定义内容，广告看起来还比较舒服。当前文章页面布局如下：从上到下 Markdown 文章内容、广告组件、页面底部说明。上线之后，发现实际的曝光量降低了很多，因为很多用户是不会浏览到页面底部的。所以广告也不会曝光。得想想办法。在写公众号时，发现公众号插入到文章中间的广告模式不错。我感觉这是一个不错的方案。可以这样试试。&lt;/p&gt;
&lt;p&gt;现在就是如何将广告显示在文章中间。文章内容是使用 towxml 渲染的。比如，将广告显示在第一个段落之后。最初的想法，是查找文章页面的段落 p 元素，然后动态插入广告。根据微信官方文档内容，发现可以使用 wx.createSelectorQuery API，但当实际调试后，发现它无法穿透查询 towxml 生成的内容。然后使用 this.createSelectorQuery，这个是查询组件元素使用的，发现这个确实可以查询组件的元素，也确实查找到了 towxml 生成的内容的顶层元素，但是 towxml 生成的内容是多层嵌套，towxml 下面的组件是 decode，decode 然后再进行嵌套自定义组件。也就是说页面中存在着多个#shadow-root，它用来隔离元素和样式。感觉如果这样的布局使用这种方案行不通了。或者即使实现也非常的复杂，还是需要修改 towxml 组件。&lt;/p&gt;
&lt;p&gt;当上面通过查询页面元素行不通后，就得想其它办法。虽然这个方案没有解决问题，但是学到了很多东西，以后在同一个#shadow-root 下的元素查找技能就学会了。本来一直不想动 towxml 库，现在的方案只有修改这个库了，来适配自己的需求。towxml 库的原理如下，解析原始的 Markdown 文件或者 Html 文件生成 AST，然后将 AST 转换成小程序 WXML 标签和结构。这里有两种方法：一种是在解析时处理，生成对应的广告组件；另一种是解析成对象后再插入数据。第一种方法实现后，就可以在 HTML 或 Markdown 文件中加入广告标签，然后 towxml 自动解析。虽然这一种功能看起来更强大，但是我的实际场景不是太适合，我的文章来源 Markdown 内容不一定是我编写，我的小程序只是内容展示平台。所以只能选择第二种方法，这个方法更灵活，可以在解析后的 AST 里插入广告数据，然后在 WXML 页面里根据条件进行渲染。这样用户提交的内容，我不进行修改就可以显示广告。想想还是不错的。&lt;/p&gt;
&lt;p&gt;接下来就要考虑如何在 AST 插入数据，以及插入到哪个位置？如何查找段落位置？页面如何显示？我可以修改 towxml 库，以适配广告组件渲染，或者在自己的文章页面进行广告组件渲染。无论使用哪种方法，完整的数据流应该是这样的：Markdown 原始文件 -&amp;gt; Towxml 解析 -&amp;gt; 插入广告节点 -&amp;gt; 绑定广告数据 -&amp;gt; 渲染带广告的 WXML。剩下的内容就是想办法解决渲染广告组件这个问题了。&lt;/p&gt;</description></item><item><title>在小程序页面查找元素</title><link>https://blog.91demo.top/wiki/queryelement.html</link><pubDate>Mon, 05 Jan 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/wiki/queryelement.html</guid><description>&lt;p&gt;为了实现在页面中动态中插入广告，我们需要学会在小程序中查找元素的技能。例如，我要将广告显示在某某元素之后。&lt;/p&gt;
&lt;p&gt;在微信小程序开发中，由于逻辑层（JavaScript）与视图层（WXML）分离的特性，开发者无法直接操作 DOM 元素。为了获取页面中某个元素的属性（如位置、尺寸等），小程序提供了 wx.createSelectorQuery API，通过创建节点查询对象来间接操作元素。&lt;/p&gt;
&lt;p&gt;wx.createSelectorQuery 用于创建一个节点查询对象（SelectorQuery 实例），开发者可以通过该对象选择页面中的元素，并获取其布局信息（如宽高、位置等）。除了 wx.createSelectorQuery 外，还有一个 this.createSelectorQuery，他们之间的区别是一个用在页面中，一个用在组件组件中。&lt;/p&gt;
&lt;p&gt;当我们创建了查询对象后，我们需要选择元素，选择元素有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select(selector)：选择匹配选择器的第一个节点。&lt;/li&gt;
&lt;li&gt;selectAll(selector)：选择所有匹配的节点。&lt;/li&gt;
&lt;li&gt;selectViewport()：选择视口（可用于获取滚动容器的信息）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当元素匹配到之后，我们可以获取元素属性，获取节点属性的方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;boundingClientRect()：获取节点的位置和尺寸。&lt;/li&gt;
&lt;li&gt;scrollOffset()：获取节点的滚动位置（适用于滚动容器）。&lt;/li&gt;
&lt;li&gt;fields({&amp;hellip;})：自定义需要获取的字段（如 id，dataset，rect 等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后，一定要记得调用 exec(callback)，这样结果才能查询出来。&lt;/p&gt;
&lt;p&gt;下面我们举个小示例，可以方便地看到它如何使用？在这个示例中，我们将获取元素的尺寸和位置。场景如下：点击页面按钮，获取页面中某个 view 元素的宽度、高度和位置信息。&lt;/p&gt;
&lt;p&gt;1，WXML 文件如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;view class=&amp;#34;target-box&amp;#34; id=&amp;#34;target&amp;#34;&amp;gt;我是目标元素&amp;lt;/view&amp;gt;
&amp;lt;button bindtap=&amp;#34;getElementInfo&amp;#34;&amp;gt;获取元素信息&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2，JS 文件如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Page({
getElementInfo() {
// 1. 创建查询实例
const query = wx.createSelectorQuery();
// 2. 选择元素并指定获取的属性
query.select(&amp;#39;#target&amp;#39;).boundingClientRect();
// 3. 执行查询
query.exec((res) =&amp;gt; {
// res 是包含查询结果的数组
const rect = res[0];
console.log(&amp;#39;元素宽度:&amp;#39;, rect.width);
console.log(&amp;#39;元素高度:&amp;#39;, rect.height);
console.log(&amp;#39;元素位置:&amp;#39;, rect.left, rect.top);
});
}
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果代码没有错误的话，控制台将输出类似如下内容：&lt;/p&gt;</description></item><item><title>Hugo 进阶：集成 Google Analytics 4 与站点收录优化</title><link>https://blog.91demo.top/wiki/googleanalytics.html</link><pubDate>Fri, 02 Jan 2026 10:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/googleanalytics.html</guid><description>详细记录如何在 Hugo FixIt 主题中集成 Google 统计分析（GA4），并完成站点地图提交，助力新站快速被 Google 检索，开启全新的技术航程。</description></item><item><title>Hugo 进阶：FixIt 主题深度配置（目录结构、多级菜单与 Logo）</title><link>https://blog.91demo.top/wiki/hugofixit.html</link><pubDate>Thu, 01 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/hugofixit.html</guid><description>在完成初步部署后，本文详细记录了如何深入定制 FixIt 主题，包括内容目录的逻辑组织、多级导航菜单的实现方案，以及站点视觉 Logo 的配置方法。</description></item><item><title>从零到一：Hugo 项目部署 Cloudflare Pages 避坑指南</title><link>https://blog.91demo.top/wiki/cloudflare.html</link><pubDate>Thu, 01 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/cloudflare.html</guid><description>记录了 豆子技术站从 Hugo 创建到 Cloudflare 部署的全过程，解决了样式丢失、自定义域名报错 1001、SSL 证书失效等核心问题。</description></item><item><title>关于我 (About Me)</title><link>https://blog.91demo.top/about.html</link><pubDate>Thu, 01 Jan 2026 09:10:00 +0800</pubDate><guid>https://blog.91demo.top/about.html</guid><description>&lt;h1 id="eagle"&gt;Eagle&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“种一棵树最好的时间是十年前，其次是现在。”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="欢迎来到我的技术领地"&gt;欢迎来到我的技术领地&lt;/h2&gt;
&lt;p&gt;我是一名拥有多年经验的资深开发者，职业深耕&lt;strong&gt;Go&lt;/strong&gt;语言，业余钟情 &lt;strong&gt;Rust&lt;/strong&gt;、&lt;strong&gt;小程序&lt;/strong&gt; 及 &lt;strong&gt;嵌入式&lt;/strong&gt; 探索。&lt;/p&gt;
&lt;p&gt;在 2026 年之前，我的技术重心主要围绕微信小程序生态展开，积累了宝贵的实践经验。迈入 2026 年，我正战略性地拓展技术边界，回归更广阔、更开放的 Web 技术栈视野。未来，我的核心精力将投入到 Go 和 Rust 等底层技术的深度沉淀，旨在构建更稳健的基础设施与服务；而小程序则会作为前端生态中的一个高效节点，继续在我整体的技术版图中发挥其独特的连接作用。&lt;/p&gt;
&lt;h2 id="关于本站我的技术演进与思考"&gt;关于本站：我的技术演进与思考&lt;/h2&gt;
&lt;p&gt;本站基于 Hugo 构建。在此之前，我的技术输出主要集中在小程序和公众号，但随着思考的深入，我发现了各自的局限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小程序：虽然便捷，但在渲染复杂的 Markdown 代码片段时，性能瓶颈导致的卡顿往往会打断阅读的连贯性。&lt;/li&gt;
&lt;li&gt;公众号：它更像是一个资讯窗口，并不适合大篇幅、深层次的代码演示与技术推演。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是，我重新梳理了我的技术版图：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小程序：定位为线上线下的连接工具，发挥其极致的触达效率。&lt;/li&gt;
&lt;li&gt;公众号：作为宣传与引流的领地，负责捕捉灵感与传播声音。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本站&lt;/strong&gt; (Hugo)：这是我真正的&lt;strong&gt;思考领域&lt;/strong&gt;。在这里，我会详细记录对技术的深度钻研、对复杂问题的复盘分析，以及那些更偏向底层、更硬核的技术研究。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这里，文字不再受限于载体，只服务于逻辑与思想。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="实战作品集-mini-programs"&gt;实战作品集 (Mini-Programs)&lt;/h2&gt;
&lt;p&gt;这是我将技术想法落地的两个核心项目，也是我多年开发经验的缩影。&lt;/p&gt;
&lt;h3 id="豆子碎片--实战模块代码集市"&gt;豆子碎片 —— 实战模块代码集市&lt;/h3&gt;
&lt;p&gt;一句话简介：我多年的技术积累，包含了我在 Go、Rust、小程序及 Web 领域的全部实战项目代码。&lt;/p&gt;
&lt;p&gt;直观功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目全览：首页列表式展示我做过的所有成熟模组与完整项目。&lt;/li&gt;
&lt;li&gt;深度阅读：点击即进入项目模块详情，完整的 Markdown 文档说明。&lt;/li&gt;
&lt;li&gt;源码获取：支持直接下载源码。无论你是想参考架构还是寻找现成的解决方案，这里都能直接拿走。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;体验入口：&lt;br&gt;
&lt;img src="https://blog.91demo.top/images/visit.webp" width="200" alt="豆子碎片小程序"&gt;&lt;/p&gt;
&lt;h3 id="豆子工具--我的微信口袋助手"&gt;豆子工具 —— 我的微信口袋助手&lt;/h3&gt;
&lt;p&gt;一句话简介：基于微信账号体系开发的“全能口袋工具”，让我脱离电脑也能在手机上处理专业任务。&lt;/p&gt;
&lt;p&gt;直观功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;便捷转换：调用底层 API 快速实现音频、图片的格式转换。&lt;/li&gt;
&lt;li&gt;安全鉴权：直接关联微信登录，内置私密密码本，确保敏感数据随身且安全。&lt;/li&gt;
&lt;li&gt;随时随地：方便在移动端操作的实用技术小工具，弥补PC客户端的不足。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;体验入口：&lt;br&gt;
&lt;img src="https://blog.91demo.top/images/wander.webp" width="200" alt="豆子工具小程序"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="欢迎交流与连接"&gt;欢迎交流与连接&lt;/h2&gt;
&lt;p&gt;如果你对 Go/Rust 底层开发、小程序架构或嵌入式方案感兴趣，欢迎通过以下方式与我联系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub: @littletow —— 这里记录着我的代码足迹与开源实践。&lt;/li&gt;
&lt;li&gt;Email: &lt;code&gt;longfengfirst (at) gmail [dot] com&lt;/code&gt; —— 深度探讨或项目交流请致信于此。&lt;/li&gt;
&lt;li&gt;公众号: 搜索 “技术源泉” —— 获取最新的技术动态与灵感推送。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="基础设施-infrastructure"&gt;基础设施 (Infrastructure)&lt;/h2&gt;
&lt;p&gt;本站坚持高可用与极速访问体验，目前稳定运行于：&lt;/p&gt;</description></item><item><title>隐私政策 (Privacy Policy)</title><link>https://blog.91demo.top/privacy.html</link><pubDate>Thu, 01 Jan 2026 09:10:00 +0800</pubDate><guid>https://blog.91demo.top/privacy.html</guid><description>&lt;h2 id="隐私政策-privacy-policy"&gt;隐私政策 (Privacy Policy)&lt;/h2&gt;
&lt;p&gt;本隐私政策详细说明了 &lt;strong&gt;豆子技术站&lt;/strong&gt;（以下简称“本站”）如何收集、使用和保护您的个人信息。&lt;/p&gt;
&lt;h3 id="1-信息收集"&gt;1. 信息收集&lt;/h3&gt;
&lt;p&gt;本站本身不强制要求用户注册，也不会主动收集您的个人敏感信息。但我们会通过第三方服务（如 Google Analytics）记录基础的匿名访问数据（如 IP 地址、浏览器类型、停留时间等），用于优化内容体验。&lt;/p&gt;
&lt;h3 id="2-google-adsense-与-cookie"&gt;2. Google AdSense 与 Cookie&lt;/h3&gt;
&lt;p&gt;本站使用 &lt;strong&gt;Google AdSense&lt;/strong&gt; 投放广告。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google 作为第三方广告供应商，使用 Cookie 根据用户对此网站及互联网上其他网站的访问情况来投放广告。&lt;/li&gt;
&lt;li&gt;Google 对广告 Cookie 的使用（包括 &lt;strong&gt;DoubleClick Cookie&lt;/strong&gt;）使 Google 及其合作伙伴能够根据用户对本站和/或互联网上其他网站的访问情况向用户投放广告。&lt;/li&gt;
&lt;li&gt;您可以访问 &lt;a href="https://www.google.com"&gt;Google 广告设置&lt;/a&gt; 来停用个性化广告。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-第三方链接"&gt;3. 第三方链接&lt;/h3&gt;
&lt;p&gt;本站包含指向其他网站（如 GitHub、外部技术文档等）的链接。我们不对这些第三方网站的隐私做法负责，建议您查阅其各自的隐私政策。&lt;/p&gt;
&lt;h3 id="4-政策更新"&gt;4. 政策更新&lt;/h3&gt;
&lt;p&gt;我们可能会不时更新本隐私政策。建议您定期查看此页面以了解任何更改。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;联系方式&lt;/strong&gt;：如果您对本政策有任何疑问，请通过 &lt;a href="https://blog.91demo.top/about.html"&gt;关于我&lt;/a&gt; 页面中的方式联系。&lt;/p&gt;</description></item><item><title>回归初心：从演示版到公版，打造最纯粹的 frp 桌面管理工具</title><link>https://blog.91demo.top/wiki/tool-intro.html</link><pubDate>Thu, 01 Jan 2026 08:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-intro.html</guid><description>去掉多余的商业化尝试，Mole 客户端正式步入“全能工具”阶段：支持多协议、优化配置体验、追求极致稳定。</description></item><item><title>豆子碎片小程序项目第七版</title><link>https://blog.91demo.top/wiki/v7.html</link><pubDate>Wed, 03 Dec 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v7.html</guid><description>最终形态：将七次进化，铸成一座时间轴博物馆</description></item><item><title>基于 Wails 3 和微信小程序的一次frp 智能管理实战</title><link>https://blog.91demo.top/go/demo-intro.html</link><pubDate>Tue, 02 Dec 2025 08:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/demo-intro.html</guid><description>记录FRP GUI客户端开发的完整流程，桌面工具如何和小程序生态进行集合完成多端通信。</description></item><item><title>Mosquitto 配置WebSocket连接</title><link>https://blog.91demo.top/wiki/mqttws.html</link><pubDate>Tue, 04 Nov 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/mqttws.html</guid><description>&lt;p&gt;在 mosquitto.conf 配置文件中，加入如下内容：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;listener 18083
protocol websockets
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样，就可以开启 Websocket 服务。&lt;/p&gt;
&lt;p&gt;如果要开启 wss，需要添加证书，可以使用 nginx 代理，在 nginx 上设置证书。nginx 配置如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; # mqtt
location = /mqtt/ws {
proxy_redirect off;
proxy_pass http://localhost:8083;
proxy_set_header Host $host:$server_port;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection &amp;#34;upgrade&amp;#34;;
proxy_read_timeout 300s;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>豆子碎片小程序使用MQTT接收文章审核消息</title><link>https://blog.91demo.top/wiki/mqttvisit.html</link><pubDate>Mon, 03 Nov 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/mqttvisit.html</guid><description>&lt;p&gt;用户可以对接消息通知。集成 MQTT 后，就可以接收到 Visit 项目（豆子碎片小程序）文章审核消息。&lt;/p&gt;
&lt;p&gt;上线消息 JSON 格式如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// flag 1 在线 2 离线
{
&amp;#34;icode&amp;#34;:&amp;#34;你的icode&amp;#34;,
&amp;#34;flag&amp;#34;:1
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;审核消息 JSON 格式如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 审核消息内容为标题+审核结果
{
&amp;#34;icode&amp;#34;:&amp;#34;你的icode&amp;#34;,
&amp;#34;content&amp;#34;:&amp;#34;审核消息内容&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从豆子碎片小程序获取 MQTT 账户及地址，就可以连接到 MQTT 代理服务器。&lt;/p&gt;
&lt;p&gt;当你的 client 连接代理服务器成功后，你需要先发送一条上线通知，当你的 client 断开连接前，需要先发送一条下线通知。&lt;/p&gt;
&lt;p&gt;当你上传文章审核后，后台会推送一条审核消息内容。你有可能收到其它用户的消息。请通过 icode 来区分消息是否是自己的？&lt;/p&gt;</description></item><item><title>配置数据库源，给 Mosquitto 配置 Go Auth 插件</title><link>https://blog.91demo.top/wiki/mosquittogoauth.html</link><pubDate>Sun, 02 Nov 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/mosquittogoauth.html</guid><description>&lt;h1&gt;&lt;/h1&gt;
&lt;p&gt;Mosquitto Go Auth 是 Mosquitto MQTT 代理的身份验证和授权插件。之所以叫这个名字，是因为历史的原因，现在改名字太晚了，影响太大。&lt;/p&gt;
&lt;p&gt;Mosquitto 是一个很流行的开源 MQTT 代理。go-auth 主要使用 Go 语言编写，它使用 cgo 来调用 mosquitto 的 auth-plugin 函数。&lt;/p&gt;
&lt;p&gt;它支持并实现了以下后端：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件&lt;/li&gt;
&lt;li&gt;Postgresql 数据库&lt;/li&gt;
&lt;li&gt;Mysql 数据库&lt;/li&gt;
&lt;li&gt;Sqlite3 数据库&lt;/li&gt;
&lt;li&gt;MongoDB 数据库&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;JWT&lt;/li&gt;
&lt;li&gt;HTTP&lt;/li&gt;
&lt;li&gt;gRPC&lt;/li&gt;
&lt;li&gt;Javascript 解释器&lt;/li&gt;
&lt;li&gt;定制后端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个后端都提供用户、超级用户和 ACL 检查，并包括适当的测试。&lt;/p&gt;
&lt;p&gt;我们先来构建它，构建它需要 cgo 环境，并且需要先构建 mosquitto。我们在 Linux 主机上安装 gcc，并安装 golang 环境，golang 版本要求 1.18 以上。&lt;/p&gt;
&lt;p&gt;我们从&lt;code&gt;https://github.com/eclipse/mosquitto&lt;/code&gt;下载源码文件。然后参考说明文档编译安装 mosquitto。我使用的 CentOS7，直接通过&lt;code&gt;yum install mosquitto&lt;/code&gt;安装，安装的 mosquitto 版本为 1.6.10。所以我要下载 mosquitto1.6.10 的源代码。将代码中的 mosquitto.h，mosqutto_plugin.h 和 mosquitto_broker.h 文件复制到/usr/local/include 目录。&lt;/p&gt;
&lt;p&gt;我们从&lt;code&gt;https://github.com/iegomez/mosquitto-go-auth&lt;/code&gt;下载 go auth 的源码文件。我下载的当前版本为 2.1.0，解压缩并进入到源文件目录。输入命令&lt;code&gt;make&lt;/code&gt;，将编译一个 pw 二进制文件和一个 go-auth.so 文件。我们将 go-auth.so 文件复制到&lt;code&gt;/usr/local/lib&lt;/code&gt;目录，将 pw 二进制文件复制到&lt;code&gt;/usr/local/bin&lt;/code&gt;目录。pw 可以用来生成密码，go-auth.so 是 mosquitto 调用的库。&lt;/p&gt;</description></item><item><title>跨平台桌面开发新选择：Wails 3 初体验及在 FRP 管理客户端中的选型实践</title><link>https://blog.91demo.top/go/wails3-demo.html</link><pubDate>Sat, 01 Nov 2025 07:34:14 +0000</pubDate><guid>https://blog.91demo.top/go/wails3-demo.html</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>Mosquitto 安全加固指南：从匿名访问到多用户 ACL 细粒度权限控制</title><link>https://blog.91demo.top/devops/mosquitto_config.html</link><pubDate>Sat, 01 Nov 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/devops/mosquitto_config.html</guid><description>记录 Mosquitto 代理服务的安全配置过程，包括用户认证、动态 ACL 权限控制以及在边缘计算场景下的安全实践。</description></item><item><title>豆子碎片小程序项目第六版</title><link>https://blog.91demo.top/wiki/v6.html</link><pubDate>Wed, 03 Sep 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v6.html</guid><description>第六次进化：从“创意原型”到“稳健产品”的深度优化</description></item><item><title>豆子碎片小程序项目第五版</title><link>https://blog.91demo.top/wiki/v5.html</link><pubDate>Sun, 03 Aug 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v5.html</guid><description>第五次进化：当“技术笔记”决定做一场“闯关游戏”</description></item><item><title>豆子碎片小程序项目第四版</title><link>https://blog.91demo.top/wiki/v4.html</link><pubDate>Tue, 03 Jun 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v4.html</guid><description>豆子碎片的“第四生命”：用一台Nginx服务器夺回控制权</description></item><item><title>豆子碎片小程序项目第三版</title><link>https://blog.91demo.top/wiki/v3.html</link><pubDate>Sat, 03 May 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v3.html</guid><description>从挫折到重启，从复杂平台回归简约工具，当服务器到期后利用Git实现无后端架构的整个过程</description></item><item><title>基于 Go + 小程序实现网页端“扫码登录”实战</title><link>https://blog.91demo.top/web/qrcode_login.html</link><pubDate>Sat, 15 Mar 2025 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/web/qrcode_login.html</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>构建自定义证码登录系统及研究语音验证码实战</title><link>https://blog.91demo.top/web/vcode_login.html</link><pubDate>Tue, 11 Mar 2025 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/web/vcode_login.html</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>从单页网站开启数字创造</title><link>https://blog.91demo.top/wiki/bean_web.html</link><pubDate>Sun, 02 Mar 2025 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/wiki/bean_web.html</guid><description>&lt;p&gt;所有的宏大工程都始于最微小的表达。这一节，我们聊聊那个只有 &lt;code&gt;index.html&lt;/code&gt; 的纯真时代。本文将带你了解数字世界背后的故事。&lt;/p&gt;
&lt;h3 id="1-域名的翻译艺术"&gt;1. 域名的“翻译”艺术&lt;/h3&gt;
&lt;p&gt;服务器在网络中是以 IP 地址（如 &lt;code&gt;123.123.123.123&lt;/code&gt;）存在的。为了让人类能够记忆，我们引入了域名系统（DNS）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解析逻辑：&lt;/strong&gt; 通过配置一条 &lt;strong&gt;A 记录&lt;/strong&gt;，我们将 &lt;code&gt;91demo.top&lt;/code&gt; 映射到特定的数字 IP 上。从此，枯燥的数字拥有了可读的身份。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-安全的阶梯从-http-到-https"&gt;2. 安全的阶梯：从 HTTP 到 HTTPS&lt;/h3&gt;
&lt;p&gt;一个现代化的网站不应“裸奔”。&lt;br&gt;
使用 &lt;strong&gt;Let&amp;rsquo;s Encrypt&lt;/strong&gt; 提供的免费证书，配合 Nginx 的反向代理配置，我们为网站穿上了 TLS 加密的铠甲。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成就达成：&lt;/strong&gt; 浏览器地址栏那个小锁头的出现，标志着你已初步掌握了网络协议的基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-守门人nginx-的静默力量"&gt;3. 守门人：Nginx 的静默力量&lt;/h3&gt;
&lt;p&gt;在本项目中，没有繁琐的数据库和 API 逻辑。&lt;strong&gt;Nginx&lt;/strong&gt; 扮演了一个纯粹的“守门人”角色。&lt;br&gt;
它安静地运行在后台，监听着 80 与 443 端口，随时准备将那一个笨拙却真诚的 &lt;code&gt;index.html&lt;/code&gt; 展示给世界。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“虽然 CSS 是 AI 生成的，但我终于学会了如何让代码在服务器上跳舞。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="4-归于简单单页的本质"&gt;4. 归于简单：单页的本质&lt;/h3&gt;
&lt;p&gt;现在的 Web 世界如迷宫般复杂，但回看那个单页网站，那不仅是 HTML，更是开发者第一次触碰数字创造的本质：&lt;strong&gt;用基础的材料，构建被看见的天地。&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>在小程序中使用相册</title><link>https://blog.91demo.top/wiki/mp-image.html</link><pubDate>Mon, 03 Feb 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mp-image.html</guid><description>&lt;h1 id="小程序使用相册"&gt;小程序使用相册&lt;/h1&gt;
&lt;p&gt;微信小程序提供了丰富的相册操作 API，允许开发者访问用户的相册、拍照、选择图片、保存图片等功能。这些功能主要通过 wx.chooseMedia、wx.saveImageToPhotosAlbum 等 API 实现。&lt;/p&gt;
&lt;p&gt;下面是使用的一些示例：&lt;/p&gt;
&lt;p&gt;1，定义变量&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; data: {
// 选择的图片列表
imageList: [],
// 当前选中的图片索引
currentIndex: 0,
// 是否显示预览
showPreview: false,
// 加载状态
loading: false,
// 保存结果提示
saveResult: &amp;#39;&amp;#39;
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; onLoad() {
console.log(&amp;#39;相册页面加载&amp;#39;);
this.checkAlbumPermission();
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 检查相册权限
*/
checkAlbumPermission() {
wx.getSetting({
success: (res) =&amp;gt; {
if (!res.authSetting[&amp;#39;scope.writePhotosAlbum&amp;#39;]) {
// 如果没有相册权限，提示用户授权
this.requestAlbumPermission();
}
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 请求相册权限
*/
requestAlbumPermission() {
wx.authorize({
scope: &amp;#39;scope.writePhotosAlbum&amp;#39;,
success: () =&amp;gt; {
console.log(&amp;#39;相册权限授权成功&amp;#39;);
},
fail: () =&amp;gt; {
console.log(&amp;#39;相册权限授权失败&amp;#39;);
this.showPermissionGuide();
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 显示权限引导
*/
showPermissionGuide() {
wx.showModal({
title: &amp;#39;权限提示&amp;#39;,
content: &amp;#39;需要相册权限才能保存图片，请在设置中开启权限&amp;#39;,
confirmText: &amp;#39;去设置&amp;#39;,
success: (res) =&amp;gt; {
if (res.confirm) {
wx.openSetting({
success: (res) =&amp;gt; {
console.log(&amp;#39;打开设置成功&amp;#39;, res);
}
});
}
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 选择相册图片
*/
chooseFromAlbum() {
wx.chooseMedia({
count: 9, // 最多选择9张
mediaType: [&amp;#39;image&amp;#39;], // 只选择图片
sourceType: [&amp;#39;album&amp;#39;], // 从相册选择
maxDuration: 30,
camera: &amp;#39;back&amp;#39;,
success: (res) =&amp;gt; {
console.log(&amp;#39;选择图片成功&amp;#39;, res);
const tempFiles = res.tempFiles;
const newImageList = tempFiles.map((file, index) =&amp;gt; ({
tempFilePath: file.tempFilePath,
size: file.size,
width: file.width,
height: file.height,
thumbTempFilePath: file.thumbTempFilePath,
id: Date.now() + index, // 生成唯一ID
selected: false
}));
// 合并到现有列表
this.setData({
imageList: [...this.data.imageList, ...newImageList],
saveResult: &amp;#39;&amp;#39;
});
wx.showToast({
title: `成功选择${newImageList.length}张图片`,
icon: &amp;#39;success&amp;#39;,
duration: 2000
});
},
fail: (err) =&amp;gt; {
console.error(&amp;#39;选择图片失败&amp;#39;, err);
this.handleChooseError(err);
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 拍照并选择
*/
takePhoto() {
wx.chooseMedia({
count: 1,
mediaType: [&amp;#39;image&amp;#39;],
sourceType: [&amp;#39;camera&amp;#39;], // 从相机拍照
camera: &amp;#39;back&amp;#39;,
success: (res) =&amp;gt; {
console.log(&amp;#39;拍照成功&amp;#39;, res);
const file = res.tempFiles[0];
const newImage = {
tempFilePath: file.tempFilePath,
size: file.size,
width: file.width,
height: file.height,
thumbTempFilePath: file.thumbTempFilePath,
id: Date.now(),
selected: false,
isFromCamera: true // 标记来自相机
};
this.setData({
imageList: [newImage, ...this.data.imageList],
saveResult: &amp;#39;&amp;#39;
});
wx.showToast({
title: &amp;#39;拍照成功&amp;#39;,
icon: &amp;#39;success&amp;#39;,
duration: 2000
});
},
fail: (err) =&amp;gt; {
console.error(&amp;#39;拍照失败&amp;#39;, err);
this.handleChooseError(err);
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 处理选择错误
*/
handleChooseError(err) {
let errorMsg = &amp;#39;选择图片失败&amp;#39;;
switch (err.errMsg) {
case &amp;#39;chooseMedia:fail auth deny&amp;#39;:
errorMsg = &amp;#39;没有相册访问权限&amp;#39;;
break;
case &amp;#39;chooseMedia:fail cancel&amp;#39;:
errorMsg = &amp;#39;用户取消了选择&amp;#39;;
break;
case &amp;#39;chooseMedia:fail no media&amp;#39;:
errorMsg = &amp;#39;没有找到图片&amp;#39;;
break;
default:
errorMsg = err.errMsg || errorMsg;
}
wx.showToast({
title: errorMsg,
icon: &amp;#39;none&amp;#39;,
duration: 2000
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 预览图片
*/
previewImage(e) {
const index = e.currentTarget.dataset.index;
const urls = this.data.imageList.map(item =&amp;gt; item.tempFilePath);
wx.previewImage({
current: urls[index],
urls: urls,
success: () =&amp;gt; {
console.log(&amp;#39;预览图片成功&amp;#39;);
},
fail: (err) =&amp;gt; {
console.error(&amp;#39;预览图片失败&amp;#39;, err);
wx.showToast({
title: &amp;#39;预览失败&amp;#39;,
icon: &amp;#39;none&amp;#39;
});
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 选择/取消选择图片
*/
toggleSelect(e) {
const index = e.currentTarget.dataset.index;
const imageList = this.data.imageList.map((item, i) =&amp;gt; {
if (i === index) {
return { ...item, selected: !item.selected };
}
return item;
});
this.setData({ imageList });
},
/**
* 全选/取消全选
*/
toggleSelectAll() {
const allSelected = this.data.imageList.every(item =&amp;gt; item.selected);
const imageList = this.data.imageList.map(item =&amp;gt; ({
...item,
selected: !allSelected
}));
this.setData({ imageList });
},
/**
* 获取选中的图片
*/
getSelectedImages() {
return this.data.imageList.filter(item =&amp;gt; item.selected);
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 保存选中图片到相册
*/
saveSelectedToAlbum() {
const selectedImages = this.getSelectedImages();
if (selectedImages.length === 0) {
wx.showToast({
title: &amp;#39;请先选择图片&amp;#39;,
icon: &amp;#39;none&amp;#39;,
duration: 2000
});
return;
}
this.setData({ loading: true, saveResult: &amp;#39;&amp;#39; });
// 批量保存
this.batchSaveImages(selectedImages);
},
/**
* 批量保存图片
*/
async batchSaveImages(images) {
let successCount = 0;
let failCount = 0;
const results = [];
for (let i = 0; i &amp;lt; images.length; i++) {
try {
await this.saveSingleImage(images[i]);
successCount++;
results.push({ index: i, success: true });
} catch (error) {
failCount++;
results.push({ index: i, success: false, error: error.message });
}
// 更新进度
this.setData({
saveResult: `保存进度: ${i + 1}/${images.length}`
});
}
this.setData({ loading: false });
this.showSaveResult(successCount, failCount, results);
},
/**
* 保存单张图片
*/
saveSingleImage(image) {
return new Promise((resolve, reject) =&amp;gt; {
wx.saveImageToPhotosAlbum({
filePath: image.tempFilePath,
success: () =&amp;gt; {
console.log(&amp;#39;保存图片成功&amp;#39;, image.tempFilePath);
resolve();
},
fail: (err) =&amp;gt; {
console.error(&amp;#39;保存图片失败&amp;#39;, err);
reject(new Error(this.getSaveErrorMsg(err)));
}
});
});
},
/**
* 获取保存错误信息
*/
getSaveErrorMsg(err) {
switch (err.errMsg) {
case &amp;#39;saveImageToPhotosAlbum:fail auth deny&amp;#39;:
return &amp;#39;没有相册写入权限&amp;#39;;
case &amp;#39;saveImageToPhotosAlbum:fail cancel&amp;#39;:
return &amp;#39;用户取消了保存&amp;#39;;
case &amp;#39;saveImageToPhotosAlbum:fail no image&amp;#39;:
return &amp;#39;图片文件不存在&amp;#39;;
default:
return err.errMsg || &amp;#39;保存失败&amp;#39;;
}
},
/**
* 显示保存结果
*/
showSaveResult(successCount, failCount, results) {
let title = &amp;#39;&amp;#39;;
if (failCount === 0) {
title = `成功保存${successCount}张图片`;
} else if (successCount === 0) {
title = `保存失败，${failCount}张图片未保存`;
} else {
title = `成功${successCount}张，失败${failCount}张`;
}
this.setData({
saveResult: title
});
wx.showModal({
title: &amp;#39;保存结果&amp;#39;,
content: title,
showCancel: false,
success: () =&amp;gt; {
// 清空选择状态
const imageList = this.data.imageList.map(item =&amp;gt; ({
...item,
selected: false
}));
this.setData({ imageList });
}
});
// 记录详细结果
console.log(&amp;#39;保存详细结果:&amp;#39;, results);
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 压缩图片
*/
compressImage(imagePath) {
return new Promise((resolve, reject) =&amp;gt; {
wx.compressImage({
src: imagePath,
quality: 80, // 压缩质量 0-100
success: (res) =&amp;gt; {
console.log(&amp;#39;图片压缩成功&amp;#39;, res);
resolve(res.tempFilePath);
},
fail: (err) =&amp;gt; {
console.error(&amp;#39;图片压缩失败&amp;#39;, err);
reject(err);
}
});
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
/**
* 获取图片信息
*/
getImageInfo(imagePath) {
return new Promise((resolve, reject) =&amp;gt; {
wx.getImageInfo({
src: imagePath,
success: (res) =&amp;gt; {
console.log(&amp;#39;图片信息获取成功&amp;#39;, res);
resolve(res);
},
fail: (err) =&amp;gt; {
console.error(&amp;#39;图片信息获取失败&amp;#39;, err);
reject(err);
}
});
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 批量压缩并保存
*/
async compressAndSaveSelected() {
const selectedImages = this.getSelectedImages();
if (selectedImages.length === 0) {
wx.showToast({
title: &amp;#39;请先选择图片&amp;#39;,
icon: &amp;#39;none&amp;#39;
});
return;
}
this.setData({ loading: true });
try {
// 先压缩所有图片
const compressedPaths = [];
for (const image of selectedImages) {
const compressedPath = await this.compressImage(image.tempFilePath);
compressedPaths.push(compressedPath);
}
// 保存压缩后的图片
for (let i = 0; i &amp;lt; compressedPaths.length; i++) {
await this.saveSingleImage({ tempFilePath: compressedPaths[i] });
this.setData({
saveResult: `压缩保存进度: ${i + 1}/${compressedPaths.length}`
});
}
this.setData({ loading: false });
this.showSaveResult(compressedPaths.length, 0, []);
} catch (error) {
this.setData({ loading: false });
wx.showToast({
title: &amp;#39;压缩保存失败&amp;#39;,
icon: &amp;#39;none&amp;#39;
});
}
},
/**
* 删除选中图片
*/
deleteSelected() {
const selectedImages = this.getSelectedImages();
if (selectedImages.length === 0) {
wx.showToast({
title: &amp;#39;请先选择要删除的图片&amp;#39;,
icon: &amp;#39;none&amp;#39;
});
return;
}
wx.showModal({
title: &amp;#39;确认删除&amp;#39;,
content: `确定要删除选中的${selectedImages.length}张图片吗？`,
success: (res) =&amp;gt; {
if (res.confirm) {
const remainingImages = this.data.imageList.filter(item =&amp;gt; !item.selected);
this.setData({
imageList: remainingImages,
saveResult: `已删除${selectedImages.length}张图片`
});
wx.showToast({
title: &amp;#39;删除成功&amp;#39;,
icon: &amp;#39;success&amp;#39;
});
}
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
/**
* 清空所有图片
*/
clearAllImages() {
if (this.data.imageList.length === 0) {
wx.showToast({
title: &amp;#39;没有图片可清空&amp;#39;,
icon: &amp;#39;none&amp;#39;
});
return;
}
wx.showModal({
title: &amp;#39;确认清空&amp;#39;,
content: &amp;#39;确定要清空所有图片吗？&amp;#39;,
success: (res) =&amp;gt; {
if (res.confirm) {
this.setData({
imageList: [],
saveResult: &amp;#39;已清空所有图片&amp;#39;
});
wx.showToast({
title: &amp;#39;清空成功&amp;#39;,
icon: &amp;#39;success&amp;#39;
});
}
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt; /**
* 分享图片
*/
onShareAppMessage() {
const selectedImages = this.getSelectedImages();
if (selectedImages.length &amp;gt; 0) {
return {
title: `分享${selectedImages.length}张图片`,
path: &amp;#39;/pages/album/album&amp;#39;,
imageUrl: selectedImages[0].thumbTempFilePath
};
}
return {
title: &amp;#39;分享我的相册&amp;#39;,
path: &amp;#39;/pages/album/album&amp;#39;
};
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
console.log(&amp;#39;下拉刷新&amp;#39;);
// 模拟刷新操作
setTimeout(() =&amp;gt; {
this.setData({
saveResult: &amp;#39;刷新完成&amp;#39;
});
wx.stopPullDownRefresh();
wx.showToast({
title: &amp;#39;刷新成功&amp;#39;,
icon: &amp;#39;success&amp;#39;
});
}, 1000);
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onReachBottom() {
console.log(&amp;#39;上拉加载更多&amp;#39;);
// 可以在这里实现加载更多图片的逻辑
},
/**
* 用户点击右上角分享
*/
onShareTimeline() {
return {
title: &amp;#39;我的小程序相册&amp;#39;,
imageUrl: &amp;#39;/images/share.jpg&amp;#39;
};
},
/**
* 错误处理统一函数
*/
handleError(error, operation = &amp;#39;操作&amp;#39;) {
console.error(`${operation}失败:`, error);
wx.showToast({
title: `${operation}失败`,
icon: &amp;#39;none&amp;#39;,
duration: 2000
});
},
/**
* 性能优化：图片懒加载
*/
onImageLoad(e) {
const index = e.currentTarget.dataset.index;
console.log(`图片${index}加载完成`);
// 可以在这里添加图片加载完成的处理逻辑
},
/**
* 性能优化：图片加载失败
*/
onImageError(e) {
const index = e.currentTarget.dataset.index;
console.error(`图片${index}加载失败`);
// 可以在这里设置默认图片或重试逻辑
}
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这是一个 WXML 文件：&lt;/p&gt;</description></item><item><title>高效文件处理工具 yq</title><link>https://blog.91demo.top/wiki/yqtool.html</link><pubDate>Sat, 01 Feb 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/yqtool.html</guid><description>&lt;p&gt;在处理小程序文章索引 JSON 文件时，发现了这个好工具 yq，特分享给大家。&lt;/p&gt;
&lt;p&gt;yq 是一个轻量级、命令行驱动的文件处理工具，专门用于处理 YAML、JSON、XML 等结构化数据格式。它类似于著名的 jq（专门处理 JSON 的工具），但扩展了对 YAML 的原生支持，并能够处理多种文件格式之间的转换。&lt;/p&gt;
&lt;p&gt;yq 采用 Go 语言编写，具有跨平台特性，可以在 Linux、macOS 和 Windows 系统上运行。它的核心功能包括查询、过滤、修改和转换结构化数据文件。&lt;/p&gt;
&lt;p&gt;yq 在大多数 Linux 发行版可以通过包管理器安装：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Ubuntu/Debian
sudo apt install yq
# CentOS/RHEL
sudo yum install yq
# macOS (使用 Homebrew)
brew install yq
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;或者从 Github 发布页面下载预编译的二进制文件。&lt;/p&gt;
&lt;p&gt;yq 常用于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;配置文件处理，例如在 DevOps 和云原生环境中处理 YAML 等配置文件。&lt;/li&gt;
&lt;li&gt;数据转换，在不同格式（YAML/JSON/XML/CSV）之间进行转换&lt;/li&gt;
&lt;li&gt;数据提取，从复杂数据结构中提取特定字段或值&lt;/li&gt;
&lt;li&gt;批量修改，对多个文件中的特定字段进行统一修改&lt;/li&gt;
&lt;li&gt;自动化脚本，在 shell 脚本中处理结构化数据&lt;/li&gt;
&lt;li&gt;数据验证，检查配置文件是否符合特定结构或值要求&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是一些使用示例：&lt;/p&gt;
&lt;p&gt;1，查询 YAML 文件&lt;/p&gt;
&lt;p&gt;假设我们有一个 config.yaml 文件：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server:
port: 8080
host: &amp;#34;localhost&amp;#34;
database:
name: &amp;#34;test_db&amp;#34;
user: &amp;#34;admin&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们要获取服务器端口：&lt;/p&gt;</description></item><item><title>豆子碎片小程序项目第二版</title><link>https://blog.91demo.top/wiki/v2.html</link><pubDate>Fri, 03 Jan 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v2.html</guid><description>从“技术自留地”到“价值循环生态”：我的“豆子碎片”平台构想全记录</description></item><item><title>ESP8266 工业级配网实战：基于 SoftAP 的可靠 WIFI 动态配置方案</title><link>https://blog.91demo.top/embedded/esp-conf.html</link><pubDate>Thu, 02 Jan 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/embedded/esp-conf.html</guid><description>记录如何在ESP8266/ESP32芯片上配置路由器的WIFI账号和密码，包含了嵌入式代码和小程序代码。</description></item><item><title> Netstat 常用命令</title><link>https://blog.91demo.top/wiki/netstat.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/netstat.html</guid><description>&lt;p&gt;查询本机 IP 连接&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -an
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看网络 TCP 连接状态数量&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -n | awk &amp;#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看 8080 端口有多少个 TCP 连接&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -ant |grep 8080|wc -l
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看当前 TCP 连接状态为 ESTABLISHED 的数量&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -ant|grep 8080|grep ESTABLISHED|wc -l
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看 TCP 连接&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -ant
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;过滤某个 TCP 端口&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -ant |grep 端口号
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看 UDP 连接&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -anu
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;过滤某个 UDP 端口&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -anu |grep 端口号
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实时监控 UDP 连接信息，每隔 2 秒自动更新一次&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;watch -n 2 &amp;#39;netstat -anu&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用 ss 或 netstat 关联进程和端口&lt;/p&gt;</description></item><item><title> Asterisk 常见错误</title><link>https://blog.91demo.top/wiki/asterisk.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/asterisk.html</guid><description>&lt;p&gt;Asterisk 是第一套以开放源代码软件实现的 用户交换机 (PBX) 系统。Asterisk 由 Digium 的创办人马克·史宾瑟（Mark Spencer）于 1999 年他还在奥本大学念书时所开发。与其他的用户交换机系统相同，Asterisk 同样支持电话拨打另一只分机，和拨打到公共交换电话网与 IP 电话系统。Asterisk 这个名称源自于星号 &amp;ldquo;*&amp;quot;。&lt;/p&gt;
&lt;p&gt;网址：https://www.asterisk.org/&lt;/p&gt;
&lt;h2 id="asterisk-常见错误"&gt;Asterisk 常见错误&lt;/h2&gt;
&lt;p&gt;403 登录失败&lt;/p&gt;
&lt;p&gt;解决：等待 1 分钟，等超时之后重新连接即可。一般是切换 IP 时或者换软电话登录会碰到。&lt;/p&gt;
&lt;p&gt;401 登录失败&lt;/p&gt;
&lt;p&gt;解决：检查账户和密码是否正确？检查连接地址是否正确？检查 Asterisk 是否启动？检查防火墙端口是否打开？&lt;/p&gt;
&lt;p&gt;408 连接超时&lt;/p&gt;
&lt;p&gt;解决：一般是 Asterisk 服务不通，检查 Asterisk 服务是否启动，检查防火墙是否打开？&lt;/p&gt;
&lt;p&gt;可以拨通，没有声音？&lt;/p&gt;
&lt;p&gt;一般是 NAT 造成，配置这三个参数：rtp_symmetric，force_rport，rewrite_contact。&lt;/p&gt;
&lt;p&gt;SIP 客户端没有自动挂机？&lt;/p&gt;
&lt;p&gt;一般是没有设置 stun 造成的，在 SIP 客户端，设置 stun 即可。如果没有 stun server，可以设置这个&lt;code&gt;stun.l.google.com:19302&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;AGI 脚本返回状态 4，正常应该为 0？&lt;/p&gt;
&lt;p&gt;查看网上资料，是 AGI 脚本中调用 Hangup 导致，将脚本中的 Hangup 去掉，放在拨号计划配置文件中执行 Hangup，可以解决这个问题。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AGI Script agidemo completed, returning 0
&lt;/code&gt;&lt;/pre&gt;&lt;h1 id="解决-asterisk-没有声音的问题"&gt;解决 asterisk 没有声音的问题&lt;/h1&gt;
&lt;p&gt;在配置好 asterisk 之后，拨打 8000 没有听到声音。&lt;/p&gt;</description></item><item><title> Bash 常用命令</title><link>https://blog.91demo.top/wiki/bash.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/bash.html</guid><description>&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ps aux | grep soffice | grep -v grep | awk &lt;span style="color:#e6db74"&gt;&amp;#39;{print $6/1024 &amp;#34; MB&amp;#34;}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="查看日志"&gt;查看日志&lt;/h2&gt;
&lt;p&gt;查询 IP 信息，查找某条请求记录，然后打印出 IP 那一列，进行排序，再去重。可以得出请求的 IP 信息。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat golog/20240905_ad.log |grep &amp;#39;ad&amp;#39;|awk &amp;#39;{print $9}&amp;#39;|sort|uniq
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从 IP 归属地库查询省份城市信息&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat ip.merge.txt |grep &amp;#39;河南省&amp;#39;|awk -v FS=&amp;#34;|&amp;#34; &amp;#39;{print $5}&amp;#39;|sort|uniq
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将行记录变成一行，并以逗号分割。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat ip.merge.txt |grep &amp;#39;河南省&amp;#39;|awk -v FS=&amp;#34;|&amp;#34; &amp;#39;{print $5}&amp;#39;|sort|uniq|paste -s -d &amp;#34;,&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="awk"&gt;Awk&lt;/h2&gt;
&lt;p&gt;awk 是一种处理文本文件的语言，是一个强大的文本分析工具。&lt;/p&gt;
&lt;p&gt;使用分隔符指定列：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;awk -F&amp;#39;,&amp;#39; &amp;#39;{print $1, $2}&amp;#39; file
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;打印满足条件的行数：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;awk &amp;#39;/pattern/ {print NR, $0}&amp;#39; file
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="scp-正则匹配复制文件"&gt;SCP 正则匹配复制文件&lt;/h2&gt;
&lt;p&gt;scp 可以使用正则复制文件，一次复制多个文件&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;scp -P 2222 \*.mp4 root@host:/data/upfile/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;scp 还可以复制文件夹，使用-r 属性，递归复制，例如下面的格式：&lt;/p&gt;</description></item><item><title> Curl 用法</title><link>https://blog.91demo.top/wiki/curl.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/curl.html</guid><description>&lt;p&gt;cURL（Client URL）是一个功能强大的命令行工具，用于通过 URL 传输数据，支持多种协议（如 HTTP/HTTPS、FTP、SFTP、SCP 等）。它广泛用于 API 测试、文件传输、数据提交等场景。&lt;/p&gt;
&lt;h2 id="安装"&gt;安装&lt;/h2&gt;
&lt;h3 id="linux-debianubuntu"&gt;Linux (Debian/Ubuntu)&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt-get install curl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="macos"&gt;macOS&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;brew install curl &lt;span style="color:#75715e"&gt;# 通过Homebrew安装/更新&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="windows"&gt;Windows&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;从&lt;a href="https://curl.se/windows/"&gt;官网下载&lt;/a&gt;二进制文件&lt;/li&gt;
&lt;li&gt;或使用 Chocolatey：
&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-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;choco install curl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="基本用法"&gt;基本用法&lt;/h2&gt;
&lt;h3 id="发起-get-请求"&gt;发起 GET 请求&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl https://example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="保存响应到文件"&gt;保存响应到文件&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -o custom_filename.html https://example.com &lt;span style="color:#75715e"&gt;# 自定义文件名&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -O https://example.com/file.zip &lt;span style="color:#75715e"&gt;# 使用远程文件名&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="常用选项"&gt;常用选项&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示详细日志（请求头、响应头、SSL 信息）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自动跟随重定向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;静默模式（隐藏进度条和错误信息）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示响应头 + 响应体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅显示响应头（发送 HEAD 请求）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跳过 SSL 证书验证（不安全，谨慎使用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置 User-Agent，如 &lt;code&gt;-A &amp;quot;Mozilla/5.0&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="http-方法"&gt;HTTP 方法&lt;/h2&gt;
&lt;h3 id="post-请求表单数据"&gt;POST 请求（表单数据）&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -X POST https://api.example.com/data -d &lt;span style="color:#e6db74"&gt;&amp;#34;name=John&amp;amp;age=30&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="post-请求json-数据"&gt;POST 请求（JSON 数据）&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -X POST -H &lt;span style="color:#e6db74"&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;John&amp;#34;, &amp;#34;age&amp;#34;:30}&amp;#39;&lt;/span&gt; https://api.example.com/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="putdelete-请求"&gt;PUT/DELETE 请求&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -X PUT https://api.example.com/item/1 -d &lt;span style="color:#e6db74"&gt;&amp;#34;data=example&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -X DELETE https://api.example.com/item/1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="处理-http-头"&gt;处理 HTTP 头&lt;/h2&gt;
&lt;h3 id="查看响应头"&gt;查看响应头&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -I https://example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="发送自定义头"&gt;发送自定义头&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -H &lt;span style="color:#e6db74"&gt;&amp;#34;Authorization: Bearer token123&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;X-Custom-Header: value&amp;#34;&lt;/span&gt; https://api.example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="文件传输"&gt;文件传输&lt;/h2&gt;
&lt;h3 id="上传文件表单"&gt;上传文件（表单）&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -F &lt;span style="color:#e6db74"&gt;&amp;#34;file=@/path/to/file.txt&amp;#34;&lt;/span&gt; https://example.com/upload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="上传文件ftp"&gt;上传文件（FTP）&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -T file.txt ftp://example.com/upload/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="断点续传"&gt;断点续传&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -C - -O https://example.com/large-file.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="认证"&gt;认证&lt;/h2&gt;
&lt;h3 id="基本认证basic-auth"&gt;基本认证（Basic Auth）&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -u username:password https://example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="cookie-管理"&gt;Cookie 管理&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -c cookies.txt https://example.com/login &lt;span style="color:#75715e"&gt;# 保存Cookie&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -b cookies.txt https://example.com/dashboard &lt;span style="color:#75715e"&gt;# 发送Cookie&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="高级功能"&gt;高级功能&lt;/h2&gt;
&lt;h3 id="限速下载100kbs"&gt;限速下载（100KB/s）&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl --limit-rate 100K -O https://example.com/large-file.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="使用代理"&gt;使用代理&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -x http://proxy-server:8080 https://example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="调试请求"&gt;调试请求&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl --trace-ascii debug.txt https://example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="实际示例"&gt;实际示例&lt;/h2&gt;
&lt;h3 id="下载-github-仓库"&gt;下载 GitHub 仓库&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -L -O https://github.com/user/repo/archive/master.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="模拟浏览器访问"&gt;模拟浏览器访问&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -A &lt;span style="color:#e6db74"&gt;&amp;#34;Mozilla/5.0&amp;#34;&lt;/span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Accept-Language: en-US&amp;#34;&lt;/span&gt; https://example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="调试-https-请求"&gt;调试 HTTPS 请求&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -v -k https://example.com &lt;span style="color:#75715e"&gt;# 查看SSL握手过程（忽略证书错误）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="注意事项"&gt;注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;敏感数据&lt;/strong&gt;：避免在命令行中直接暴露密码，推荐使用&lt;code&gt;--netrc&lt;/code&gt;或环境变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 安全&lt;/strong&gt;：生产环境中尽量不使用&lt;code&gt;-k&lt;/code&gt;（跳过证书验证）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命令顺序&lt;/strong&gt;：组合选项时注意顺序（如&lt;code&gt;-L -O&lt;/code&gt;需先重定向再保存文件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;速率限制&lt;/strong&gt;：使用&lt;code&gt;--limit-rate&lt;/code&gt;避免占用过多带宽。&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title> Docker 常用命令</title><link>https://blog.91demo.top/wiki/docker.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/docker.html</guid><description>&lt;h1 id="docker"&gt;DOCKER&lt;/h1&gt;
&lt;p&gt;Docker 是一个开源的应用容器引擎，基于 Go 语言 并遵从 Apache2.0 协议开源。&lt;/p&gt;
&lt;p&gt;Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中，然后发布到任何流行的 Linux 机器上，也可以实现虚拟化。&lt;/p&gt;
&lt;p&gt;容器是完全使用沙箱机制，相互之间不会有任何接口（类似 iPhone 的 app）,更重要的是容器性能开销极低。&lt;/p&gt;
&lt;p&gt;Docker 网址：https://www.docker.com/&lt;/p&gt;
&lt;h2 id="docker-常用命令"&gt;Docker 常用命令：&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker init # Creates Docker-related starter files
docker build -t friendlyname . # Create image using this directory&amp;#39;s Dockerfile
docker run -p 4000:80 friendlyname # Run &amp;#34;friendlyname&amp;#34; mapping port 4000 to 80
docker run -d -p 4000:80 friendlyname # Same thing, but in detached mode
docker exec -it [container-id] bash # Enter a running container
docker ps # See a list of all running containers
docker stop &amp;lt;hash&amp;gt; # Gracefully stop the specified container
docker ps -a # See a list of all containers, even the ones not running
docker kill &amp;lt;hash&amp;gt; # Force shutdown of the specified container
docker rm &amp;lt;hash&amp;gt; # Remove the specified container from this machine
docker rm -f &amp;lt;hash&amp;gt; # Remove force specified container from this machine
docker rm $(docker ps -a -q) # Remove all containers from this machine
docker images -a # Show all images on this machine
docker rmi &amp;lt;imagename&amp;gt; # Remove the specified image from this machine
docker rmi $(docker images -q) # Remove all images from this machine
docker logs &amp;lt;container-id&amp;gt; -f # Live tail a container&amp;#39;s logs
docker login # Log in this CLI session using your Docker credentials
docker tag &amp;lt;image&amp;gt; username/repository:tag # Tag &amp;lt;image&amp;gt; for upload to registry
docker push username/repository:tag # Upload tagged image to registry
docker run username/repository:tag # Run image from a registry
docker system prune # Remove all unused containers, networks, images (both dangling and unreferenced), and optionally, volumes. (Docker 17.06.1-ce and superior)
docker system prune -a # Remove all unused containers, networks, images not just dangling ones (Docker 17.06.1-ce and superior)
docker volume prune # Remove all unused local volumes
docker network prune # Remove all unused networks
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="docker-compose"&gt;DOCKER COMPOSE&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker-compose up # Create and start containers
docker-compose up -d # Create and start containers in detached mode
docker-compose down # Stop and remove containers, networks, images, and volumes
docker-compose logs # View output from containers
docker-compose restart # Restart all service
docker-compose pull # Pull all image service
docker-compose build # Build all image service
docker-compose config # Validate and view the Compose file
docker-compose scale &amp;lt;service_name&amp;gt;=&amp;lt;replica&amp;gt; # Scale special service(s)
docker-compose top # Display the running processes
docker-compose run -rm -p 2022:22 web bash # Start web service and runs bash as its command, remove old container.
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="docker-services"&gt;DOCKER SERVICES&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker service create &amp;lt;options&amp;gt; &amp;lt;image&amp;gt; &amp;lt;command&amp;gt; # Create new service
docker service inspect --pretty &amp;lt;service_name&amp;gt; # Display detailed information Service(s)
docker service ls # List Services
docker service ps # List the tasks of Services
docker service scale &amp;lt;service_name&amp;gt;=&amp;lt;replica&amp;gt; # Scale special service(s)
docker service update &amp;lt;options&amp;gt; &amp;lt;service_name&amp;gt; # Update Service options
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="docker-stack"&gt;DOCKER STACK&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker stack ls # List all running applications on this Docker host
docker stack deploy -c &amp;lt;composefile&amp;gt; &amp;lt;appname&amp;gt; # Run the specified Compose file
docker stack services &amp;lt;appname&amp;gt; # List the services associated with an app
docker stack ps &amp;lt;appname&amp;gt; # List the running containers associated with an app
docker stack rm &amp;lt;appname&amp;gt; # Tear down an application
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="docker-machine"&gt;DOCKER MACHINE&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker-machine create --driver virtualbox myvm1 # Create a VM (Mac, Win7, Linux)
docker-machine create -d hyperv --hyperv-virtual-switch &amp;#34;myswitch&amp;#34; myvm1 # Win10
docker-machine env myvm1 # View basic information about your node
docker-machine ssh myvm1 &amp;#34;docker node ls&amp;#34; # List the nodes in your swarm
docker-machine ssh myvm1 &amp;#34;docker node inspect &amp;lt;node ID&amp;gt;&amp;#34; # Inspect a node
docker-machine ssh myvm1 &amp;#34;docker swarm join-token -q worker&amp;#34; # View join token
docker-machine ssh myvm1 # Open an SSH session with the VM; type &amp;#34;exit&amp;#34; to end
docker-machine ssh myvm2 &amp;#34;docker swarm leave&amp;#34; # Make the worker leave the swarm
docker-machine ssh myvm1 &amp;#34;docker swarm leave -f&amp;#34; # Make master leave, kill swarm
docker-machine start myvm1 # Start a VM that is currently not running
docker-machine stop $(docker-machine ls -q) # Stop all running VMs
docker-machine rm $(docker-machine ls -q) # Delete all VMs and their disk images
docker-machine scp docker-compose.yml myvm1:~ # Copy file to node&amp;#39;s home dir
docker-machine ssh myvm1 &amp;#34;docker stack deploy -c &amp;lt;file&amp;gt; &amp;lt;app&amp;gt;&amp;#34; # Deploy an app
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title> Firewall-Cmd 常用命令</title><link>https://blog.91demo.top/wiki/firewall.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/firewall.html</guid><description>&lt;p&gt;firewall 是 Linux CentOS 等操作系统的防火墙。&lt;/p&gt;
&lt;p&gt;使用 rich rule 封禁 IP&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;firewall-cmd --permanent --add-rich-rule=&amp;#34;rule family=&amp;#39;ipv4&amp;#39; source address=&amp;#39;222.222.222.222&amp;#39; reject&amp;#34; 单个IP
firewall-cmd --permanent --add-rich-rule=&amp;#34;rule family=&amp;#39;ipv4&amp;#39; source address=&amp;#39;222.222.222.0/24&amp;#39; reject&amp;#34; IP段
firewall-cmd --permanent --add-rich-rule=&amp;#34;rule family=ipv4 source address=192.168.1.2 port port=80 protocol=tcp accept&amp;#34; 单个IP的某个端口
firewall-cmd --list-rich-rules 查看封禁IP
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用 ip set 封禁 IP&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;firewall-cmd --permanent --new-ipset=dog --type=hash:ip 封禁IP
firewall-cmd --permanent --ipset=dog --add-entry=ip地址
firewall-cmd --permanent --new-ipset=blacklist --type=hash:net 封禁网段
firewall-cmd --permanent --ipset=blacklist --add-entry=222.222.222.0/24
firewall-cmd --permanent --add-rich-rule=&amp;#39;rule source ipset=blacklist drop&amp;#39; 使ipset生效
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;删除一个 ipset&lt;/p&gt;</description></item><item><title> Mdbook 常用命令</title><link>https://blog.91demo.top/wiki/mdbook.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mdbook.html</guid><description>&lt;p&gt;mdBook 是一个工具，可以将 Markdown 文件呈现为更适合 HTML 或 EPUB 等最终用户形式。&lt;/p&gt;
&lt;p&gt;mdbook 使用步骤：&lt;br&gt;
1， 编写 markdown 格式的文章内容&lt;br&gt;
2， 整理 SUMMARY.md 文件&lt;br&gt;
3， 使用 mdbook 浏览和编译&lt;br&gt;
4， 上传编译后的书籍内容到 Web 服务器&lt;/p&gt;
&lt;p&gt;mdbook 使用新的端口开发：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mdbook serve -p 13000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将文章内容编译为书籍，默认存放在项目目录的 book 文件夹下。可以将该目录上传到 Web 服务器，然后启动 Web 服务器就可以访问书籍了。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mdbook build
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="集成-google-统计"&gt;集成 google 统计&lt;/h2&gt;
&lt;p&gt;一番搜索后，找到可以集成 google 统计，并且这个功能在 7 年前已经有了，但是网上没有一篇文章介绍这个。经过查看源码，现将集成 google 统计的步骤记录下来。&lt;/p&gt;
&lt;p&gt;第一阶段，是在[output.html]配置中直接配置 google 统计，例如在 book.toml 中下面的配置&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[output.html]
default-theme = &amp;#34;rust&amp;#34;
google-analytics = &amp;#34;XXXXXXX&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当使用这种配置后，mdbook 编译会输出警告，这个 google 统计在将来的版本中会删除，推荐放在 theme 中的 head.hbs 文件中。&lt;/p&gt;
&lt;p&gt;首先，在项目目录下创建 mytheme 文件夹，然后放入 head.hbs 文件。文件的内容，就是 google 统计的代码，例如：&lt;/p&gt;</description></item><item><title> Mosquitto安装与配置</title><link>https://blog.91demo.top/wiki/mosquitto.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mosquitto.html</guid><description>&lt;p&gt;MQTT（Message Queuing Telemetry Transport）是一种轻量级、基于发布-订阅模式的消息传输协议，适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。它在物联网应用中广受欢迎，能够实现传感器、执行器和其它设备之间的高效通信。&lt;/p&gt;
&lt;p&gt;Eclipse Mosquitto 是一个开源（EPL/EDL 许可）消息代理，实现了 MQTT 协议版本 5.0、3.1.1 和 3.1。Mosquitto 重量轻，适用于从低功耗单板计算机到全服务器的所有设备。&lt;/p&gt;
&lt;h2 id="在-centos-源码安装-mosquitto"&gt;在 CentOS 源码安装 mosquitto&lt;/h2&gt;
&lt;p&gt;1，从 &lt;a href="https://mosquitto.org/download/"&gt;https://mosquitto.org/download/&lt;/a&gt; 下载源码到 Linux 服务器。&lt;/p&gt;
&lt;p&gt;2，解压缩后，编译安装&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;make
make install
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在编译时，提示 cJSON 找不动，先安装 cJSON。&lt;br&gt;
从该地址下载：https://github.com/DaveGamble/cJSON&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;make
make install
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;3，配置 mosquitto，在/etc/mosquitto 目录下，将 mosquitto.conf.example 改为 mosquitto.conf。&lt;br&gt;
将以下内容修改：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# 端口
listener 1883
# 日志
log_dest file /var/log/mosquitto/mosquitto.log
log_type warning
connection_messages true
log_timestamp true
log_timestamp_format %Y-%m-%dT%H:%M:%S
# 安全
allow_anonymous false
password_file /etc/mosquitto/pwfile
acl_file /etc/mosquitto/aclfile
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里，浪费了我很长时间，在添加 password_file 之后，服务无法启动。后经排查，是权限的问题。&lt;br&gt;
我使用的 root 账户，这是最终的修复脚本：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;chmod 0700 /etc/mosquitto/pwfile
chown mosquitto:mosquitto /etc/mosquitto/pwfile
chmod 0700 /etc/mosquitto/aclfile
chown mosquitto:mosquitto /etc/mosquitto/aclfile
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;pwfile 可以使用 mosquitto_passwd 命令创建，脚本如下：&lt;/p&gt;</description></item><item><title> Sqlite 常用命令</title><link>https://blog.91demo.top/wiki/sqlite3.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/sqlite3.html</guid><description>&lt;p&gt;SQLite 是一个软件库，实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。常用在单机模式或嵌入式模式下。&lt;/p&gt;
&lt;p&gt;官网地址：https://www.sqlite.org/&lt;/p&gt;
&lt;h2 id="只导出数据库表结构"&gt;只导出数据库表结构&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sqlite3 DATABASE_FILE.sqlite &amp;#39;.schema&amp;#39; &amp;gt; schema.sql
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="导出所有内容"&gt;导出所有内容&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sqlite3 DATABASE_FILE.sqlite &amp;#39;.dump&amp;#39; &amp;gt; dump.sql
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="导出-某张表内容"&gt;导出 某张表内容&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sqlite3 DATABASE_FILE.sqlite &amp;#39;.dump demotable1&amp;#39; &amp;gt; dump.sql
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="将一个表中的数据导入到另一张表中"&gt;将一个表中的数据导入到另一张表中。&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;insert into v_arts(uuid,openid,title,keyword,views,status,ispub,islock,createtime,updatetime) select uuid,openid,title,&amp;#39;&amp;#39; as keyword,views,status,1 as ispub,0 as islock,createtime,updatetime from blog_articles;
insert into v_artconts(uuid,content) select uuid,content from blog_article_contents;
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>FFmpeg 常用命令</title><link>https://blog.91demo.top/wiki/ffmpeg.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/ffmpeg.html</guid><description>&lt;p&gt;一个完整的跨平台解决方案，用于录制、转换和流式传输音频和视频。&lt;/p&gt;
&lt;p&gt;网址：https://www.ffmpeg.org/&lt;/p&gt;
&lt;p&gt;将视频转换为音频文件：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffmpeg -i input.mp4 output.avi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;m4a 文件转换为 mp3 文件：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffmpeg -i input.m4a -c:v copy -c:a libmp3lame -q:a 2 output.mp3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;mp3 转 g711a 命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用 ffplay 播放测试&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffplay -i test.alaw -f alaw -ac 1 -ar 8000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;mp3 转 gsm 命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffmpeg -i test.mp3 -ar 8000 -ac 1 test.gsm
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;WAV 转 MP3 的 FFMPEG 命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; ffmpeg -i input.wav -codec:a libmp3lame -qscale:a 2 output.mp3
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>HTTP并发同一时间请求了两次</title><link>https://blog.91demo.top/wiki/twicehttp.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/twicehttp.html</guid><description>&lt;p&gt;在 APP 和小程序网络请求中，都会出现同一时间并发请求两次的情况，以前没有碰到，不代表没有发生，现在在用户登陆后，添加了赠送豆子点数的逻辑，在查看记录的时候，发现了此问题。会出现同一用户在同一时间登陆两次的请求。不知道造成会同时请求两次的问题的根本原因是什么？但需要解决赠送两次的问题。&lt;/p&gt;
&lt;p&gt;经过排查，在用户同时请求两次时，没有加锁，导致判断添加一直有效。最简单的方法是加锁。在 golang 中，锁是 sync.Mutex。加锁会影响一点点性能。&lt;/p&gt;
&lt;p&gt;在加锁后，此问题得到解决。代码片段：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var lock sync.Mutex
var beanMap map[string]int64
lock.Lock()
defer lock.UnLock()
val,ok:=map[openid] // 查看是否已赠送？
if ok {
// 已赠送，可继续业务逻辑
}else{
// 还未赠送
// 赠送逻辑
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Iperf3 常用命令</title><link>https://blog.91demo.top/wiki/iperf3.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/iperf3.html</guid><description>&lt;p&gt;Iperf3 是一个网络性能测试工具。Iperf 可以测试最大 TCP 和 UDP 带宽性能，具有多种参数和 UDP 特性，可以根据需要调整，可以报告带宽、延迟抖动和数据包丢失.对于每个测试，它都会报告带宽，丢包和其他参数，可在 Windows、Mac OS X、Linux、FreeBSD 等各种平台使用，是一个简单又实用的小工具。&lt;/p&gt;
&lt;h2 id="安装"&gt;安装&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;yum install iperf3
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="使用"&gt;使用&lt;/h2&gt;
&lt;p&gt;网络带宽测试&lt;/p&gt;
&lt;p&gt;Iperf3 是 C/S(客户端/服务器端)架构模式，在使用 iperf3 测试时，要同时在 server 端与 client 端都各执行一个程序，让它们互相传送报文进行测试。&lt;/p&gt;
&lt;p&gt;启动 Server 端的程序：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;iperf3 -s
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;启动 Client 端的程序：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;iperf3 -c 服务端IP地址
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从打印内容可以查看网络带宽等参数。&lt;/p&gt;
&lt;p&gt;更多内容请查看命令手册。&lt;/p&gt;</description></item><item><title>Linux Debian系设置固定 IP</title><link>https://blog.91demo.top/wiki/setip.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/setip.html</guid><description>&lt;p&gt;1，Ubuntu18.04 设置固定 IP&lt;br&gt;
修改/etc/netplan 下的 yaml 文件，修改为&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;network:
ethernets:
eth0:
addresses: - 172.28.45.220/20
nameservers:
addresses: [202.102.224.68,202.102.227.68]
dhcp4: false
version: 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后执行&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo netplan apply
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用 ip a 查看地址是否变更&lt;br&gt;
当 ping &lt;a href="https://www.baidu.com"&gt;www.baidu.com&lt;/a&gt;提示Temporary failure in name resolution&lt;br&gt;
那么需要执行&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo dhclient -v -4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;就可以解决了。&lt;/p&gt;</description></item><item><title>Linxu脚本自动交互</title><link>https://blog.91demo.top/wiki/scriptinteractive.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/scriptinteractive.html</guid><description>&lt;p&gt;今天学习 Linux 脚本自动交互。需要完成 IP 归属地数据文件的维护，而 gmaker 工具里面有交互命令，现在想用脚本替代手动输入。&lt;/p&gt;
&lt;p&gt;在我们日常的 Linux 指令中，有很多命令提供交互式操作，例如 sudo、ssh、ftp 等，如何使用脚本完成自动交互呢？&lt;/p&gt;
&lt;p&gt;1，使用重定向&lt;/p&gt;
&lt;p&gt;重定向是将标准输入输出进行重定向操作，例如将标准输入输出重定向到文件。用重定向标准输入到文件的前提，是命令支持有参数指定输入方式，如 ftp 就有-i 参数来指定使用标准输入来输入密码。&lt;/p&gt;
&lt;p&gt;shell 使用重定向标准输入的方法有：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;command &amp;lt; file 将输入重定向到file
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;命令交互式重定向&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;command &amp;lt;&amp;lt; delimiter
document
delimiter
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;shell 会将分界符 delimiter 之间的内容作为输入。&lt;/p&gt;
&lt;p&gt;例如，我们来使用 ftp 实现自动登录并运行 ls 的脚本，其中用户名为 test，密码为 123456。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ftp -i -n 192.168.1.10 &amp;lt;&amp;lt;EOF
user test 123456
ls
EOF
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2，使用管道&lt;/p&gt;
&lt;p&gt;管道命令使用|这个符号，它会将前一个命令的输出作为下个命令的输入。它也需要命令支持。&lt;/p&gt;
&lt;p&gt;例如，实现 sudo 自动输入密码的脚本：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;echo &amp;#39;123456&amp;#39; | sudo -S cp file1 /backup/file1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，123456 是密码。&lt;/p&gt;
&lt;p&gt;3，使用 expect 工具&lt;/p&gt;
&lt;p&gt;expect 是用来做交互，基本任何交互的场合都能使用。在像 ssh 命令没有指定输入的参数，就很适合这种方式。&lt;/p&gt;
&lt;p&gt;需要先安装 expect，安装命令如下：&lt;/p&gt;</description></item><item><title>MongoDB 常用命令</title><link>https://blog.91demo.top/wiki/mongodb.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mongodb.html</guid><description>&lt;p&gt;MongoDB 是一个流行的开源文档型数据库，它使用类似 JSON 的文档模型存储数据，这使得数据存储变得非常灵活。&lt;/p&gt;
&lt;p&gt;官网地址：https://www.mongodb.com/&lt;/p&gt;
&lt;p&gt;当你需要迁移数据库时，或者进行备份时，使用 mongodump，可以将文件导出。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mongodump -h 127.0.0.1 --username &amp;#39;xxx&amp;#39; -p &amp;#39;密码&amp;#39; --authenticationDatabase admin -d 数据库 --gzip --archive=xxx.gz
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将数据导入到另一个服务器 mongo 数据库，或者进行升级。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mongorestore --uri=&amp;#34;mongodb://localhost:27017&amp;#34; --gzip --archive=xxx.gz
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如何你的数据库需要认证，那么请在 uri 中添加用户名和密码。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mongorestore --uri=&amp;#34;mongodb://user:password@localhost:27017&amp;#34; --gzip --archive=xxx.gz
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果你的密码中包含如@等特殊字符，需要使用 uri encoded 工具将密码加密一下。&lt;/p&gt;
&lt;p&gt;如果你要在 mongo 中创建用户，请使用如下命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.createUser({user:&amp;#34;user&amp;#34;,pwd:&amp;#34;你的密码&amp;#34;,roles:[{role:&amp;#34;readWrite&amp;#34;,db:&amp;#34;你的数据库&amp;#34;}]})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果你已经拥有账户，想赋予新的数据库操作权限，请使用如下命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.grantRolesToUser(&amp;#39;你的用户名&amp;#39;,[{role:&amp;#39;readWrite&amp;#39;,db:&amp;#39;你的数据库&amp;#39;}])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看数据库的命令&lt;code&gt;show dbs;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;删除数据库命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;use yourdb;
db.dropDatabase();
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;删除 id 为 4f29e4860b2e2ecb9910e304 的数据,操作&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.logs.remove({&amp;#39;_id&amp;#39;:ObjectId(&amp;#39;4f29e4860b2e2ecb9910e304&amp;#39;)})
db.logs.deleteOne({&amp;#39;_id&amp;#39;:ObjectId(&amp;#39;4f29e4860b2e2ecb9910e304&amp;#39;)})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查询文档记录&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.logs.find({&amp;#39;adId&amp;#39;:&amp;#39;887760470&amp;#39;})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当报错&lt;code&gt;no geo indices for geoNear&lt;/code&gt;，在集合中创建：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.feed.createIndex({loc:&amp;#34;2dsphere&amp;#34;})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;清空 logs 表记录&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.logs.deleteMany({})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;修改 logs 表记录&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;db.logs.update({userId:{$in:[id1,id2]}},{$set:{&amp;#39;sex&amp;#39;:1}},{multi:true});
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Mysql 常用SQL语句</title><link>https://blog.91demo.top/wiki/mysql.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mysql.html</guid><description>&lt;p&gt;MySQL 是最流行的关系型数据库管理系统，在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System：关系数据库管理系统)应用软件之一。&lt;/p&gt;
&lt;p&gt;官网地址：https://www.mysql.com/&lt;/p&gt;
&lt;p&gt;备份数据&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mysqldump -uroot -p --database logs &amp;gt; logs.sql
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;mysql 快速导入数百万级记录，这种方案可以节省很多时间。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mysql -uroot -p
create database dbname;
set sql_log_bin=off;
set autocommit=0;
use dbname;
start transaction;
source dbfilename;
commit;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查询用户 ID，在有规律的名称中，例如群 mKe，群 aer，等中，查找出这些群，然后取出创建者，并去重。&lt;/p&gt;
&lt;p&gt;在这里没有使用%模糊匹配，而使用_进行匹配一个任意字符。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;select distinct creater_id from group_info where name like &amp;#39;群___&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;修改表的主键 key 自增值&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;alter table users auto_increment=1000;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查询用户留存率&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;select id,DATEDIFF(now(),ifnull(login_date,add_date)) as &amp;#39;days&amp;#39; from user;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查询天数&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;select id,floor((DATEDIFF(now(),login_date))/7) as &amp;#39;groupid&amp;#39; from user where login_date != &amp;#39;&amp;#39;;
select t2.* from (select id,floor((DATEDIFF(now(),login_date))/7) as &amp;#39;groupid&amp;#39; from user t1 where login_date != &amp;#39;&amp;#39;) t2 where groupid &amp;lt; 2 and groupid &amp;gt;=1;
select id,floor(0.05) as &amp;#39;groupid&amp;#39; from user where login_date !=&amp;#39;&amp;#39;;
select now from user ;
select t1.id,now() from user t1 where id = &amp;#39;9xxxxxxx&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查询周数&lt;/p&gt;</description></item><item><title>Postgresql 常用SQL语句</title><link>https://blog.91demo.top/wiki/postgresql.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/postgresql.html</guid><description>&lt;p&gt;PostgreSQL 是一个免费的对象-关系数据库服务器(ORDBMS)，在灵活的 BSD 许可证下发行。&lt;/p&gt;
&lt;p&gt;官网地址：https://www.postgresql.org/&lt;/p&gt;
&lt;h2 id="数据库安装"&gt;数据库安装&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;yum install postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="数据库启动停止"&gt;数据库启动停止&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;systemctl start postgresql
systemctl stop postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Postgresql 数据文件目录&lt;code&gt;/var/lib/postgres&lt;/code&gt;，使用的端口&lt;code&gt;5432&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="pg94-启动脚本"&gt;pg9.4 启动脚本&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash
#su - postgres -c &amp;#34;/usr/pgsql-9.4/bin/postmaster -D /var/lib/pgsql/9.4/data/&amp;#34;
su - postgres -c &amp;#34;/usr/pgsql-9.4/bin/pg_ctl -D /var/lib/pgsql/9.4/data/ -l logfile start&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="pg94-关闭脚本"&gt;pg9.4 关闭脚本&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash
su - postgres -c &amp;#34;kill -INT `head -1 /var/lib/pgsql/9.4/data/postmaster.pid`&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="pg94-安装脚本"&gt;pg9.4 安装脚本&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash
# install pg9.4 for centos7
type wget &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || { echo &amp;gt;&amp;amp;2 &amp;#34;I require wget but it&amp;#39;s not installed. I will install it!&amp;#34;;yum install -y wget; }
echo &amp;#34;Downloading pg9.4&amp;#34;
wget -c https://download.postgresql.org/pub/repos/yum/9.4/redhat/rhel-7-x86_64/pgdg-centos94-9.4-3.noarch.rpm &amp;amp;&amp;amp; rpm -ivh pgdg-centos94-9.4-3.noarch.rpm &amp;amp;&amp;amp; yum install postgresql94-server -y
/usr/pgsql-9.4/bin/postgresql94-setup initdb
echo &amp;#34;Install Done!&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="登录-postgresql"&gt;登录 postgresql&lt;/h2&gt;
&lt;p&gt;使用 rd 登录 postgresql&lt;/p&gt;</description></item><item><title>Rsync 常用命令</title><link>https://blog.91demo.top/wiki/rsync.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/rsync.html</guid><description>&lt;p&gt;rsync 是一个常用的 Linux 应用程序，用于文件同步。它可以在本地计算机与远程计算机之间，或者两个本地目录之间同步文件。&lt;/p&gt;
&lt;p&gt;rsync 在 Linux 安装，安装命令如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# ubuntu
sudo apt install rsync
# centos
sudo yum install rsync
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果使用 Windows ，可以使用 cwRsync，它是 Windows 客户端 GUI 的一个包含 Rsync 的包装。您可以使用 cwRsync 快速远程文件备份和同步。&lt;/p&gt;
&lt;h2 id="基本用法"&gt;基本用法&lt;/h2&gt;
&lt;p&gt;在本机使用 rsync 命令时，可以作为 cp 和 mv 命令的替代方法，将源目录同步到目标目录。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -r source destination
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，-r 表示递归，即包含子目录。source 目录为源目录，destination 表示目标目录。&lt;/p&gt;
&lt;p&gt;如果有多个目录需要同步，可以写成下面这样。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -r source1 source2 destination
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们可以使用-a 参数替代-r，除了可以递归同步以外，还可以同步元信息（比如修改时间，权限）。由于 rsync 默认使用文件大小和修改时间决定文件是否需要更新，所以-a 比-r 更有用。下面的用法才是常见的写法。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -a source destination
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;目标目录 destination 如果不存在，rsync 会自动创建。执行上面的命令后，源目录 source 被完整地复制到目标目录 destination 下面，即形成了 destination/source 的目录结构。&lt;/p&gt;</description></item><item><title>白话消息队列遥测传输协议（MQTT）</title><link>https://blog.91demo.top/wiki/mqtt.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/mqtt.html</guid><description>&lt;p&gt;MQTT 是一种基于发布/订阅模式的轻量级通讯协议，该协议构建于 TCP/IP 协议上，由 IBM 在 1999 年发布，它是开放消息协议，简单容易实现，消息支持 Qos。MQTT 最大优点在于，可以以极少的代码和有限的带宽，为连接远程设备提供实时可靠的消息服务。这些特性，使其在物联网、小型设备、移动应用、车联网、电力能源等方面有着较为广泛的应用。&lt;/p&gt;
&lt;p&gt;我是在看嵌入式资料时，了解到了 MQTT 协议。嵌入式还有一种协议叫 CoAP。今天主要讲下 MQTT。我们先看下 MQTT 协议的由来。&lt;/p&gt;
&lt;p&gt;MQTT 最初是由 AndyStandford-Clark 博士和 Arlen Nipper 博士于 1999 年发明的通讯协议。他们当时是为了在狭窄的网络带宽和微小电力损耗的前提之下，提供石油管道传感器和人造卫星之间一个轻量、可靠的二进制通讯协议。2011 年，IBM 和 Eurotech 将 MQTT 协议捐赠给 Eclipse 基金会，并加入了 Eclipse M2M Industry 工作组织。2014 年 10 月，MQTT 正式成为一个开放的 OASIS 国际标准。&lt;/p&gt;
&lt;p&gt;在看到这里的时候，我在想，如果 MQTT 不公布出来，不就是一个私有协议吗？前段时间，在学习 TCP/IP 时，我还在想一件事，TCP 连接之后，就可以发送数据，但是数据格式还需要自己进行定义，发送方和接收方都需要按照规定的格式进行发送和解析。还有就是 TCP 连接如何认证？除了 IP 白名单之外，如何在应用上实现认证？这些在看了 MQTT 规范后，这些问题都找到了答案，我不知道他们当时怎么想的，我真想掰开他们的脑子，他们是真的太厉害了。他们在 1999 年就已经完成了，现在 20 多年过去了。对于入门 TCP/IP 的我来说，这是一种精神食粮，能吃还很香。&lt;/p&gt;
&lt;p&gt;这两天一直在看 MQTT 的相关资料，以及理解 MQTT 协议，不知道哪篇资料写的 MQTT 是基于 IP 协议的，后来才基于 TCP/IP 协议。我个人觉得基于 IP 协议反而更贴切当时的环境，肯定是后来协议优化之后选择的 TCP/IP 协议。在说 MQTT 协议之前，先说下我心中的疑惑？我是看嵌入式资料时，知道 MQTT 的，现在的物联网很火，完全可以做到通过私有协议来连接控制，为什么要 MQTT 协议？后来，看看身边的事物，例如 USB，是为了兼容和互通。我不知道为啥选择 MQTT？开源应该算一个原因吧，MQTT 协议设计非常好算另一个原因吧。我们来看下 MQTT 的特点吧。&lt;/p&gt;</description></item><item><title>编程开发架构发展史</title><link>https://blog.91demo.top/wiki/devarch.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/devarch.html</guid><description>&lt;p&gt;编程开发架构的发展史充满了许多重要的里程碑。以下是按时间顺序列出的编程开发架构发展史中的一些关键事件：&lt;/p&gt;
&lt;p&gt;1940 年代 - 单机编程&lt;/p&gt;
&lt;p&gt;早期计算机使用机器语言和汇编语言进行单机编程，程序员直接在单台计算机上编写和运行代码。&lt;/p&gt;
&lt;p&gt;1960 年代 - 批处理系统&lt;/p&gt;
&lt;p&gt;批处理系统允许计算机一次处理一批任务，程序员将代码提交到计算机中心，代码被集中处理后返回结果。&lt;/p&gt;
&lt;p&gt;1960 年代 - 时分多任务操作系统&lt;/p&gt;
&lt;p&gt;开发了时分多任务操作系统（如 IBM 的 OS/360），允许多个用户共享计算机资源，支持多任务处理。&lt;/p&gt;
&lt;p&gt;1970 年代 - 分时系统&lt;/p&gt;
&lt;p&gt;分时系统（如 Multics 和 UNIX）允许多个用户同时使用计算机，通过时间片轮转机制实现资源共享。&lt;/p&gt;
&lt;p&gt;1980 年代 - 客户端/服务器架构&lt;/p&gt;
&lt;p&gt;客户端/服务器架构（Client/Server Architecture）兴起，客户端应用程序与服务器进行通信，服务器处理数据并返回结果。&lt;/p&gt;
&lt;p&gt;1990 年代 - 三层架构&lt;/p&gt;
&lt;p&gt;三层架构（Three-Tier Architecture）被广泛采用，分为表示层（用户界面）、逻辑层（业务逻辑）和数据层（数据库）。&lt;/p&gt;
&lt;p&gt;1990 年代 - Web 开发&lt;/p&gt;
&lt;p&gt;万维网（World Wide Web）的兴起带来了 Web 开发，使用 HTML、CSS 和 JavaScript 开发网页，服务器端使用 CGI、PHP、ASP 等技术。&lt;/p&gt;
&lt;p&gt;2000 年代 - 服务导向架构（SOA）&lt;/p&gt;
&lt;p&gt;服务导向架构（SOA）提出，通过定义服务接口，应用程序可以相互通信和集成，促进了分布式系统的发展。&lt;/p&gt;
&lt;p&gt;2006 年 - 云计算&lt;/p&gt;
&lt;p&gt;亚马逊推出 AWS（亚马逊网络服务），开启了云计算时代，开发者可以按需使用计算资源，提升了应用程序的可扩展性和灵活性。&lt;/p&gt;
&lt;p&gt;2010 年代 - 微服务架构&lt;/p&gt;</description></item><item><title>编程语言发展史</title><link>https://blog.91demo.top/wiki/devlang.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/devlang.html</guid><description>&lt;p&gt;编程开发的发展史充满了许多重要的里程碑。以下是按时间顺序列出的编程开发史中的一些关键事件：&lt;/p&gt;
&lt;p&gt;1940 年代 - 机器语言&lt;/p&gt;
&lt;p&gt;机器语言是计算机的最基本的编程语言，直接用二进制代码来控制计算机硬件。&lt;/p&gt;
&lt;p&gt;1950 年代 - 汇编语言&lt;/p&gt;
&lt;p&gt;汇编语言（Assembly Language）引入了助记符，简化了编程过程，使程序员能够更容易地编写代码。&lt;/p&gt;
&lt;p&gt;1957 年 - Fortran&lt;/p&gt;
&lt;p&gt;IBM 发布了 Fortran（Formula Translation），这是第一个高层次编程语言，主要用于科学和工程计算。&lt;/p&gt;
&lt;p&gt;1959 年 - COBOL&lt;/p&gt;
&lt;p&gt;COBOL（Common Business-Oriented Language）被开发出来，主要用于商业数据处理。&lt;/p&gt;
&lt;p&gt;1960 年 - LISP&lt;/p&gt;
&lt;p&gt;由 John McCarthy 开发的 LISP（LISt Processing）语言，成为人工智能研究的主要语言之一。&lt;/p&gt;
&lt;p&gt;1964 年 - BASIC&lt;/p&gt;
&lt;p&gt;约翰·肯尼和托马斯·库尔茨开发了 BASIC（Beginner&amp;rsquo;s All-purpose Symbolic Instruction Code），旨在为初学者提供简单的编程语言。&lt;/p&gt;
&lt;p&gt;1970 年 - Pascal&lt;/p&gt;
&lt;p&gt;Niklaus Wirth 开发了 Pascal 语言，主要用于教学和系统编程。&lt;/p&gt;
&lt;p&gt;1972 年 - C 语言&lt;/p&gt;
&lt;p&gt;Dennis Ritchie 在贝尔实验室开发了 C 语言，它结合了高效和灵活性，成为系统编程的标准语言。&lt;/p&gt;
&lt;p&gt;1980 年 - Ada&lt;/p&gt;</description></item><item><title>豆子碎片文章内容无法查看解决方法</title><link>https://blog.91demo.top/wiki/fixartcontentnotfound.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/fixartcontentnotfound.html</guid><description>&lt;p&gt;当我的点数为 0 时，发现点击文章列表，进入到文章详情，无法查看文章内容。&lt;/p&gt;
&lt;p&gt;以前没有发现这个问题，是因为我的账户中有点数。&lt;/p&gt;
&lt;p&gt;造成的原因是加锁文章逻辑问题。&lt;/p&gt;
&lt;p&gt;原来的代码为：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;if u.Points &amp;lt; 1 {
return nil, errors.New(&amp;#34;点数不足&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;优化后的代码为：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 判断是否加锁文章
if b.IsLock == 1 {
// 只有不是自己的文章才计算点数
if bopenid != openid {
if u.Points &amp;lt; 1 {
return nil, errors.New(&amp;#34;点数不足&amp;#34;)
}
// 给文章作者奖励
VTransAddPoints(bopenid, global.ViewLockArtA, 1, uuid)
// 扣减读者
VTransSubPoints(openid, global.ViewLockArtD, 1, uuid)
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>豆子碎片小程序项目介绍</title><link>https://blog.91demo.top/wiki/visit-project.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/visit-project.html</guid><description>&lt;p&gt;豆子碎片是展示和搜索文章的微信小程序。它的项目名称是 visit，该项目经历多个版本迭代。现在已经成熟和稳定。&lt;/p&gt;
&lt;p&gt;visit 项目在小程序端的二维码：&lt;/p&gt;
&lt;p&gt;&lt;img alt="豆子碎片" loading="lazy" src="https://ant.91demo.top/imgs/visit.webp#pic_center"&gt;&lt;/p&gt;
&lt;h2 id="visit-项目地址"&gt;Visit 项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gitee.com/littletow/visit"&gt;https://gitee.com/littletow/visit&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="云环境版本"&gt;云环境版本&lt;/h2&gt;
&lt;p&gt;visit 项目使用小程序云环境，在云存储中存放文章的 markdown 文件，然后小程序调用云函数获取使用 towxml 包转换后的 json 数据，在小程序端进行渲染。新的 towxml 组件必须使用微信小程序基础库 2.9.4 版本以上，才能正常显示。该项目在搜索时，是根据文章标题和关键字进行模糊查询检索匹配的。在云环境开始收费后，转为服务器版本。服务器灵活性强，可操作空间大，可以有多种用途。&lt;/p&gt;
&lt;h2 id="服务器版本"&gt;服务器版本&lt;/h2&gt;
&lt;p&gt;visit 项目当前使用服务器版本，在服务器使用数据库存储 markdown 文件的数据。小程序调用 API 开放接口从后台获取文件的数据，在前端使用 towxml 组件进行渲染。&lt;/p&gt;
&lt;p&gt;注意：小程序端只有文章显示和搜索功能。 主要是方便上架审核。上传和管理文章等通过后台接口或工具完成，文章使用 Markdown 格式。&lt;/p&gt;
&lt;p&gt;当制作好文档后，需要使用工具或者 API 开放接口上传到后台。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用工具上传，工具请使用开源项目 &lt;a href="https://gitee.com/littletow/upart-go"&gt;upart-go 项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;使用 API 开放接口上传，API 接口文档请访问&lt;a href="https://www.91demo.top/zh-cn/api/index.html"&gt;开放接口&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="项目介绍"&gt;项目介绍&lt;/h2&gt;
&lt;p&gt;visit 项目主要是为了在小程序端记录和分享技术文章，也是学习小程序的入门项目。上传的文章以编程技术或技术相关经验为主题。小程序包含首页和我的两个栏目，首页显示公开的和自己上传的文章，可通过关键字搜索文章，搜索文章时，是通过文章标题或关键字进行模糊查询检索匹配。也可以使用快捷按钮检索文章，快捷按钮目前包含 3 个：最新，最近上传的文章；最火，浏览量最高的文章；最冷，浏览量最低的文章。我的页面只有两个功能，一个是获取我的识别码，使用工具，或者开放接口时需要用到此识别码，用作 API 接口认证。另一个是看广告获取点数。上传文章是需要豆子点数的。加入广告，是为了希望获取一点收益，承担一点我的服务器支出。文章可通过工具上传，工具可使用 upart-go 开源项目。如果要公开文章，那么公开的文章需要后台审核。文章也可以加锁，当加锁后，其它用户访问你的文章，你将获得豆子点数奖励。&lt;/p&gt;
&lt;p&gt;直接在 Markdown 文件中添加的图片链接在小程序端是无法打开的。如果确实需要在文章中显示图片，可以将图床域名地址告知我，添加到小程序域名白名单中。&lt;br&gt;
目前有一个方案是微信公众号图片小程序不会过滤，可以将您的图片上传到微信公众号，然后获取图片链接，添加到 markdown 文件中。&lt;/p&gt;
&lt;p&gt;在 Markdown 文件中添加的 HTTP 和 HTTPS 链接在小程序端无法打开。&lt;/p&gt;
&lt;p&gt;豆子碎片完整使用由四部分应用组成：小程序端，上传文章工具，Web 审核后台，以及后台接口服务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小程序端，用来显示和搜索文章。&lt;/li&gt;
&lt;li&gt;上传文章工具，用于上传文章，以及管理文章。&lt;/li&gt;
&lt;li&gt;Web 审核后台，用来管理待审核的文章。&lt;/li&gt;
&lt;li&gt;后台接口服务，为上面三个终端提供服务，将数据存储在服务器，并处理数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，小程序端和上传文章工具已开源，其它两项也将以教程的方式持续的发布在&lt;a href="https://www.91demo.top"&gt;豆子笔记&lt;/a&gt;中。&lt;/p&gt;</description></item><item><title>管理后台如何添加 IP 归属地拦截？</title><link>https://blog.91demo.top/wiki/addipterritory.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/addipterritory.html</guid><description>&lt;p&gt;在使用 IP 归属地这么长时间，我发现这个作用越来越大了。今天将源文件重新维护了下，主要是规范省份名称，例如河南调整为河南省，内蒙古调整为内蒙古自治区等，调整之后，源文件明显增大了尺寸，但是为了严谨，是值得的。&lt;/p&gt;
&lt;p&gt;因为我的后台，访问地点是固定的，通过白名单设置 IP 太麻烦，就用 IP 归属地进行拦截。&lt;/p&gt;
&lt;p&gt;我精确到省份，其实可以精确到市，我的 IP 库精确到市。&lt;/p&gt;
&lt;p&gt;添加还是非常简单的，利用开放接口即可。&lt;/p&gt;
&lt;p&gt;我这里不用使用，可以直接调用函数，如果是两个服务，就需要开放接口了。&lt;/p&gt;
&lt;p&gt;先获取归属地，归属地格式，国家|省份|城市，国外的只显示国家名，其它项为 0，返回字符串，以|分割。&lt;/p&gt;
&lt;p&gt;取到归属地字符串后，切分字符串为数组，取出省份。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;strArr := strings.Split(str, &amp;#34;|&amp;#34;)
n := len(strArr)
// 这个情况基本不存在，源文件中以0作为占位符。
if n != 3 {
return &amp;#34;&amp;#34;
}
return strArr[1]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;归属地数组索引 0 是国家，1 是省份，2 是城市。&lt;/p&gt;
&lt;p&gt;现在，使用获取到的省份信息比对。&lt;/p&gt;
&lt;p&gt;以前，是直接将省份字段和“河南”比较，现在优化了一下，包含就可以。防止数据文件出现河南省这样的情况导致问题。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ok := strings.Contains(province, &amp;#34;河南&amp;#34;)
if !ok {
c.JSON(http.StatusOK, ErrWxResp(&amp;#34;拒绝登录-1&amp;#34;, nil))
return
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样，归属地不是河南将不允许登录。&lt;/p&gt;</description></item><item><title>计算机病毒发展史</title><link>https://blog.91demo.top/wiki/virus.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/virus.html</guid><description>&lt;p&gt;网络安全的发展史充满了许多重要的里程碑。以下是按时间顺序列出的网络安全发展史中的一些关键事件：&lt;/p&gt;
&lt;p&gt;1983 年 - 计算机病毒的概念&lt;/p&gt;
&lt;p&gt;弗雷德·科恩（Fred Cohen）在他的论文中首次提出“计算机病毒”的概念，定义了自我复制的软件程序。&lt;/p&gt;
&lt;p&gt;1986 年 - Brain 病毒&lt;/p&gt;
&lt;p&gt;Brain 病毒是第一个 IBM PC 兼容机上的计算机病毒，由巴基斯坦的两个兄弟编写，它感染了引导扇区。&lt;/p&gt;
&lt;p&gt;1988 年 - Morris 蠕虫&lt;/p&gt;
&lt;p&gt;Robert Tappan Morris 发布了 Morris 蠕虫，这是第一个在互联网上广泛传播的蠕虫，导致了大量计算机系统的瘫痪。&lt;/p&gt;
&lt;p&gt;1995 年 - 微软 Word 宏病毒&lt;/p&gt;
&lt;p&gt;Concept 病毒是第一个宏病毒，感染了微软 Word 文档，标志着宏病毒时代的开始。&lt;/p&gt;
&lt;p&gt;1999 年 - Melissa 病毒&lt;/p&gt;
&lt;p&gt;Melissa 病毒通过电子邮件传播，迅速感染了数十万台计算机，导致企业网络瘫痪。&lt;/p&gt;
&lt;p&gt;2000 年 - ILOVEYOU 病毒&lt;/p&gt;
&lt;p&gt;ILOVEYOU 病毒通过电子邮件传播，造成了数十亿美元的损失，是历史上传播最快的病毒之一。&lt;br&gt;
2001 年 - Code Red 蠕虫&lt;/p&gt;
&lt;p&gt;Code Red 蠕虫攻击了微软的 IIS 服务器，感染了超过 35 万台计算机，对互联网造成了严重影响。&lt;/p&gt;
&lt;p&gt;2003 年 - SQL Slammer 蠕虫&lt;/p&gt;</description></item><item><title>计算机发展史</title><link>https://blog.91demo.top/wiki/computer.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/computer.html</guid><description>&lt;p&gt;计算机发展史充满了许多重要的里程碑。以下是按时间顺序列出的计算机发展史中的一些关键事件：&lt;/p&gt;
&lt;p&gt;1941 年 - 第一台电子计算机：Z3&lt;/p&gt;
&lt;p&gt;由德国工程师康拉德·楚泽（Konrad Zuse）发明的 Z3 被认为是世界上第一台可编程的电子计算机。&lt;/p&gt;
&lt;p&gt;1943 年 - Colossus&lt;/p&gt;
&lt;p&gt;英国在二战期间开发的 Colossus 计算机，用于破解德国的密码，特别是 Lorenz 密码。&lt;/p&gt;
&lt;p&gt;1946 年 - ENIAC&lt;/p&gt;
&lt;p&gt;美国研制的 ENIAC（Electronic Numerical Integrator and Computer）是第一台通用电子计算机。&lt;/p&gt;
&lt;p&gt;1951 年 - UNIVAC I&lt;/p&gt;
&lt;p&gt;UNIVAC I（Universal Automatic Computer I）是第一台商用计算机，由 J. Presper Eckert 和 John Mauchly 设计。&lt;/p&gt;
&lt;p&gt;1956 年 - 磁盘存储器&lt;/p&gt;
&lt;p&gt;IBM 引入了第一个硬盘驱动器 IBM 305 RAMAC，它使用旋转磁盘来存储数据。&lt;/p&gt;
&lt;p&gt;1964 年 - IBM System/360&lt;/p&gt;
&lt;p&gt;IBM 发布的 System/360 系列计算机，成为一种标准化的计算机系统，影响深远。&lt;/p&gt;
&lt;p&gt;1971 年 - 微处理器&lt;/p&gt;
&lt;p&gt;英特尔发布了 4004 微处理器，这是第一款商用微处理器，标志着个人计算机时代的开始。&lt;/p&gt;
&lt;p&gt;1973 年 - 以太网&lt;/p&gt;</description></item><item><title>记录 Frp 常用配置</title><link>https://blog.91demo.top/wiki/frp.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/frp.html</guid><description>&lt;p&gt;简单、高效的内网穿透工具。&lt;/p&gt;
&lt;p&gt;网址：https://www.gofrp.org&lt;/p&gt;
&lt;h2 id="通过-ssh-访问内网机器"&gt;通过 SSH 访问内网机器&lt;/h2&gt;
&lt;p&gt;1, 在具有公网 IP 的机器上部署 frps&lt;/p&gt;
&lt;p&gt;部署 frps 并编辑 frps.toml 文件。以下是简化的配置，其中设置了 frp 服务器用于接收客户端连接的端口：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;bindPort = 7000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2, 在需要被访问的内网机器上部署 frpc&lt;/p&gt;
&lt;p&gt;部署 frpc 并编辑 frpc.toml 文件，假设 frps 所在服务器的公网 IP 地址为 x.x.x.x。以下是示例配置：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;serverAddr = &amp;#34;x.x.x.x&amp;#34;
serverPort = 7000
[[proxies]]
name = &amp;#34;ssh&amp;#34;
type = &amp;#34;tcp&amp;#34;
localIP = &amp;#34;127.0.0.1&amp;#34;
localPort = 22
remotePort = 6000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;localIP 和 localPort 配置为需要从公网访问的内网服务的地址和端口。&lt;br&gt;
remotePort 表示在 frp 服务端监听的端口，访问此端口的流量将被转发到本地服务的相应端口。&lt;/p&gt;
&lt;p&gt;3, 通过 SSH 访问内网机器&lt;/p&gt;
&lt;p&gt;使用以下命令通过 SSH 访问内网机器，假设用户名为 test：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh -o Port=6000 test@x.x.x.x
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>记录 URL 跳转携带原路径</title><link>https://blog.91demo.top/wiki/urlredirect.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/urlredirect.html</guid><description>&lt;p&gt;在使用我的笔记中，发现登录之后，无法跳转到登录前要到达的页面。&lt;/p&gt;
&lt;p&gt;首先，检查 Nginx 配置，查看是否配置了携带登录前的 URL 路径。查看之后，发现没有配置，现在将其配置好，配置如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;location @error401{
return 302 https://$host/login/?url=https://$host$request_uri;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;配置好后，继续测试，发现登录后，还是调整到首页，并没有调整到登录前的页面。检查登录应用，发现登录应用没有取 URL 参数，所以直接跳转到根路径。&lt;/p&gt;
&lt;p&gt;将应用配置好，取 URL 参数，当 URL 参数不为空时，跳转到该 URL 路径。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;url := c.Query(&amp;#34;url&amp;#34;)
if url != &amp;#34;&amp;#34; {
c.Redirect(http.StatusFound, url)
} else {
c.Redirect(http.StatusFound, &amp;#34;/&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将应用打包，发布到服务器继续测试，还是失败，看日志，浏览器请求时是携带 URL 参数的。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;GET /login/?url=https://www.91demo.top/zh-cn/private/project/visit/ch1.html
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;一度对人生产生怀疑，为啥我的不可以呢？我是参考网上教程配置的，网上教程使用 Python 举例，我这里使用 Go 实现，原理上应该是可行的啊。&lt;/p&gt;
&lt;p&gt;经过多次细读对比，发现自己的 GET 请求路径是/login/，POST 请求路径是/api/doLogin，而网上教程 GET 和 POST 请求都是/login/，将自己的应该重新调整，无论 GET 请求还是 POST 请求都调整为/login 路径。当将应用推到服务器时，发现浏览器上显示跳转次数过多。排除一番后，将应用路径调整为/login/，Nginx 配置文件也进行了修改，修改为如下配置：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;location /login/ {
proxy_pass http://127.0.0.1:9982/login/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;原来的配置如下：&lt;/p&gt;</description></item><item><title>记录无线技术发展史</title><link>https://blog.91demo.top/wiki/wireless.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/wireless.html</guid><description>&lt;p&gt;无线技术的发展史充满了许多重要的里程碑。以下是按时间顺序列出的无线技术发展史中的一些关键事件：&lt;/p&gt;
&lt;p&gt;1864 年 - 电磁波理论&lt;/p&gt;
&lt;p&gt;詹姆斯·克拉克·麦克斯韦（James Clerk Maxwell）提出了电磁波理论，预言了电磁波的存在。&lt;/p&gt;
&lt;p&gt;1888 年 - 电磁波的实验验证&lt;/p&gt;
&lt;p&gt;亨利·赫兹（Heinrich Hertz）通过实验验证了麦克斯韦的电磁波理论，成功产生并检测到电磁波。&lt;/p&gt;
&lt;p&gt;1895 年 - 无线电报&lt;/p&gt;
&lt;p&gt;古列尔莫·马可尼（Guglielmo Marconi）进行了首次成功的无线电报传输，标志着无线通信的开始。&lt;/p&gt;
&lt;p&gt;1901 年 - 跨大西洋无线电信号&lt;/p&gt;
&lt;p&gt;马可尼成功实现了跨大西洋的无线电信号传输，从英国到加拿大，证明了无线电波可以覆盖长距离。&lt;/p&gt;
&lt;p&gt;1933 年 - 频率调制（FM）&lt;/p&gt;
&lt;p&gt;埃德温·霍华德·阿姆斯特朗（Edwin Howard Armstrong）发明了频率调制（FM）技术，改进了无线电传输的质量和抗干扰能力。&lt;/p&gt;
&lt;p&gt;1947 年 - 蜂窝通信概念&lt;/p&gt;
&lt;p&gt;贝尔实验室的研究人员提出了蜂窝通信的概念，奠定了现代移动通信网络的基础。&lt;/p&gt;
&lt;p&gt;1973 年 - 第一部手机通话&lt;/p&gt;
&lt;p&gt;摩托罗拉的工程师马丁·库帕（Martin Cooper）进行了首次手机通话，使用了一部原型移动电话。&lt;/p&gt;
&lt;p&gt;1983 年 - 第一代移动通信（1G）&lt;/p&gt;
&lt;p&gt;美国推出了第一代移动通信系统（1G），基于模拟信号的蜂窝网络。&lt;/p&gt;
&lt;p&gt;1991 年 - 第二代移动通信（2G）&lt;/p&gt;
&lt;p&gt;第二代移动通信系统（2G）在芬兰推出，采用数字信号，提高了通话质量和网络安全性。&lt;/p&gt;
&lt;p&gt;1997 年 - Wi-Fi&lt;/p&gt;
&lt;p&gt;IEEE 802.11 标准发布，标志着 Wi-Fi 技术的诞生，提供了无线局域网（WLAN）解决方案。&lt;/p&gt;
&lt;p&gt;2001 年 - 第三代移动通信（3G）&lt;/p&gt;
&lt;p&gt;第三代移动通信系统（3G）在日本推出，提供了更快的数据传输速率，支持多媒体通信。&lt;/p&gt;
&lt;p&gt;2009 年 - 第四代移动通信（4G）&lt;/p&gt;</description></item><item><title>记录项目中某个文件无法删除的问题</title><link>https://blog.91demo.top/wiki/fixfilecannotremove.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/fixfilecannotremove.html</guid><description>&lt;p&gt;这几日，碰到一个奇怪问题，我一篇要删除的文章，在删除文件后，并提交到 git，当我运行 mdbook build 之后，这篇文章的文件又出现了。&lt;/p&gt;
&lt;p&gt;然后，我在 Git 中删除，然后再次提交。发现几日后又出现了，我想了一下，中间运行过 mdbook build。&lt;/p&gt;
&lt;p&gt;既然这样，我猜想一定是 SUMMARY.md 文件中还存在这篇文章的索引。经检查，果然存在。&lt;/p&gt;
&lt;p&gt;所以，下次删除文章时，先删除 SUMMARY.md 中的索引，然后再删除文件，才能完全删除。&lt;/p&gt;</description></item><item><title>将sqlite数据库迁移到postgresql数据库</title><link>https://blog.91demo.top/wiki/sqlite2pg.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/sqlite2pg.html</guid><description>&lt;p&gt;最近，又想接入 mqtt 的认证，使用 sqlite 数据库的缺陷更明显了。sqlite3 不支持并发，当另一个应用访问时，会提示数据库已经占用。&lt;/p&gt;
&lt;p&gt;第一想法是将 sqlite 迁移到 mysql 中去，但是安装了 mysql 之后，发现内存占用很大，我的 1G 服务器都占用 800MB 左右，受不了，这样的话，我的服务器无法再安装其它服务。&lt;/p&gt;
&lt;p&gt;然后我又安装了 postgresql，发现内存占用还可以接受，默认没有连接应用的情况下，内存占用 10MB 左右。先观察一下 pg 内存占用是否稳定，现在没有连接，不知道真实情况如何？&lt;/p&gt;
&lt;p&gt;这两天看了一下 PG 的教程，并观察了一下 PG，发现运行还是很稳定的。决定使用 PG。&lt;/p&gt;
&lt;p&gt;如果采用 PG，需要先将 Sqlite3 的数据迁移到 PG 中去。&lt;/p&gt;
&lt;p&gt;这里是我这几天看 Postgresql 记录的，包含迁移 sqlite3 数据到 PG。详情请查看&lt;a href="https://www.91demo.top/zh-cn/public/db/postgresql.html"&gt;postgresql 数据库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最近又将语音验证码从 Sqlite3 迁移到了 Postgresql，现在不需要同步了，在豆子工具获取 AST 账户就可以直接使用了。&lt;/p&gt;
&lt;p&gt;哎，这几天挺累的，但收获颇丰。&lt;/p&gt;</description></item><item><title>配置 Asterisk PJSIP 使用Postgresql</title><link>https://blog.91demo.top/wiki/pjsippgsql.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/pjsippgsql.html</guid><description>&lt;p&gt;要使用 Postgresql，需要先安装 pg 头文件&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;yum install postgresql-devel
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 Asterisk 源文件中，执行&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;./configure
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行下面的命令，查看 res_config_pgsql 模块有没有选择，没有请先选择。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;make menuselect
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后执行&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;make;make install
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在/etc/asterisk/modules.conf 配置文件中，添加&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;load = res_config_pgsql.so
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将源代码中 configs/samples/res_pgsql.conf.sample 复制到/etc/asterisk 目录下，并改为 res_pgsql.conf 文件。修改用户名等参数。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[general]
dbhost=127.0.0.1
dbport=5432
dbname=asterisk
dbuser=asterisk
dbpass=password
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;修改配置文件/etc/asterisk/extconfig.conf，将下面三项调整为：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ps_endpoints =&amp;gt; pgsql,general
ps_auths =&amp;gt; pgsql,general
ps_aors =&amp;gt; pgsql,general
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其它配置文件，参考 sqlite3 数据库的配置。&lt;/p&gt;</description></item><item><title>配置Asterisk 使用数据库存储PJSIP信息</title><link>https://blog.91demo.top/wiki/pjsiprealtime.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/pjsiprealtime.html</guid><description>&lt;p&gt;Asterisk 如何将 PJSIP 通道驱动程序与实时数据库存储后端相连接。实时接口允许将 PJSIP 的大部分配置（如端点、auth、aor 等）存储在数据库中，而不仅是 pjsip.conf 配置文件中。&lt;/p&gt;
&lt;p&gt;我们假设 Asterisk 安装在 Linux 服务器上，并希望 Asterisk 通过 odbc 连接器连接到 Mysql 数据库。我们还需要安装如下依赖包：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unixodbc 和 unixodbc-dev&lt;/li&gt;
&lt;li&gt;odbc 及其开发包&lt;/li&gt;
&lt;li&gt;libmyodbc&lt;/li&gt;
&lt;li&gt;odbc 到 mysql 接口包&lt;/li&gt;
&lt;li&gt;python-dev 和 python-pip&lt;/li&gt;
&lt;li&gt;python-mysqldb&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果在 Ubuntu 服务器，可以直接通过如下命令安装：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# apt-get install unixodbc unixodbc-dev libmyodbc python-dev python-pip python-mysqldb
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;一旦安装了这些包，你需要使用 make menuconfig 工具检查 res_config_odbc 和 res_odbc 和 res_pjsip_xxx 资源模块被安装。然后执行&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;./configure;make;make install
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;创建数据库，使用 mysqladmin 工具创建一个数据库，用来存储配置信息。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# mysqladmin -u root -p create asterisk
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;安装 Alembic，可以用来创建配置信息表结构。Alembic 是一个完整的数据库迁移工具，支持升级现有数据库的模式、模式版本控制、创建新表和数据库等等。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# pip install alembic
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;切换到 Asterisk 源码目录 contrib/ast-db-manage/，它包含 Alembic 脚本。&lt;/p&gt;</description></item><item><title>配置Nginx 基于时间控制网页访问</title><link>https://blog.91demo.top/wiki/nginxtimecontrol.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/nginxtimecontrol.html</guid><description>&lt;p&gt;今天，突发奇想，要控制我的网站在 6:00-23:00 之间访问，其余时间不让访问，为了让我们休息，不要太操劳了。&lt;/p&gt;
&lt;p&gt;网上搜索了一番，还真可以，现在就实践一把。&lt;/p&gt;
&lt;p&gt;如果要限制 IP，请使用 allow 和 deny 指令。&lt;/p&gt;
&lt;p&gt;例如，禁止某个 IP 访问就使用 deny IP，要允许某个 IP 访问就使用 allow IP，禁止所有就是 deny all，允许所有就是 allow all。常见的应用场景，管理后台，工具站等。还有一种方案就是使用防火墙限制，更安全一些。&lt;/p&gt;
&lt;p&gt;不满足需求，我们继续搜索时间段限制。&lt;/p&gt;
&lt;p&gt;Nginx 提供了一个叫做 ngx_http_time_module 的时间模块，该模块可以帮助我们根据当前时间来对请求进行访问控制。这个时间模块包含了很多有用的指令，如$time_iso8601、$time_local、$time_gmt 等，它们可以用于获取当前服务器时间，并进行时间相关的判断。&lt;/p&gt;
&lt;p&gt;现在配置网站在每天 6:00-23:00 访问，当配置网站后，生效了，但是体验很差，就将配置加在了管理后台。&lt;/p&gt;
&lt;p&gt;首先，定义一个变量，用于获取当前时间：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;map $time_iso8601 $currtime {
default 0;
&amp;#34;~^(\d{4})-(\d{2})-(\d{2})T(0[6-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])&amp;#34; 1;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面的配置，使用了 map 模块和正则表达式。map 不能配置在 Server 区块内，请配置在 Http 区块内。&lt;/p&gt;
&lt;p&gt;现在，我们将变量$currtime 应用到我们的资源配置中：&lt;/p&gt;
&lt;p&gt;这是一种写法，设置一个变量，解决 else 问题。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;location / {
set $ok 0;
if($curr_time=1){
// 访问页面资源
set $ok 1;
}
# 如果没有匹配到，跳转到这个页面
if($ok=0){
// 访问广告页
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们的需求简单，这里可以简化成&lt;/p&gt;</description></item><item><title>深度复盘Rust上传工具崩溃问题，优化报错提示</title><link>https://blog.91demo.top/rust/upartrs.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/rust/upartrs.html</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>使用Caddy部署网站</title><link>https://blog.91demo.top/wiki/caddy.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/caddy.html</guid><description>&lt;p&gt;Caddy 是一个功能强大、可扩展的平台，用 Go 编写，可为您的网站、服务和应用程序提供服务。&lt;/p&gt;
&lt;p&gt;大多数人使用 Caddy 用作 Web 服务器或者代理服务。我是想让我的网站和小程序支持 quic，而 Caddy 是一种解决方案。在使用 Caddy 前，我使用 nginx 作为网站服务器，以及小程序后台接口服务的代理。目前我已经在服务器上部署了 Caddy，并将小程序后台服务和网站都迁移到 Caddy 上。虽然我配置了 HTTP3 协议，但是 HTTP3 并没有生效。网站和小程序可以正常访问，使用的是 HTTP2 协议。我折腾了一段时间，还是没有找到问题点，依旧是 HTTP2 协议或 HTTP1.1 协议。这并没有达到我的初衷。&lt;/p&gt;
&lt;p&gt;之所以给大家继续推荐 Caddy，是因为 Caddy 确实有一些优势。我说下在我服务器部署 Caddy 之后的感受吧。首先，Caddy 在配置上要比 Nginx 简单，下面会详细介绍一下 Caddy 配置 Web 服务器和代理。其次，Caddy 天然支持 HTTPS，这对于网站和小程序后台，有很大的优势，特别是小程序后台，微信强制要求必须是 HTTPS。性能这块，可能因为我的网站比较小，我并没有感到太大的差异（凭感觉，没有经过性能工具测试）因为使用 Nginx 或 Caddy，我的网站和小程序访问都非常流畅。网上的帖子说 Nginx 性能会更好，特别是高并发时。在内存使用上，Caddy 要远远大于 Nginx，我的服务器内存比较小，这块非常明显。Nginx 在我服务器上的内存占用在 1MB 左右，而 Caddy 在 20MB 左右。&lt;/p&gt;
&lt;p&gt;现在我们讲下 Caddy 如何安装？在 Caddy 的官网上，详细的介绍了 Caddy 的安装方法，我以我的服务器 CentOS7 为例，介绍一下 Caddy 的安装。&lt;/p&gt;</description></item><item><title>使用小程序账号绑定我的客户端</title><link>https://blog.91demo.top/wiki/bindmp.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/bindmp.html</guid><description>&lt;p&gt;小程序提供登录 API，可以获取小程序的唯一标识（openid）。我们可以通过这个唯一标识（openid）和客户端绑定起来。&lt;/p&gt;
&lt;p&gt;豆子碎片小程序是一款内容类小程序。它包含 Golang、Rust、小程序、Web 开发、数据库以及开发环境等内容的文章。现在，它已经成为一个研究学习平台以及内容查找和内容承载平台。&lt;/p&gt;
&lt;p&gt;upart-go 项目是用来上传文章到豆子碎片的一个命令行工具。它使用 Golang 开发。可以用来上传文章和管理文章。在使用该工具上传文章时，我们需要进行标识，是谁上传了文章？&lt;/p&gt;
&lt;p&gt;以前，我们会提供用户注册服务，让用户使用手机号或者邮箱进行注册。然后通过手机号或邮箱来标识用户。现在，我们还可以使用小程序账号来标识用户，免去用户注册（用户也可能不想不会使用手机号或邮箱注册）。&lt;/p&gt;
&lt;p&gt;我以 upart-go 项目文章上传客户端为例，介绍一下它的账号绑定原理。在用户使用小程序后，会调用登录 API 进行自动登录，然后后台会获取用户当前小程序的 openid。当获取到用户 openid 后，就可以唯一标识用户。此时，我们为小程序用户提供一个识别码的功能。识别码用于开放接口，用来识别用户。它基于 openid，生成 icode 和 isecret，分别代表账号和密码。这样当用户使用 icode 和 isecret 调用开放接口时，我们就可以定位到这个用户的 openid，从而定位到用户。我们的客户端可以在配置文件中配置这个 icode 和 isecret。这样当我们调用后台服务上传文章时，我们就知道将文章划分到谁的名下？在小程序中也可以进行浏览和查看。&lt;/p&gt;
&lt;p&gt;讲完了原理，我们开始实现功能。我们在客户端中已经实现了通过 icode 和 isecret 识别用户。但我们的客户端还可以进行优化，免去用户创建配置文件以及配置识别码。如何去做呢？&lt;/p&gt;
&lt;p&gt;我们启动客户端后，获取客户端主机的 MAC 地址，标识当前的客户端，然后我们提供一个窗口展示小程序码和验证码，小程序码让用户扫描，扫描之后打开的页面会识别当前用户的 openid，在打开的页面填入验证码，我们就可以完成小程序账号和客户端的绑定，是不是很棒？&lt;/p&gt;
&lt;p&gt;如果你对这个功能感兴趣，可参考项目，地址为：&lt;/p&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;</description></item><item><title>探测手机流量 UDP 是否拦截封禁</title><link>https://blog.91demo.top/wiki/udpprobe.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/udpprobe.html</guid><description>&lt;p&gt;最近，自助语音验证码使用手机流量拨打，又没有声音了。&lt;/p&gt;
&lt;p&gt;查看了 Ast 星日志，发现连接是正常的，使用 WIFI 拨打正常。于是又各种调整 PJSIP 参数，发现都没有生效。&lt;/p&gt;
&lt;p&gt;打开 RTP 日志后，发现 RTP 推送的音频流是发送到公网的，网上搜索，发现通信运营商会拦截封禁 UDP。我猜测大概率是这个造成的。&lt;/p&gt;
&lt;p&gt;但是如何探测 UDP 是否真的被封禁了？我想做一个工具。&lt;/p&gt;
&lt;p&gt;网上很多内容介绍如何使用 APP 工具探测 UDP，由于不擅长 APP，我想到了微信小程序。可以使用它实现这个功能。&lt;/p&gt;
&lt;p&gt;我们想探测移动、联通、电信是否封禁了 UDP，我们需要一个工具，我这里使用微信小程序，在小程序打开后，向后台服务器发送 UDP 端口的请求，后台服务器根据发送的 UDP 端口，发送 UDP 包请求。可以每 500 毫秒发送一个包，发送 10 次，5 秒钟内发送完。在小程序端，按钮发送后，显示加载动画，如何 5s 内没有收到消息，判定 UDP 消息无法送达。显示结果。&lt;/p&gt;</description></item><item><title>网络流量监控常用命令</title><link>https://blog.91demo.top/wiki/netmon.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/netmon.html</guid><description>&lt;p&gt;Linux 系统中有各种查看网络流量的工具，比如 sar、iftop、nethogs 等，它们可以从不同的纬度来分析系统中流量信息。&lt;/p&gt;
&lt;h2 id="sar"&gt;Sar&lt;/h2&gt;
&lt;p&gt;sar（System Activity Reporter 系统活动情况报告）是目前 Linux 上最为全面的系统性能分析工具之一，可以从多方面对系统的活动进行报告。sar 可以从网络接口层面来分析数据包的收发情况、错误信息等。&lt;/p&gt;
&lt;p&gt;执行如下命令，使用 sar 每 1 秒统计一次网络接口的活动状况，连续统计 5 次。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sar -n DEV 1 5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;显示结果主要字段说明&lt;/p&gt;
&lt;p&gt;IFACE：网络接口名称。&lt;/p&gt;
&lt;p&gt;rxpck/s、txpck/s：每秒接收或发送的数据包数量。&lt;/p&gt;
&lt;p&gt;rxkB/s、txkB/s：每秒接收或发送的字节数，以 kB/s 为单位。&lt;/p&gt;
&lt;p&gt;rxcmp/s、txcmp/s：每秒接收或发送的压缩过的数据包数量。&lt;/p&gt;
&lt;p&gt;rxmcst/s：每秒接收到的多播数据包。&lt;/p&gt;
&lt;h2 id="iftop"&gt;Iftop&lt;/h2&gt;
&lt;p&gt;iftop 是 Linux 系统中一个免费的网卡实时流量监控工具，可以监控包括指定网卡的实时流量、端口连接信息、反向解析 IP 等信息。&lt;/p&gt;
&lt;p&gt;执行如下命令，查看详细端口流量占用情况。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;iftop -i eth0 -P
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行如下命令，查看端口对应的进程。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;netstat -tunlp |grep [$Port]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;确认对应服务后，您可以通过停止服务或使用 iptables 服务来对指定地址进行处理。例如屏蔽 IP 地址或限速，以保证服务器带宽能够正常使用。&lt;/p&gt;
&lt;h2 id="nethogs"&gt;Nethogs&lt;/h2&gt;
&lt;p&gt;Nethogs 是一款开源的网络流量监控工具，可用于显示每个进程的带宽占用情况。这样可以更直观定位异常流量的来源。Nethogs 支持 IPv4 和 IPv6 协议，支持本地网卡及 PPP 连接。直接输入 nethogs 启动工具即可。不带任何参数时，Nethogs 默认监控 eth0。用户可以通过 ifconfig 等指令核实具体哪个网络接口（eth1、eth0）对应公网网卡。&lt;/p&gt;
&lt;p&gt;查看网卡上进程级的流量信息。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nethogs eth1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;若确定进程是恶意程序，可以通过执行如下命令，终止进程。&lt;/p&gt;</description></item><item><title>芯片发展史</title><link>https://blog.91demo.top/wiki/chip.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/wiki/chip.html</guid><description>&lt;p&gt;芯片的发展史充满了许多重要的里程碑。以下是按时间顺序列出的芯片发展史中的一些关键事件：&lt;/p&gt;
&lt;p&gt;1947 年 - 晶体管的发明&lt;/p&gt;
&lt;p&gt;约翰·巴丁（John Bardeen）、沃尔特·布拉顿（Walter Brattain）和威廉·肖克利（William Shockley）在贝尔实验室发明了晶体管，取代了电子管，成为后续芯片发展的基础。&lt;/p&gt;
&lt;p&gt;1958 年 - 第一块集成电路&lt;/p&gt;
&lt;p&gt;杰克·基尔比（Jack Kilby）在德州仪器公司发明了第一块集成电路（IC），它将多个晶体管集成到一个硅片上。&lt;/p&gt;
&lt;p&gt;1960 年 - 平面工艺技术&lt;/p&gt;
&lt;p&gt;罗伯特·诺伊斯（Robert Noyce）在仙童半导体公司开发了平面工艺技术，使得大规模生产集成电路成为可能。&lt;/p&gt;
&lt;p&gt;1965 年 - 摩尔定律&lt;/p&gt;
&lt;p&gt;戈登·摩尔（Gordon Moore）提出摩尔定律，预测集成电路中的晶体管数目每两年翻一番，这一预测指导了半导体行业的发展。&lt;/p&gt;
&lt;p&gt;1971 年 - 英特尔 4004 微处理器&lt;/p&gt;
&lt;p&gt;英特尔发布了 4004 微处理器，这是世界上第一款商用微处理器，标志着微处理器时代的开始。&lt;/p&gt;
&lt;p&gt;1974 年 - 英特尔 8080 微处理器&lt;/p&gt;
&lt;p&gt;英特尔发布了 8080 微处理器，是第一款广泛应用于个人计算机的微处理器。&lt;/p&gt;
&lt;p&gt;1982 年 - ARM 架构&lt;/p&gt;
&lt;p&gt;英国 Acorn 公司开发了 ARM 架构（Advanced RISC Machine），低功耗高性能的设计使其在移动设备中得到广泛应用。&lt;/p&gt;
&lt;p&gt;1985 年 - 英特尔 80386 微处理器&lt;/p&gt;
&lt;p&gt;英特尔发布了 80386 微处理器，首次引入了 32 位架构，极大地提升了计算性能。&lt;/p&gt;
&lt;p&gt;1993 年 - 英特尔 Pentium 处理器&lt;/p&gt;</description></item><item><title>在 mdbook 中集成 Mermaid 实现自动化流程图渲染</title><link>https://blog.91demo.top/devops/addmermaid.html</link><pubDate>Wed, 01 Jan 2025 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/devops/addmermaid.html</guid><description>&lt;p&gt;mermaid 是很强大的一个库，可以使用文本展示图表。mdbook 是一个可以通过 Markdown 格式的文章内容生成在线书籍网站。mdbook-mermaid 这个库将 mermaid 和 mdbook 粘合在了一起。&lt;/p&gt;
&lt;p&gt;下面是 mdbook-mermaid 的一个示例，&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-mermaid" data-lang="mermaid"&gt; graph TD;
A--&amp;gt;B;
A--&amp;gt;C;
B--&amp;gt;D;
C--&amp;gt;D;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;该插件使用 Rust 开发，可以通过 Cargo 安装，&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cargo install mdbook-mermaid
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在首次使用 mdbook-mermaid 时，需要下载一些依赖文件和配置，使用命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mdbook-mermaid install path/to/your/book
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面的/path/to/your/book 是你的数据路径，运行之后，将会在你的书籍 book.toml 配置文件中添加如下内容：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[preprocessor.mermaid]
command = &amp;#34;mdbook-mermaid&amp;#34;
[output.html]
additional-js = [&amp;#34;mermaid.min.js&amp;#34;, &amp;#34;mermaid-init.js&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;插件将检测是否已配置 mdbbok-mermaid，如果已配置将跳过。否则，将添加上面的内容到 book.toml 配置文件中，并将文件 mermaid.min.js，mermaid-init.js 复制到你书籍的目录中。你可以在 src/bin/assets 目录中找到这些文件。你还可以修改 mermaid-init.js 来配置 mermaid。&lt;/p&gt;
&lt;p&gt;最后，重新编译书籍上传即可。&lt;/p&gt;</description></item><item><title>用户如何上传和更新Markdown文章？</title><link>https://blog.91demo.top/wiki/gupart-use.html</link><pubDate>Tue, 15 Oct 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-use.html</guid><description>&lt;p&gt;首先，下载最新版本的命令行工具。下载地址：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://gitee.com/littletow/upart-go/releases/download/v2.1.0/upart-go.zip
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;或者使用源码自行编译，源码地址：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://gitee.com/littletow/upart-go/archive/refs/tags/v2.1.0.zip
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="如何上传豆子碎片文章"&gt;如何上传豆子碎片文章？&lt;/h2&gt;
&lt;p&gt;命令行工具提供了上传文章的功能。你需要先使用 Markdown 格式写好内容。&lt;/p&gt;
&lt;p&gt;上传文章使用 upload 命令，后面跟参数题目，关键字，Markdown 文件，是否公开，是否加锁。其中前 3 项必填。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;gart upload title keyword content ispub islock
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;gart --help
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注意：更新完成后，可以在豆子碎片小程序中进行体验。&lt;/p&gt;</description></item><item><title>上传工具实现获取有效城市功能</title><link>https://blog.91demo.top/wiki/gupart-city.html</link><pubDate>Sun, 13 Oct 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-city.html</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;pre tabindex="0"&gt;&lt;code&gt;gart area 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后获取有效城市名称，例如我查询河南省的有效城市。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;gart area 2 河南省
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注意：以上数据来源参考微信地址信息，精确到地级市或同等行政级别。&lt;/p&gt;
&lt;h2 id="如何限制文章同城访问"&gt;如何限制文章同城访问？&lt;/h2&gt;
&lt;p&gt;命令行工具提供了限制文章同城访问功能。 下载最新版本的命令行工具。&lt;/p&gt;
&lt;p&gt;需要先使用 area 命令获取有效城市，然后使用下面命令限制文章访问。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;gart city uuid 城市名称
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注意：更新成功后，可以在豆子碎片小程序中进行体验。&lt;/p&gt;</description></item><item><title>上传工具实现更新功能</title><link>https://blog.91demo.top/wiki/gupart-update.html</link><pubDate>Thu, 26 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-update.html</guid><description>&lt;h2 id="更新文章标题"&gt;更新文章标题&lt;/h2&gt;
&lt;p&gt;有的时候，我们上传完文章之后，会发现我们的文章标题不合适，或想更好的描述文章内容。如果没有这个接口，我们需要删除文章，然后再重新上传。所以，我们实现这个功能，方便我们修改文章标题。&lt;/p&gt;
&lt;p&gt;修改文章标题，我打算使用的命令格式如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;gart title uuid newtitle
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，紧挨着 gart 的 title 是命令，表示要更新文章标题，uuid 是文章的主键，可以查询识别是哪篇文章，newtitle 就是要修改的文章标题了。&lt;/p&gt;
&lt;p&gt;实现的代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
func init() {
rootCmd.AddCommand(uptTitleCmd)
}
var uptTitleCmd = &amp;amp;cobra.Command{
Use: &amp;#34;title&amp;#34;,
Short: &amp;#34;更新文章标题，参数需要UUID，新的标题。&amp;#34;,
Long: `更新文章标题，参数需要UUID，新的标题，需要先获取文章的UUID。`,
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
uuid := args[0]
title := args[1]
uar := service.UpdateArtReq{
Uuid: uuid,
Title: title,
UptType: 1,
}
err := service.UpdateArt(token, &amp;amp;uar)
if err != nil {
fmt.Println(&amp;#34;更新标题发生错误,&amp;#34;, err)
} else {
fmt.Println(&amp;#34;更新成功&amp;#34;)
}
},
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。&lt;/p&gt;</description></item><item><title>上传工具实现删除文章</title><link>https://blog.91demo.top/wiki/gupart-delete.html</link><pubDate>Wed, 25 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-delete.html</guid><description>&lt;h2 id="删除文章"&gt;删除文章&lt;/h2&gt;
&lt;p&gt;当上传完文章之后，由于各种原因，会有删除文章的需求。今天我们实现了删除文章的功能。删除文章，需要先知道文章的 UUID，这个可以通过上一节介绍的搜索文章查到。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
func init() {
rootCmd.AddCommand(rmCmd)
}
var rmCmd = &amp;amp;cobra.Command{
Use: &amp;#34;remove&amp;#34;,
Short: &amp;#34;删除文章&amp;#34;,
Long: `删除文章， 需要先获取文章的UUID。`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
uuid := args[0]
err := service.RemoveArt(token, uuid)
if err != nil {
fmt.Println(&amp;#34;删除发生错误,&amp;#34;, err)
} else {
fmt.Println(&amp;#34;删除成功&amp;#34;)
}
},
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实现比较简单，当删除后，可以看命令行输出，也可以去小程序中查看还有没有该篇文章。&lt;/p&gt;</description></item><item><title>上传工具实现查询文章</title><link>https://blog.91demo.top/wiki/gupart-query.html</link><pubDate>Tue, 24 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-query.html</guid><description>&lt;h2 id="查询文章"&gt;查询文章&lt;/h2&gt;
&lt;p&gt;当我们上传了文章之后，肯定希望能够查询我们上传的文章了，查询文章根据用户输入的内容，后台进行文章的标题和关键字进行匹配。然后返回查询到的数据。后台限制最多返回 20 条记录，这是为了服务器性能综合考虑的。如果用户查找的内容，不在返回的记录中，请输入更多内容，进行更精确的匹配。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
func init() {
rootCmd.AddCommand(qCmd)
}
var qCmd = &amp;amp;cobra.Command{
Use: &amp;#34;search&amp;#34;,
Short: &amp;#34;查找文章，最多返回20条记录。&amp;#34;,
Long: `查找文章， 根据文章的标题和关键字匹配查询，最多返回20条记录。可以输入更多的内容进行精确查找。`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
content := args[0]
list, err := service.SearchArt(token, content)
if err != nil {
fmt.Println(&amp;#34;查询发生错误,&amp;#34;, err)
} else {
n := len(list)
if n &amp;gt; 0 {
var (
cts string
uts string
ispub string = &amp;#34;否&amp;#34;
islock string = &amp;#34;否&amp;#34;
)
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{&amp;#34;UUID&amp;#34;, &amp;#34;题目&amp;#34;, &amp;#34;关键字&amp;#34;, &amp;#34;是否公开&amp;#34;, &amp;#34;是否加锁&amp;#34;, &amp;#34;创建时间&amp;#34;, &amp;#34;修改时间&amp;#34;})
for _, v := range list {
cts = utils.TS2Str(v.Createtime)
uts = utils.TS2Str(v.Updatetime)
if v.IsPub == 1 {
ispub = &amp;#34;是&amp;#34;
}
if v.IsLock == 1 {
islock = &amp;#34;是&amp;#34;
}
t.AppendRows([]table.Row{
{v.Uuid, v.Title, v.Keyword, ispub, islock, cts, uts},
})
t.AppendSeparator()
}
t.Render()
} else {
fmt.Println(&amp;#34;未找到记录，尝试更换内容试试。&amp;#34;)
}
}
},
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;代码的组织架构和上传的差不多。都需要先定义命令，然后添加到 root 命令中。这里的输出结果使用美化的 table 显示。当查询到内容后，就可以使用结果中的 UUID 进行文章的管理维护了。&lt;/p&gt;</description></item><item><title>上传工具实现上传文章命令</title><link>https://blog.91demo.top/wiki/gupart-upload.html</link><pubDate>Mon, 23 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-upload.html</guid><description>&lt;h2 id="上传文章"&gt;上传文章&lt;/h2&gt;
&lt;p&gt;定义上传文章的 command，并将 command 添加到 Root 中。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func init() {
rootCmd.AddCommand(upCmd)
}
var upCmd = &amp;amp;cobra.Command{
Use: &amp;#34;upload&amp;#34;,
Short: &amp;#34;上传文章，可在豆子碎片小程序中查看。&amp;#34;,
Long: `上传文章，可在豆子碎片小程序中查看。参数依次为题目，关键字，Markdown文件，是否公开，是否加锁。
参数题目，关键字，Markdown文件必填，是否公开，是否加锁选填，默认为否。
例如：gart upload 上传示例1 命令行，工具 ./example.md
`,
Args: cobra.RangeArgs(3, 5),
Run: func(cmd *cobra.Command, args []string) {
var (
title string
keyword string
filename string
ispub int
islock int
)
title = args[0]
keyword = args[1]
filename = args[2]
l := len(args)
switch l {
case 4:
ispub = utils.Str2Int(args[3])
case 5:
ispub = utils.Str2Int(args[3])
islock = utils.Str2Int(args[4])
}
fmt.Println(&amp;#34;上传参数如下：&amp;#34;)
var isPubStr string = &amp;#34;否&amp;#34;
var isLockStr string = &amp;#34;否&amp;#34;
if ispub == 1 {
isPubStr = &amp;#34;是&amp;#34;
}
if islock == 1 {
isLockStr = &amp;#34;是&amp;#34;
}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{&amp;#34;题目&amp;#34;, &amp;#34;关键字&amp;#34;, &amp;#34;文件名称&amp;#34;, &amp;#34;是否公开&amp;#34;, &amp;#34;是否加锁&amp;#34;})
t.AppendRows([]table.Row{
{title, keyword, filename, isPubStr, isLockStr},
})
t.AppendSeparator()
t.Render()
err := service.UploadArt(token, title, keyword, filename, ispub, islock)
if err != nil {
fmt.Println(&amp;#34;上传发生错误,&amp;#34;, err)
} else {
fmt.Println(&amp;#34;上传成功&amp;#34;)
}
},
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在上面的实现中，使用了 cobra 参数约束函数，&lt;code&gt;Args: cobra.RangeArgs(3, 5),&lt;/code&gt;，保证参数最少 3 个，最多不能超过 5 个。这完全满足我的需求。&lt;/p&gt;</description></item><item><title>上传工具使用Cobra实现添加更多的命令</title><link>https://blog.91demo.top/wiki/gupart-cobra.html</link><pubDate>Sun, 22 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart-cobra.html</guid><description>&lt;h2 id="添加更多的命令"&gt;添加更多的命令&lt;/h2&gt;
&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;</description></item><item><title>Go 版本文章上传命令行工具</title><link>https://blog.91demo.top/wiki/gupart.html</link><pubDate>Sat, 21 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/gupart.html</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>Rust 版本文章上传命令行工具</title><link>https://blog.91demo.top/wiki/rupart.html</link><pubDate>Mon, 21 Aug 2023 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/rupart.html</guid><description>&lt;p&gt;这个工具使用Rust开发，仅实现了上传文章功能，使用 GUI 桌面开发。&lt;/p&gt;
&lt;p&gt;该工具使用的库为 nwg，即 native-windows-gui 库，该库支持老的 Windows GUI。&lt;/p&gt;
&lt;p&gt;这应该算是一个半成品，这是 Rust 实现的一个桌面上传文章工具。它只有上传功能，之所以会这样，是因为后续的开发重心都放在go版本实现上。&lt;/p&gt;
&lt;p&gt;不过这也有一定的价值，首先就是它实现了图形桌面，这个可以学会如何实现一个窗口，显示输入框和按钮，如何布局。然后呢，可以在Rust中使用HTTP上传文件，这也是一个非常棒的技能点。&lt;/p&gt;
&lt;p&gt;以后有精力了，可以扩展完善一下。&lt;/p&gt;
&lt;h2 id="项目地址"&gt;项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gitee.com/littletow/upart-rs"&gt;https://gitee.com/littletow/upart-rs&lt;/a&gt;&lt;/p&gt;</description></item><item><title>语音验证码实现中常见错误</title><link>https://blog.91demo.top/wiki/errors.html</link><pubDate>Wed, 24 Aug 2022 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/errors.html</guid><description>&lt;p&gt;403 登录失败&lt;/p&gt;
&lt;p&gt;解决：等待 1 分钟，等超时之后重新连接即可。一般是切换 IP 时或者换软电话登录会碰到。&lt;/p&gt;
&lt;p&gt;401 登录失败&lt;/p&gt;
&lt;p&gt;解决：检查账户和密码是否正确？检查连接地址是否正确？检查 Asterisk 是否启动？检查防火墙端口是否打开？&lt;/p&gt;
&lt;p&gt;408 连接超时&lt;/p&gt;
&lt;p&gt;解决：一般是 Asterisk 服务不通，检查 Asterisk 服务是否启动，检查防火墙是否打开？&lt;/p&gt;
&lt;p&gt;可以拨通，没有声音？&lt;/p&gt;
&lt;p&gt;一般是 NAT 造成，配置这三个参数：rtp_symmetric，force_rport，rewrite_contact。&lt;/p&gt;
&lt;p&gt;SIP 客户端没有自动挂机？&lt;/p&gt;
&lt;p&gt;一般是没有设置 stun 造成的，在 SIP 客户端，设置 stun 即可。如果没有 stun server，可以设置这个&lt;code&gt;stun.l.google.com:19302&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;AGI 脚本返回状态 4，正常应该为 0？&lt;/p&gt;
&lt;p&gt;查看网上资料，是 AGI 脚本中调用 Hangup 导致，将脚本中的 Hangup 去掉，放在拨号计划配置文件中执行 Hangup，可以解决这个问题。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AGI Script agidemo completed, returning 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看客户端是否在线？&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;pjsip show endpoints
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;要求最大一个客户端在线，请在aors配置中使用配置&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;max_contacts 设置为 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MicroSIP客户端配置后还显示IDLE？&lt;/p&gt;
&lt;p&gt;SIP SERVER字段必须填写，和域名地址保持一致。这样状态IDLE会变为ONline。&lt;/p&gt;
&lt;p&gt;MicroSIP客户端连接后，Asterisk不显示在线？&lt;/p&gt;
&lt;p&gt;如果是服务器在公网，客户端在局域网的网络结构：&lt;br&gt;
我的Asterisk显示的客户端Contact是局域网地址，针对这种情况。&lt;br&gt;
1，临时解决方法：请在账户配置中，勾选IP REWRITE或ICE，具体选择哪个看服务端的配置。他们分别表示IP地址重写和启用更智能的 NAT 穿透技术。如果需要向好友显示在线状态，请勾选Publish Presence。&lt;br&gt;
2，更推荐的解决方法：检查Asterisk的PJSIP配置，需要添加 &lt;code&gt;rewrite_contact=yes&lt;/code&gt;这是解决 NAT 的“银弹”。它告诉 Asterisk：“忽略报文里写的内网 IP，直接回复给数据包发过来的那个公网地址和端口。”&lt;br&gt;
设置 &lt;code&gt;rtp_symmetric=yes&lt;/code&gt;：确保语音流（RTP）也走同样的路径，防止通话时单通（没声音）。&lt;/p&gt;</description></item><item><title>私有化语音验证码方案：基于 Asterisk 与 Go 的 SIP 通信及 AGI 脚本实战</title><link>https://blog.91demo.top/go/vcode.html</link><pubDate>Sun, 21 Aug 2022 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/go/vcode.html</guid><description>&lt;h2 id="音频文件处理"&gt;音频文件处理&lt;/h2&gt;
&lt;p&gt;我们使用手机上的录音机来录制音频文件。&lt;br&gt;
Android 录音机录制的音频文件格式为 mp3，如果是 amr 格式，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。&lt;br&gt;
IOS 录音机录制的音频文件格式为 m4a，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。&lt;/p&gt;
&lt;p&gt;我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能后续会集成到豆子工具中。&lt;/p&gt;
&lt;p&gt;mp3 转 g711a 命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用 ffplay 播放测试&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffplay -i test.alaw -f alaw -ac 1 -ar 8000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将制作好的音频文件存放在 asterisk sounds 目录，就可以在拨号计划中使用 Playback 应用调用它了。&lt;/p&gt;
&lt;h2 id="配置实时数据库"&gt;配置实时数据库&lt;/h2&gt;
&lt;p&gt;今天讲解 Asterisk 如何实时将 SIP 用户写入 sqlite3 数据库。&lt;/p&gt;
&lt;h3 id="先定义数据库表结构我当前使用的-pjsip-协议"&gt;先定义数据库表结构，我当前使用的 PJSIP 协议。&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
CREATE TABLE ps_endpoints (
id VARCHAR(40) NOT NULL,
transport VARCHAR(40),
aors VARCHAR(200),
auth VARCHAR(40),
context VARCHAR(40),
disallow VARCHAR(200),
allow VARCHAR(200),
direct_media varchar(5) check(direct_media in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
connected_line_method varchar(10) check(connected_line_method in (&amp;#39;invite&amp;#39;,&amp;#39;reinvite&amp;#39;,&amp;#39;update&amp;#39;)),
direct_media_method varchar(10) check(direct_media_method in (&amp;#39;invite&amp;#39;,&amp;#39;reinvite&amp;#39;,&amp;#39;update&amp;#39;)),
direct_media_glare_mitigation varchar(20) check(direct_media_glare_mitigation in (&amp;#39;none&amp;#39;,&amp;#39;outgoing&amp;#39;,&amp;#39;incoming&amp;#39;)),
disable_direct_media_on_nat varchar(5) check(disable_direct_media_on_nat in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
dtmf_mode varchar(20) check(dtmf_mode in (&amp;#39;rfc4733&amp;#39;,&amp;#39;inband&amp;#39;,&amp;#39;info&amp;#39;)),
external_media_address VARCHAR(40),
force_rport varchar(5) check(force_rport in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
ice_support varchar(5) check(ice_support in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
identify_by varchar(10) check(identify_by in (&amp;#39;username&amp;#39;)),
mailboxes VARCHAR(40),
moh_suggest VARCHAR(40),
outbound_auth VARCHAR(40),
outbound_proxy VARCHAR(40),
rewrite_contact varchar(5) check(rewrite_contact in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
rtp_ipv6 varchar(5) check(rtp_ipv6 in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
rtp_symmetric varchar(5) check(rtp_symmetric in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
send_diversion varchar(5) check(send_diversion in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
send_pai varchar(5) check(send_pai in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
send_rpid varchar(5) check(send_rpid in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
timers_min_se INTEGER,
timers varchar(20) check(timers in (&amp;#39;forced&amp;#39;,&amp;#39;no&amp;#39;,&amp;#39;required&amp;#39;,&amp;#39;yes&amp;#39;)),
timers_sess_expires INTEGER,
callerid VARCHAR(40),
callerid_privacy varchar(40) check(callerid_privacy in (&amp;#39;allowed_not_screened&amp;#39;,&amp;#39;allowed_passed_screened&amp;#39;,&amp;#39;allowed_failed_screened&amp;#39;,&amp;#39;allowed&amp;#39;,&amp;#39;prohib_not_screened&amp;#39;,&amp;#39;prohib_passed_screened&amp;#39;,&amp;#39;prohib_failed_screened&amp;#39;,&amp;#39;prohib&amp;#39;,&amp;#39;unavailable&amp;#39;)),
callerid_tag VARCHAR(40),
`100rel` varchar(20) check(`100rel` in (&amp;#39;no&amp;#39;,&amp;#39;required&amp;#39;,&amp;#39;yes&amp;#39;)),
aggregate_mwi varchar(5) check(aggregate_mwi in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
trust_id_inbound varchar(5) check(trust_id_inbound in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
trust_id_outbound varchar(5) check(trust_id_outbound in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
use_ptime varchar(5) check(use_ptime in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
use_avpf varchar(5) check(use_avpf in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
media_encryption varchar(10) check(media_encryption in (&amp;#39;no&amp;#39;,&amp;#39;sdes&amp;#39;,&amp;#39;dtls&amp;#39;)),
inband_progress varchar(5) check(inband_progress in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
call_group VARCHAR(40),
pickup_group VARCHAR(40),
named_call_group VARCHAR(40),
named_pickup_group VARCHAR(40),
device_state_busy_at INTEGER,
fax_detect varchar(5) check(fax_detect in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
t38_udptl varchar(5) check(t38_udptl in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
t38_udptl_ec varchar(20) check(t38_udptl_ec in (&amp;#39;none&amp;#39;,&amp;#39;fec&amp;#39;,&amp;#39;redundancy&amp;#39;)),
t38_udptl_maxdatagram INTEGER,
t38_udptl_nat varchar(5) check(t38_udptl_nat in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
t38_udptl_ipv6 varchar(5) check(t38_udptl_ipv6 in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
tone_zone VARCHAR(40),
language VARCHAR(40),
one_touch_recording varchar(5) check(one_touch_recording in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
record_on_feature VARCHAR(40),
record_off_feature VARCHAR(40),
rtp_engine VARCHAR(40),
allow_transfer varchar(5) check(allow_transfer in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
allow_subscribe varchar(5) check(allow_subscribe in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
sdp_owner VARCHAR(40),
sdp_session VARCHAR(40),
tos_audio INTEGER,
tos_video INTEGER,
cos_audio INTEGER,
cos_video INTEGER,
sub_min_expiry INTEGER,
from_domain VARCHAR(40),
from_user VARCHAR(40),
mwi_fromuser VARCHAR(40),
dtls_verify VARCHAR(40),
dtls_rekey VARCHAR(40),
dtls_cert_file VARCHAR(200),
dtls_private_key VARCHAR(200),
dtls_cipher VARCHAR(200),
dtls_ca_file VARCHAR(200),
dtls_ca_path VARCHAR(200),
dtls_setup varchar(20) check(dtls_setup in (&amp;#39;active&amp;#39;,&amp;#39;passive&amp;#39;,&amp;#39;actpass&amp;#39;)),
srtp_tag_32 varchar(5) check(srtp_tag_32 in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
UNIQUE (id)
);
CREATE INDEX ps_endpoints_id ON ps_endpoints (id);
CREATE TABLE ps_auths (
id VARCHAR(40) NOT NULL,
auth_type varchar(10) check(auth_type in (&amp;#39;md5&amp;#39;,&amp;#39;userpass&amp;#39;)),
nonce_lifetime INTEGER,
md5_cred VARCHAR(40),
password VARCHAR(80),
realm VARCHAR(40),
username VARCHAR(40),
UNIQUE (id)
);
CREATE INDEX ps_auths_id ON ps_auths (id);
CREATE TABLE ps_aors (
id VARCHAR(40) NOT NULL,
contact VARCHAR(40),
default_expiration INTEGER,
mailboxes VARCHAR(80),
max_contacts INTEGER,
minimum_expiration INTEGER,
remove_existing varchar(5) check(remove_existing in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
qualify_frequency INTEGER,
authenticate_qualify varchar(5) check(authenticate_qualify in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
UNIQUE (id)
);
CREATE INDEX ps_aors_id ON ps_aors (id);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;PJSIP 测试数据：&lt;/p&gt;</description></item><item><title>如何实现自助语音验证码？</title><link>https://blog.91demo.top/wiki/implast.html</link><pubDate>Sat, 20 Aug 2022 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/implast.html</guid><description>&lt;p&gt;用户通过 VOIP 电话连接到 Asterisk，拨打固定的数字 8000，Asterisk 将调用 AGI 接口获取验证码，然后将获取到的验证码播报给用户，播报完毕后自动挂断。&lt;br&gt;
我们根据自助语音验证码原理，将自助验证码实现分解为以下几个技术要点，只要我们解决以下技术要点，就可以实现自助语音验证码。&lt;/p&gt;
&lt;p&gt;技术要点：&lt;/p&gt;
&lt;p&gt;1，如何播放音频文件？音频文件从哪里来？如何播放动态数据？&lt;/p&gt;
&lt;p&gt;2，如何存储用户信息？如何使用数据库存储用户信息？&lt;/p&gt;
&lt;p&gt;3，如何获取验证码？&lt;/p&gt;
&lt;p&gt;下面内容是我对各个技术点的对应解决方案。&lt;/p&gt;
&lt;p&gt;1，Asterisk 自带 Playback 应用，可以通过它播放音频文件。音频文件需要我们提前录制好，并且转换为对应的音频格式，最简单的方法就是使用手机上的录音机。当在拨号计划中多次执行 Playback 应用，Asterisk 会将音频流自动连接起来。所以我们可以使用循环多次执行 Playback 应用即可。&lt;/p&gt;
&lt;p&gt;2，Asterisk 使用配置文件写入 SIP 用户信息，但当写入新的 SIP 用户信息后，需要重新加载配置文件。为了方便和第三方对接，我们推荐使用数据库。Asterisk 支持数据库，并且可以实时获取用户信息。&lt;/p&gt;
&lt;p&gt;3，Asterisk 支持 AGI 接口，我们可以使用 AGI 获取第三方应用的验证码，获取后和提前录制好的文件结合起来进行播放。&lt;/p&gt;
&lt;p&gt;当掌握了这些技术点后，我们就可以灵活应用到其它解决方案。&lt;/p&gt;</description></item><item><title>使用Rust基于 Native-Windows-GUI 构建的轻量小程序码生成工具</title><link>https://blog.91demo.top/rust/mpcode-intro.html</link><pubDate>Tue, 02 Nov 2021 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/rust/mpcode-intro.html</guid><description>早于官方后台功能的桌面实践：探索使用 Rust + native-windows-gui 打造极致轻量的小程序工具。</description></item><item><title>构建高可靠 APK 上传工具，彻底终结网络超时与手误引发的生产事故</title><link>https://blog.91demo.top/rust/apkup-rs.html</link><pubDate>Thu, 02 Jan 2020 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/rust/apkup-rs.html</guid><description>记录一个未公开的 Rust 实战小工具，分享如何通过技术手段解决网络超时与手误操作带来的生产事故。</description></item><item><title>豆子碎片小程序项目第一版</title><link>https://blog.91demo.top/wiki/v1.html</link><pubDate>Thu, 03 Jan 2019 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/v1.html</guid><description>&lt;h2 id="启动项目"&gt;启动项目&lt;/h2&gt;
&lt;p&gt;在学习了微信小程序之后，我使用wxml页面可以做出唐诗的内容了。结合wxss，我可以定义好看的布局。在小程序需要备案时，因为唐诗需要资质，而我个人无法满足这些条件，所以我需要开发新的项目。恰好最近在为我的技术笔记发愁。我希望有一个可以展示我笔记内容的工具。而visit 就是一个用于展示和搜索文章的小程序工具。&lt;/p&gt;
&lt;h3 id="项目需求"&gt;项目需求&lt;/h3&gt;
&lt;p&gt;我的最初想法非常简单，1，可以更新文章内容而不需要升级小程序。2，需要在小程序端进行展示。3，能够根据标题或者关键字进行搜索文章内容。&lt;/p&gt;
&lt;h3 id="技术选型"&gt;技术选型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;小程序使用原生开发&lt;/li&gt;
&lt;li&gt;界面 UI 使用 WeUI&lt;/li&gt;
&lt;li&gt;内容渲染使用Markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="知识储备"&gt;知识储备&lt;/h3&gt;
&lt;p&gt;需要你了解微信小程序开发基础知识，Markdown内容编写，以及towxml库。&lt;/p&gt;
&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;搜索框、按钮、列表、文本&lt;/li&gt;
&lt;li&gt;界面交互&lt;/li&gt;
&lt;li&gt;网络请求、数据存储&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在微信开发者工具，app.json 文件中配置首页，配置后会自动生成首页模板页面。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;pages&amp;#34;: [
&amp;#34;pages/index/index&amp;#34;
],
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 pages/index/index.wxml 文件中，编写首页内容。包含标题，描述，搜索框，以及快捷按钮。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&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;收集了很多经典的代码片段和库，是学习编程的好工具。可通过关键字查找内容，用于解决开发中遇到的问题。&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;page__bd page__bd_spacing&amp;#34;&amp;gt;
&amp;lt;!-- 搜索框--&amp;gt;
&amp;lt;view class=&amp;#34;weui-search-bar {{inputShowed ? &amp;#39;weui-search-bar_focusing&amp;#39; : &amp;#39;&amp;#39;}}&amp;#34; id=&amp;#34;searchBar&amp;#34;&amp;gt;
&amp;lt;form class=&amp;#34;weui-search-bar__form&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-search-bar__box&amp;#34;&amp;gt;
&amp;lt;i class=&amp;#34;weui-icon-search&amp;#34;&amp;gt;&amp;lt;/i&amp;gt;
&amp;lt;input type=&amp;#34;text&amp;#34; confirm-type=&amp;#34;search&amp;#34; class=&amp;#34;weui-search-bar__input&amp;#34; placeholder=&amp;#34;请输入您要查找的内容&amp;#34; value=&amp;#34;{{inputVal}}&amp;#34; focus=&amp;#34;{{inputShowed}}&amp;#34; bindinput=&amp;#34;inputTyping&amp;#34; bindconfirm=&amp;#34;search&amp;#34; /&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;label class=&amp;#34;weui-search-bar__label&amp;#34; bindtap=&amp;#34;showInput&amp;#34;&amp;gt;
&amp;lt;i class=&amp;#34;weui-icon-search&amp;#34;&amp;gt;&amp;lt;/i&amp;gt;
&amp;lt;span class=&amp;#34;weui-search-bar__text&amp;#34;&amp;gt;搜索&amp;lt;/span&amp;gt;
&amp;lt;/label&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;view class=&amp;#34;weui-search-bar__cancel-btn&amp;#34; bindtap=&amp;#34;hideInput&amp;#34;&amp;gt;取消&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;!-- 查询快捷按钮--&amp;gt;
&amp;lt;view class=&amp;#34;weui-flex&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-flex__item&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;placeholder&amp;#34; id=&amp;#34;btnHot&amp;#34; bindtap=&amp;#34;bindBtn&amp;#34; hover-class=&amp;#34;placeholder-hover&amp;#34;&amp;gt;最火&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-flex__item&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;placeholder&amp;#34; id=&amp;#34;btnNew&amp;#34; bindtap=&amp;#34;bindBtn&amp;#34; hover-class=&amp;#34;placeholder-hover&amp;#34;&amp;gt;最新&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-flex__item&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;placeholder&amp;#34; id=&amp;#34;btnCold&amp;#34; bindtap=&amp;#34;bindBtn&amp;#34; hover-class=&amp;#34;placeholder-hover&amp;#34;&amp;gt;最冷&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;!-- 内容--&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当使用模拟器预览时，发现首页内容是正确的，但是样式非常丑， 我使用 WeUI 来美化一下，使用weui是因为虽然它不是最美，但它的兼容性和稳定性最好，如果自己写wxss，需要考虑各种机型和设备。&lt;/p&gt;</description></item><item><title>Wander 项目功能列表状态--列表形式</title><link>https://blog.91demo.top/wiki/wfuncs.html</link><pubDate>Sun, 25 Feb 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/wfuncs.html</guid><description>&lt;p&gt;记录曾经存在的功能，列表形式展示。&lt;/p&gt;
&lt;h2 id="音频转-mp3"&gt;音频转 MP3&lt;/h2&gt;
&lt;p&gt;可以将 IOS 的录音文件 m4a 格式转换为 mp3 格式，在老设备播放，或者其它特殊应用场景。&lt;/p&gt;
&lt;p&gt;使用方法：搜索豆子工具小程序，点击音频格式转换。&lt;/p&gt;
&lt;p&gt;推荐理由：方便用户在手机上操作，可以转换音频，还可以试听效果。&lt;/p&gt;
&lt;h2 id="获取-ast-账户"&gt;获取 AST 账户&lt;/h2&gt;
&lt;p&gt;AST 账户用于连接 Asterisk，是自助语音验证码重要的一个环节。&lt;/p&gt;
&lt;p&gt;你可以搜索【豆子工具】小程序，获取自己的 AST 账户。&lt;/p&gt;
&lt;p&gt;注意：AST 账户获取后并不是马上生效，它需要到次日方可使用。这是因为它在凌晨会进行一次同步，会同步到 Asterisk 数据库。&lt;/p&gt;
&lt;p&gt;后续我们会讲下如何实现自助语音验证码。&lt;/p&gt;
&lt;h2 id="获取识别码"&gt;获取识别码&lt;/h2&gt;
&lt;p&gt;识别码用于开放接口认证。专属 ID 码为自定义识别码，方便用户记忆。&lt;/p&gt;
&lt;p&gt;目前开放接口为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取本机公网 IP&lt;/li&gt;
&lt;li&gt;获取 IP 地址的归属地&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以搜索【豆子工具】小程序，获取自己的识别码。&lt;/p&gt;
&lt;p&gt;然后使用你喜爱的编程语言调用开放接口。&lt;/p&gt;
&lt;p&gt;开放接口 API 请查看【豆子笔记】网站。&lt;/p&gt;
&lt;h2 id="获取本机局域网-ip"&gt;获取本机局域网 IP&lt;/h2&gt;
&lt;p&gt;状态：上架&lt;/p&gt;
&lt;p&gt;上架原因：有适用场景，可用于获取手机的局域网 IP，排查问题时使用。&lt;/p&gt;
&lt;p&gt;实现原理：调用小程序的本地获取 IP API。&lt;/p&gt;
&lt;h2 id="获取网络打印机地址"&gt;获取网络打印机地址&lt;/h2&gt;
&lt;p&gt;状态：上架&lt;/p&gt;
&lt;p&gt;上架原因：有适用场景，可用于获取局域网的网络打印机 IP 地址。&lt;/p&gt;
&lt;p&gt;实现原理：调用小程序的 mdns API。&lt;/p&gt;
&lt;h2 id="获取验证码"&gt;获取验证码&lt;/h2&gt;
&lt;p&gt;验证码用于访问豆子笔记网站认证使用。它是一次性码，用完即作废，5 分钟内有效。&lt;/p&gt;
&lt;p&gt;豆子笔记网址：www.91demo.top&lt;/p&gt;
&lt;p&gt;除了在豆子工具小程序获取验证码外，还有一个自助语音验证码，使用 Asterisk 实现。我们会在后续的章节进行讲解。&lt;/p&gt;
&lt;h2 id="获取豆子点数"&gt;获取豆子点数&lt;/h2&gt;
&lt;p&gt;观看激励广告可获得豆子点数。&lt;/p&gt;
&lt;p&gt;豆子点数用于使用小工具。添加豆子点数机制，是为了更好的促进豆子工具良性的发展。&lt;/p&gt;
&lt;p&gt;除了观看激励广告获取豆子点数外，还可以使用九宫格大转盘抽取豆子点数，&lt;br&gt;
报 IP 地址归属地也可以获取豆子点数奖励。&lt;/p&gt;</description></item><item><title>实战笔记：为了管好那堆记不住的密码，我给自己写了个全加密密码本</title><link>https://blog.91demo.top/wiki/tool-pwbook.html</link><pubDate>Thu, 11 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-pwbook.html</guid><description>深度解析一款基于 RSA+AES 离线加密算法的小程序密码本开发心路历程：从 Excel 到 Flutter 再到小程序的进化。</description></item><item><title>实战笔记：为了不再漏掉任何一个域名到期提醒，我做了个自动化检测工具</title><link>https://blog.91demo.top/wiki/tool-chkdomain.html</link><pubDate>Wed, 10 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-chkdomain.html</guid><description>记录如何通过 Go 语言实现多域名 SSL 证书到期自动巡检，并通过钉钉/企业微信机器人实现精准预警。</description></item><item><title>实战笔记：为了不再发错下载链接，我给工具箱加了“扫码鉴定”</title><link>https://blog.91demo.top/wiki/tool-scan.html</link><pubDate>Tue, 09 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-scan.html</guid><description>记录一次因二维码链接填错导致的事故，以及如何利用小程序扫码能力快速校验二维码内容的真实性。</description></item><item><title>实战笔记：当我想查端口却没装 Telnet 时，我决定自己写个工具</title><link>https://blog.91demo.top/wiki/tool-chkport.html</link><pubDate>Mon, 08 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-chkport.html</guid><description>记录因 Windows 缺少 Telnet 组件引发的思考：如何利用 Go + 小程序实现跨平台的远程端口连通性检测。</description></item><item><title>实战笔记：从一个按钮到全能生成器，随机数功能的“进化论”</title><link>https://blog.91demo.top/wiki/tool-random.html</link><pubDate>Sun, 07 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-random.html</guid><description>复盘随机数功能从单一按钮到支持自定义长度、字符集及格式处理的进化过程。</description></item><item><title>实战笔记：知识真的就是金钱，聊聊我的局域网调试工具</title><link>https://blog.91demo.top/wiki/tool-tcp.html</link><pubDate>Sat, 06 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/tool-tcp.html</guid><description>记录我如何通过小程序 TCP/UDP 调试工具赚到第一桶金的故事，以及技术方案如何转化为实际生产力。</description></item><item><title>实战笔记：为了省下服务器流量费，我给小程序加上了 Webp 转换</title><link>https://blog.91demo.top/wiki/image-convert.html</link><pubDate>Fri, 05 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/image-convert.html</guid><description>在“流量贵如油”的今天，作为一名自己买服务器、撸代码的站长，如何给服务器“减负”是每天都要思考的必修课。</description></item><item><title>实战笔记：我把 FFmpeg 搬进小程序，搞定了音频格式转换</title><link>https://blog.91demo.top/wiki/audio-convert.html</link><pubDate>Thu, 04 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/audio-convert.html</guid><description>在“豆子工具”众多的功能里，**音频转换（m4a 转 mp3）** 是我使用频率最高、也最具有“个人救赎”色彩的一个。</description></item><item><title>八年了，从一个随机数按钮到我的技术底座：豆子工具复盘</title><link>https://blog.91demo.top/wiki/wander-intro.html</link><pubDate>Wed, 03 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/wiki/wander-intro.html</guid><description>有些事，不做笔记真的意识不到已经过去了这么久。</description></item></channel></rss>