<?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>小程序 on 豆子技术站</title><link>https://blog.91demo.top/tags/%E5%B0%8F%E7%A8%8B%E5%BA%8F/</link><description>Recent content in 小程序 on 豆子技术站</description><generator>Hugo -- 0.155.1</generator><language>zh-cn</language><lastBuildDate>Sun, 24 May 2026 06:40:16 +0800</lastBuildDate><atom:link href="https://blog.91demo.top/tags/%E5%B0%8F%E7%A8%8B%E5%BA%8F/index.xml" rel="self" type="application/rss+xml"/><item><title>从 wasm-bindgen 到 C-ABI 写微信小程序WASM：一次真实的 WASM 落地复盘</title><link>https://blog.91demo.top/wx-wasm/</link><pubDate>Sun, 24 May 2026 06:40:16 +0800</pubDate><guid>https://blog.91demo.top/wx-wasm/</guid><description>&lt;blockquote&gt;
&lt;p&gt;在写完 ESP8266 IoT 架构之后，我就在想一件事情：既然代码开源，IoT 网关岂不是谁都能连？&lt;br&gt;
于是我试图在微信小程序端做文章。但开源的小程序不能硬编码密钥，也不能仅靠接口鉴权。&lt;br&gt;
我想到了很早之前预研的 WebAssembly（WASM）。配合后端防护，可以大幅提升攻击者的成本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一背景与目标"&gt;一、背景与目标&lt;/h2&gt;
&lt;p&gt;实现的功能非常简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;传入 &lt;strong&gt;小程序 APPID + 设备 ID + 时间戳&lt;/strong&gt;，使用 &lt;strong&gt;Ed25519&lt;/strong&gt; 签名，返回 Token。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以前在微信小程序调通过 &lt;code&gt;WxWebAssembly&lt;/code&gt;（简称 wxwasm），但当时主要处理 Int 类型，这次需要返回字符串。&lt;/p&gt;
&lt;p&gt;当我拿出以前的预研代码进行修改时，我才意识到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“能用 Rust 写 WASM”&lt;/strong&gt; 和 &lt;strong&gt;“能在微信小程序里稳定运行”&lt;/strong&gt;，完全是两回事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="二第一阶段理想很丰满--rust--wasm-bindgen"&gt;二、第一阶段：理想很丰满 —— Rust + wasm-bindgen&lt;/h2&gt;
&lt;p&gt;最初的环境是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust &lt;strong&gt;1.95&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wasm-bindgen&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wasm-pack&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cargotoml"&gt;Cargo.toml&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;dependencies&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;wasm-bindgen&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0.2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ed25519-dalek&lt;/span&gt; = { &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;2.1&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;features&lt;/span&gt; = [&lt;span style="color:#e6db74"&gt;&amp;#34;rand_core&amp;#34;&lt;/span&gt;] }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;hex&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0.4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="librs"&gt;lib.rs&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[wasm_bindgen]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;generate_token&lt;/span&gt;(appid: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;, device_id: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;, timestamp: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String, JsValue&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Ed25519 签名逻辑
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;编译很顺利（就是有点慢 😭），本地 &lt;code&gt;test.html&lt;/code&gt; 测试一切正常。&lt;/p&gt;</description></item><item><title>一步一步将ESP8266 WIFI 配网搞定并丝滑体验</title><link>https://blog.91demo.top/esp-wifi-config/</link><pubDate>Wed, 29 Apr 2026 22:22:34 +0000</pubDate><guid>https://blog.91demo.top/esp-wifi-config/</guid><description>&lt;p&gt;准备入手ESP网关项目了，手头有一块esp8266的开发小板子，就先从esp8266开始入手。白天搞定了ESP8266 的UDP配网编码和小程序端的控制编码。就一个功能模块给ESP8266配置WIFI网络。这是必须的入门操作，因为WIFI 的SSID和密码不能真的写入到代码中。&lt;/p&gt;
&lt;p&gt;网络配置有很多种方式，我选择了&lt;code&gt;SoftAP配网&lt;/code&gt;，虽然配置有点繁琐，但这是一种很可靠的配网方案，兼容性极高，不依赖手机硬件的特殊协议。以前做过HTTP配网，这是2025年时做的，到现在已经忘的差不多了。现在捡起来再回忆一下。&lt;/p&gt;
&lt;p&gt;准确来说，这不应该算是一个项目，但因为它具有通用性和实用性。我决定把它记录下来，可以方便地应用到我的其它项目中。&lt;/p&gt;
&lt;h2 id="soft配网原理"&gt;Soft配网原理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;初始化模式：ESP8266 启动后进入 WIFI_AP_STA 模式。它会开启一个无密码（或已知密码）的热点（AP），并运行一个轻量级的 Web 服务器（HTTP Server）。&lt;/li&gt;
&lt;li&gt;通道建立：手机通过小程序或系统设置连接到该热点。此时手机与 ESP8266 处于同一个局域网内。&lt;/li&gt;
&lt;li&gt;数据交互：小程序通过 HTTP Post 请求将目标 WiFi 的 SSID 和 Password 发送给 ESP8266 的固定接口（如 /config）。&lt;/li&gt;
&lt;li&gt;校验与切换：ESP8266 收到参数后，尝试作为客户端（STA）连接路由器。如果连接成功，则关闭 AP 热点，保存参数到 Flash；如果失败，则返回并重置到AP状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="esp8266端核心代码arduino-ide和小程序端代码"&gt;ESP8266端核心代码（Arduino IDE）和小程序端代码&lt;/h2&gt;
&lt;p&gt;在Arduino IDE打开项目后，需要安装ESP8266 WebServer库。这个库提供了Web服务，可以接收其它HTTP客户端的连接。&lt;/p&gt;
&lt;p&gt;它的核心是实现了一个非常简单的Web服务，它监听&lt;code&gt;/config&lt;/code&gt;接口并能够接收WIFI信息，以及接收之后可以本地存储，实现动态配置的功能。&lt;/p&gt;
&lt;p&gt;下面是Arduino端的代码片段：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#include &amp;lt;ESP8266WiFi.h&amp;gt;
#include &amp;lt;ESP8266WebServer.h&amp;gt;
// 定义AP热点的名称
const char* ap_ssid = &amp;#34;ESP8266_Config_Device&amp;#34;;
ESP8266WebServer server(80);
void handleConfig() {
// 处理SSID和密码逻辑
}
void setup() {
// 设置为AP+STA模式
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(ap_ssid);
// 注册接口
server.on(&amp;#34;/config&amp;#34;, HTTP_POST, handleConfig);
server.begin();
}
void loop() {
server.handleClient();
if (WiFi.status() == WL_CONNECTED) {
// 如果连接成功，可以根据需要在这里处理业务逻辑
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;完整代码可以从&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;下载。小程序端则充当了HTTP客户端，它用来作为输入WIFI信息的终端。这个界面非常简单，一个WiFi信息提交表单，包含了SSID信息和密码信息，以及一个提交按钮。&lt;/p&gt;</description></item><item><title>对于微信小程序的一些回忆和思考</title><link>https://blog.91demo.top/mp-story/</link><pubDate>Fri, 24 Apr 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/mp-story/</guid><description>&lt;p&gt;有些事，不做笔记真的意识不到已经过去了这么久。&lt;/p&gt;
&lt;p&gt;最近在整理博客内容时，我翻到了&lt;strong&gt;小程序&lt;/strong&gt;的最早版本记录。那是 2018 年，最初的它简陋得甚至有点滑稽：整个页面只有一个按钮，点一下，生成一个随机数。&lt;/p&gt;
&lt;h2 id="一豆子工具小程序"&gt;一、豆子工具小程序&lt;/h2&gt;
&lt;p&gt;这个小程序项目名称“Wander”，它的小程序名称换了很多，只有到微信开始要求备案和实名后，才开始真正的确定下来。&lt;/p&gt;
&lt;p&gt;这几年里，我断断续续的维护着这个项目，是由于自己没事的时候喜欢做一些东西，当忙起来时又会忘记了它，当有新的想法时，会改变它，然后更换微信小程序名称。印象深刻的有几次：1，随机数，纯粹是为了小程序上线，为了学习小程序，做了最简单的示例，然后上线了。2，图片集，制作了一个图片列表，然后展示图片，图片存储在云服务上，那是还是免费，并且提供两个环境，为了学习云存储，云函数。3，找厕所，这是我最费心最耗时最吃力的一次改进，为了学习地图map，最后没有完成放弃了。4，自用工具，音频格式转换，图片格式转换，MD5哈希等小工具，从这之后应该算是入门了吧，毕竟以前是自学，现在因为有了工作经历，感觉自己进步了，思维和技巧不在停留在简单的示例教程上。5，工具+项目，以前自学的一些项目以及现在新做的模块都包含进了这个小程序，工具还是自己在用，项目呢，可以用来分享给有需要的人。&lt;/p&gt;
&lt;p&gt;在这几年里，我在这款小程序上倾注了太多的时间和精力，看起来是总在变化，没有一个明确的目标，但是现在回忆总结一下，还有在一个圆内，只不过半径大了一些而已。它像是我个人开发者生涯的一个“活化石”，记录了我每一个阶段遇到的问题和想出的方案。&lt;/p&gt;
&lt;p&gt;总结了最近几年开发过和上线了的一些内容。现在的“豆子工具【已备案】”已经从当年的随机数生成器，进化成了一个覆盖网络、多媒体、开发调试的全能工具箱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;算法&lt;/strong&gt;：随机数生成，MD5，SHA256，RSA，AES，Base64，URL编码等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络&lt;/strong&gt;：获取公网IP地址和归属地，获取 WIFI 公网地址、云厂商安全组管理、查服务端口、域名证书检测，局域网TCP、UDP、WebSocket调试，ESP网络配置等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多媒体&lt;/strong&gt;：音频格式转换（m4a 转 mp3）、图片格式转换（png 转 webp）、二维码识别与生成，图片拼接等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统功能&lt;/strong&gt;：邮件通知、微信服务通知、微信聊天分享，打开文档，导出文档等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;小程序一直在迭代中，这中间的很多功能因为自身原因或者微信审核政策的原因，遗憾地消失在了版本更迭中。&lt;/p&gt;
&lt;h2 id="二豆子碎片小程序"&gt;二、豆子碎片小程序&lt;/h2&gt;
&lt;p&gt;这个小程序项目名称“Visit”，它的微信小程序名称也是变更了多次，只有在备案时才确定了下来。不过它的内容现在已经融入了豆子工具v11.0.0版本中。只是为了维护方便。&lt;/p&gt;
&lt;p&gt;这个Visit小程序在Wander小程序一年后注册，刚开始做的功能就是浏览 Go Web 框架 Beego 的教程。当时，正在自学 Beego，而 Beego 的网站时常打不开，就想做一个小程序，可以浏览 Beego 内容。刚做出来的一段时间，因为新鲜度小火了一把。&lt;/p&gt;
&lt;p&gt;后来又切换到唐诗主题，当时自己费劲的采集了小学的古诗词作为内容源。印象最深的是添加拼音，当时没有AI，从网上搜索和自己琢磨。最后还是放弃了，一是因为素材的原因，没有解析，没有音频，界面美观一般（回忆来看）。到备案的时候，卡住了，个人主体不允许出现诗词这些关键字。要改名字了，我觉得内容不符合，就换内容了，对于我来说，名字需要和内容匹配，心里才会舒服。当时为了选内容而发愁，因为不想放弃这个小程序。想了几天后，觉得把自己的开发经验做成笔记吧。因为碎片化，所以叫豆子碎片。添加豆子前缀，是因为当时备案时，需要区分小程序名称。觉得豆子比较简单上口。那段时间学了towxml，将内容使用Markdown来编写。&lt;/p&gt;
&lt;p&gt;这之后，虽然也改过游戏关卡，作品集，模块集市。但都是通过Markdown来编写和展示内容。&lt;/p&gt;
&lt;h2 id="三思考现在与未来"&gt;三、思考现在与未来&lt;/h2&gt;
&lt;p&gt;今年已经是2026年，AI相当发达，我以前收录的代码片段和想法思考，AI已经分分钟替代，并且做的更好。现在的小程序在备案和实名后，自己的小程序更是无人问津。到了今天，已经没有精力再去想尝鲜了，为了一个新出的功能或者想法去搞好几天去研究了。总结了小程序最近几年的变更，虽然内容一直在变，但是自己在技术上并没有新的突破，依旧是列表详情布局，依旧是本地和后端联动。以前围绕着微信小程序为中心，当抬高视野时，发现局限性很大，小程序很多内容是不能做的，很多功能是做不了的。当前的豆子工具是功能小程序，已经可用的功能如音频格式转换，图片格式转换继续保留。豆子碎片当前的模块集市依旧保留，它是一个项目列表，可以查看详情并可获取到源码和实用客户端工具。所以可以将豆子碎片和豆子工具的内容合并到一起，减少小程序的维护，可以有更多的精力去做一些其他事情。2026年已经计划在博客中写文章了，已经不再使用小程序记笔记，因为小程序的缺陷很明显，特别对于长篇文章。那么未来就可以这样，首页网站可以做Web实验性项目，博客中写项目实现和想法，豆子工具中做小程序最合适的工具和上架模块源码，公众号主要用来引流和发布一些比较好的博客文章，或者发布一些工具更新，或者一些好的开源项目。以后的内容可以深入一些，实用一些，比如以前的模块，现在可以做一个产品。然后继续新的方向，桌面客户端工具和嵌入式开发尝试，继续深入Rust和Go。&lt;/p&gt;</description></item><item><title>小程序激励广告防刷演进史：从前端校验到“挑战模式”</title><link>https://blog.91demo.top/crypto-link/</link><pubDate>Tue, 07 Apr 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/crypto-link/</guid><description>&lt;p&gt;在开发“模块集市”时，用户通过观看激励广告获取源码下载地址。虽然对接广告并不难，但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调（Server-to-Server），奖励的发放完全依赖前端触发，这给了一些“羊毛党”可乘之机。&lt;/p&gt;
&lt;p&gt;在接口防护的过程中，我经历了四个阶段：&lt;/p&gt;
&lt;h2 id="第一阶段身份鉴权解决匿名刷取"&gt;第一阶段：身份鉴权（解决匿名刷取）&lt;/h2&gt;
&lt;p&gt;我的获取奖励接口是&lt;code&gt;getAdReward&lt;/code&gt;。调用它就可以获取奖励，后台进行发放。最初，由于经验不足，我没有对它做任何防护。这样任何人任何客户端都可以进行访问。了解小程序微信账号体系后，我添加了鉴权，只有携带Token的用户可以访问。这样可以保证只有能够调用wx.login的用户才可以访问。&lt;/p&gt;
&lt;p&gt;这解决了一部分问题，但是又有了新的问题，它防不住“真实用户”，只要用户登录后拦截并解析出接口地址和 Token，就可以跳过广告手动调用了&lt;code&gt;getAdReward&lt;/code&gt;接口，没有观看广告就获取了奖励。&lt;/p&gt;
&lt;h2 id="第二阶段共享密钥加密解决接口暴露"&gt;第二阶段：共享密钥加密（解决接口暴露）&lt;/h2&gt;
&lt;p&gt;为了防止直接调用接口，我引入了对称加密。前端与后端约定一个硬编码的共享密钥，在小程序端和服务端同时使用这个共享密钥加密随机数和时间戳进行校验，匹配则发放奖励。这样可以防止fidder等工具直接拦截调用接口。&lt;/p&gt;
&lt;p&gt;它正常运行了一段时间，我发现又有了新的情况。用户可能通过反编译或者其它方式获取到了共享密钥。这在一段时间内对我造成困扰，除了定期更新共享密钥，我没有好的办法，但更新共享密钥需要升级发布小程序。直到当我了解到小程序提供加密网络通道时，我发现这是一个好的解决方案。&lt;/p&gt;
&lt;h2 id="第三阶段微信加密网络通道解决密钥安全"&gt;第三阶段：微信加密网络通道（解决密钥安全）&lt;/h2&gt;
&lt;p&gt;首先，我们来了解一下加密网络通道，它是微信为小程序提供的一套加密KEY机制，它可以同时在微信小程序端和服务器端获取相同的密钥。由于密钥由微信官方通道生成且动态更新，反编译源码也拿不到密钥。除非攻击者能拦截微信的网络通道，否则无法伪造解密过程。同时，配合时间戳校验，有效防御了初级的重放攻击。这对于我来说，是非常好的利好消息，我不需要在源码中硬编码共享密钥了。&lt;/p&gt;
&lt;p&gt;我们可以参考获取短信验证码，短信验证码之所以安全，是因为应用和短信验证码是两个不相同的通道。同样的这个加密KEY，在小程序获取和服务端获取都是通过微信的网络通道，和自己的应用通道也不是一个。对于我的小程序稍加改造就可以了。在获取奖励的接口中，首先使用&lt;code&gt;wx.getUserEncryptKey&lt;/code&gt;获取微信的加密KEY，然后使用自定义的AES加密方法，将要传递的时间戳和Nonce等参数加密后，将加密数据提交给服务器就可以了。当数据到达服务器，服务器同样调用微信的接口获取微信的加密KEY，然后进行AES解密。这样就可以防止获取共享密钥的弊端。&lt;/p&gt;
&lt;p&gt;好了，我们来看看具体如何使用？为了避免小程序与开发者后台通信时数据被截取和篡改，微信侧维护了一个用户维度的可靠key，用于小程序和后台通信时进行加密和签名。开发者可以分别通过小程序前端和微信后台提供的接口，获取用户的加密 key。&lt;/p&gt;
&lt;p&gt;1，小程序端获取：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const somedata = &amp;#39;xxxxx&amp;#39;
const userCryptoManager = wx.getUserCryptoManager()
userCryptoManager.getLatestUserKey({
success({encryptKey, iv, version, expireTime}) {
const encryptedData = someAESEncryptMethod(encryptKey, iv, somedata)
wx.request({
data: encryptedData,
success(res) {
const decryptedData = someAESDEcryptMethod(encryptKey, iv, res.data)
console.log(decryptedData)
}
})
}
})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，someAESEncryptMethod 和 someAESDEcryptMethod 分别为加解密函数，由开发者自行引入加解密库来实现，基础库暂时不提供加解密能力。&lt;/p&gt;
&lt;p&gt;2，服务端获取：&lt;br&gt;
在开发者服务端，可以调用getUserEncryptKey后台接口获取用户最近三次的key。在获取key的同时，接口会携带version信息，开发者可以比较version版本来选择使用对应的key对数据进行加解密。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -X POST &amp;#34;https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=ACCESS_TOKEN&amp;amp;openid=OPENID&amp;amp;signature=SIGNATURE&amp;amp;sig_method=hmac_sha256&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，openid是用户的openid，signature用sessionkey对空字符串签名得到的结果。即 signature = hmac_sha256(session_key, &amp;ldquo;&amp;quot;)，sig_method为签名方法，固定为hmac_sha256。&lt;/p&gt;
&lt;p&gt;这个是核心，获取激励广告奖励基于它再添加一些参数进行加解密。&lt;/p&gt;
&lt;h2 id="第四阶段后端挑战模式解决重放与逻辑伪造"&gt;第四阶段：后端“挑战模式”（解决重放与逻辑伪造）&lt;/h2&gt;
&lt;p&gt;我在参数中添加的Nonce，它是一个噪音参数，它是小程序端本地生成的，还有时间戳也是本地生成的，增加它们是为了增加解密难度。在实际运行中，我发现硬核玩家会通过修改本地时间戳和 Nonce（随机数）来绕过检测，或者使用相同的参数进行重放攻击，或者不够15s（广告正常播放最短时间）调用多次接口。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我将逻辑升级为“挑战模式”：首先，小程序端在广告开始前进行一次预请求，前端必须先调用 getNonce 接口获取Nonce然后使用这个Nonce进行加密，&lt;code&gt;getNonce&lt;/code&gt;是一个新增接口，用来生成Nonce，并记录生成时间和关联openid，服务端存入缓存（如 Redis），完成服务端打标。当前端再次调用&lt;code&gt;getAdReward&lt;/code&gt;接口时，如果差值小于广告常规时长（如 15s），则判定为非正常观看，直接拒绝。&lt;/p&gt;</description></item><item><title>我是如何设计和实现模块源码集市的？</title><link>https://blog.91demo.top/mod-carrier/</link><pubDate>Thu, 26 Mar 2026 07:22:34 +0000</pubDate><guid>https://blog.91demo.top/mod-carrier/</guid><description>&lt;p&gt;当我开发完51单片机温度采集并转换Modbus RTU后，我一度想分享出去，但是我又不想草率的去分享。我一直在思考，如何能让这些代码发挥更大的价值，而不仅仅是躺在个人仓库里。作为一名摸爬滚打多年的开发者，我手头积累了大量来自实践的代码、模块和解决方案。它们散落在硬盘的各个角落，像一颗颗未经打磨的珍珠。 于是，我萌生了做一个“模块源码集市”小程序的想法——一个可以直接浏览、查阅、并一键下载优质源码的平台。&lt;/p&gt;
&lt;p&gt;经过最近一段时间的集中开发，这个小程序终于和大家见面了。今天，我想和大家分享其背后的设计与思考，希望能为有相似想法的朋友提供一些参考。这个小程序的核心功能简洁而实用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首页全景浏览：所有上架的代码模块都在首页清晰展示，你可以快速了解其名称、简介和分类，对全站资源一目了然。&lt;/li&gt;
&lt;li&gt;沉浸式详情阅读：点击任一模块，即可进入详情页。这里我使用了Markdown渲染引擎，来展示模块的详细介绍、使用说明、技术要点等。Markdown的优雅排版能让技术文档的阅读体验大幅提升。&lt;/li&gt;
&lt;li&gt;一键获取源码：在详情页，点击获取模块源码链接按钮，即可轻松获取模块的完整源码压缩包。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了让这个简单的流程稳定、安全、可扩展，我在几个关键环节做了一些设计和取舍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;动态资源与安全通道源码文件并非直接打包在小程序内，而是存储在后台的MinIO对象存储中。当用户点击下载时，服务端会动态生成一个具有时效性的访问链接返回给小程序。这样做的好处是资源可以随时更新。同时，为了维持后端服务器持续运行，我在列表及详情中接入了少量广告。为了保护下载接口不被恶意刷取，我集成了激励式视频广告。用户观看一则简短的广告，即可解锁下载权限。这不仅是简单的广告接入，更重要的是，我为此构建了一套防刷机制，我使用了微信的加密网络通道，对关键数据进行加密传输和验证，极大地增加了自动化盗刷的成本和难度，保护了服务器资源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;灵活的沟通与通知我深知，一个“活”的产品需要与用户保持沟通。因此，小程序内设置了系统消息中心，我可以在这里向所有用户下发重要的系统公告、模块上新以及更新日志。更重要的是，我为每个模块的上新功能，接入了微信服务通知。当有新的、优秀的代码模块入库时，订阅了的用户会在早上8点后收到一条微信消息提醒，能让你随时跟上“仓库”的更新节奏。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;持续生长的内容生态目前上架的或正在上架的模块，覆盖了我擅长的多个领域：小程序、Go语言后端工具、Rust系统编程实践、Web前端片段、嵌入式软硬件代码，包含音视频、网络、加密等技术要点，形式包括客户端、命令行工具、固件、小程序、网页等。这只是一个开始。我的计划是将其作为一个长期项目，持续将我实践中验证过的、有价值的代码沉淀、整理并开源出来。未来，我还会在项目详情页中，增加关联的公众号技术文章跳转，或是有功能关联的其他小程序跳转，形成一个立体化的技术知识网络。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;坦率地说，这个小程序的开发过程远超我最初的预期。从架构设计到具体实现，尤其是几个关键的技术卡点上，耗费了我大量的精力。&lt;/p&gt;
&lt;p&gt;与广告盗刷的攻防：我的服务器每天都有人在扫描，所以在实现激励广告防刷机制时，我花了大量时间加强防刷机制，其实这个在微信小程序社区中也可以看到很多类似的问题。我这里研究了一种解决方案。就是使用加密网络通道。在对接的过程中，我与各种加密错误、签名错误（如常见的40079错误）作斗争。例如，处理会话密钥时，发现不需要进行Base64编码，而是直接以空字符串的字节形式处理；在URL拼接的细节上，POST与GET方法的误用也会导致失败；而加解密环节更是陷阱重重——加密密钥需要从Base64解码，IV却是一个直接的16位字符串，加密数据则需要从十六进制格式解码……这些细枝末节，任何一个出错都会导致整个流程崩掉，往往需要清空Token重新登录来排查。这个过程让我对网络数据安全有了更深刻的认识。这些错误和经验本想专开一篇文章介绍，但是细想一下，可能一段话就能总结，但是其中的辛酸和感悟无法表达在纸面上。索性后期整理出来开源这个模块，减少其他开发者的一些坎坷。&lt;/p&gt;
&lt;p&gt;存储方案的抉择：在对象存储的选择上，也有一段小插曲。我最熟悉的是Mino，但是在我了解到MinIO版本已归档，便尝试了rustfs等其他方案进行探索。但经过综合对比在可靠性、API友好度以及与现有技术栈的契合度后，最终还是选择了功能完备、文档清晰的MinIO作为存储后端，事实证明这个选择是稳定高效的。&lt;/p&gt;
&lt;p&gt;所幸，在这个艰难的过程中，AI编程助手给予了我巨大的帮助（但是也让我吃了很多苦头，因为它上面的资料都比较旧）。许多代码片段的生成、调试过程中错误信息的解读、API文档的快速查询，都因AI的介入而效率倍增。如果没有它，单靠我自己，恐怕几个月也难见成果，绝无可能在十几天内推动项目成型上线。&lt;/p&gt;
&lt;p&gt;做这个小程序，并非为了追逐热点，这个小程序刚刚上线，还需要时间去细细打磨。开发其初心很简单：整理自己的知识资产，并以一种对开发者最友好、最便捷的方式分享出去。我希望它不仅仅是一个下载站点，更可以成为一个高质量的、经过实战检验的“代码片段集市”和“工具箱”。路漫漫其修远兮。在代码世界的探索中，每个人都是学生，也是老师。我在这里抛砖引玉，期待这些代码模块能真正帮助到在具体问题上寻找解决方案的你。也欢迎你常来看看，这些项目模块，会和我一样，在技术的道路上不断生长。&lt;/p&gt;
&lt;p&gt;扫码体验：&lt;img src="https://blog.91demo.top/images/visit.webp" width="200" alt="豆子碎片小程序"&gt;&lt;/p&gt;</description></item><item><title>Modbus转MQTT网关固件研发与共创计划</title><link>https://blog.91demo.top/modujson-gateway/</link><pubDate>Tue, 24 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/modujson-gateway/</guid><description>&lt;h2 id="一-项目愿景让设备上云像接线一样简单"&gt;一、 项目愿景：让设备上云，像接线一样简单&lt;/h2&gt;
&lt;p&gt;新的一年，新的计划，我们致力于为工程师、创客及小团队，打造一套开箱即用、稳定可靠的工业物联网数据采集方案。你无需深究复杂的协议栈，只需聚焦业务，即可让传统设备轻松上云。&lt;/p&gt;
&lt;p&gt;本项目的核心价值在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协议翻译官：打造一个高性能的“中间件”，将底层的Modbus协议（支持RS485/TTL）无缝转换为上层的MQTT协议，打通设备与云端的“最后一公里”。&lt;/li&gt;
&lt;li&gt;软硬解耦：不绑定特定硬件。无论是WiFi环境（ESP32）还是4G环境（Cat.1模组），只要具备基本通信能力，软件就能赋予其“智能”，提供极大的硬件选型自由度。&lt;/li&gt;
&lt;li&gt;解决技术断层：解决有业务需求但没有研发实力的小团队（如硬件工程师、创客、小厂），本项目提供了一种低成本解决方案。它只需通用开发板，就能实现数据采集和上云，花小钱办大事。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二-技术架构三位一体的全栈实现"&gt;二、 技术架构：三位一体的全栈实现&lt;/h2&gt;
&lt;p&gt;为确保方案的极致稳定与高度可扩展，我们采用“固件 + 配置工具 + 演示端”的三位一体架构，确保方案的完整性与专业性。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;核心固件（“翻译官” - 逻辑版）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;固件架构：采用分层架构设计，硬件抽象层隔离具体芯片与通信接口（如RS485/TTL），实现核心逻辑与硬件的解耦；协议栈层实现完整的Modbus协议栈（主/从站，RTU/TCP），并进行高效、健壮的JSON封装；服务层内置MQTT客户端，负责可靠的数据上传、断线重连与本地缓存；配置管理层，通过Rust编写的PC工具下发设备配置，实现软件定义功能。&lt;/p&gt;
&lt;p&gt;技术栈：采用C语言开发，优先实现裸机（No OS）逻辑版本，确保在资源受限的MCU上也能稳定运行，降低用户使用门槛。&lt;/p&gt;
&lt;p&gt;功能：负责实时采集Modbus寄存器数据（如电压、电流），并根据配置的倍率进行数据转换，最后通过MQTT协议将数据打包上传。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;PC配置工具（“指挥官” - Rust版）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;技术栈：使用Rust语言开发，利用其内存安全特性，确保配置过程绝对可靠，工具长期运行不崩溃。&lt;/p&gt;
&lt;p&gt;功能：提供图形化界面，用户可轻松配置通信模式、寄存器地址、倍率等参数。支持通过串口（USB转TTL） 进行固件刷新与参数下发，解决现场实施难题，无需复杂的网络配置。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;数据演示端（“仪表盘”）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;技术栈：微信小程序。&lt;/p&gt;
&lt;p&gt;功能：扫码即可查看实时数据曲线与数值面板，直观验证数据上云效果，方便现场调试与演示。&lt;/p&gt;
&lt;h2 id="三-当前状态诚邀你成为共创者"&gt;三、 当前状态：诚邀你，成为“共创者”&lt;/h2&gt;
&lt;p&gt;项目正处于核心研发阶段，当前全力攻克裸机版本，实现通信方式支持WiFi和4G的Modbus RTU（串口）协议的稳定性与性能。它适合简单应用，保证运行稳定。我们相信，最好的产品源于真实场景的千锤百炼。&lt;/p&gt;
&lt;p&gt;未来会研发通信方式支持LoRa、以太网的Modbus TCP，以及FreeRTOS多任务版，用于解决需要同时处理多路数据采集，复杂业务逻辑的高端场景。&lt;/p&gt;
&lt;p&gt;因此，我们发起此次共创计划：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的支持，将直接转化为测试硬件（ESP32、各类Modbus传感器），用于极限环境下的兼容性验证。&lt;/li&gt;
&lt;li&gt;你的设备，可以寄来成为我们的“适配样本”，共同完善设备库。&lt;/li&gt;
&lt;li&gt;你的反馈，将直接塑造产品的下一个版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="四未来承诺"&gt;四、未来承诺&lt;/h2&gt;
&lt;p&gt;当固件通过大量真实场景验证，达到工业级稳定标准后，我们将正式申请软著，并启动授权售卖。所有共创阶段的支持者，都将依据历史贡献，获得丰厚的升级折扣与永久优先支持权。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我入驻了爱发电，更多详情内容请查看：&lt;a href="https://afdian.com/a/modujson"&gt;https://afdian.com/a/modujson&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>基于 Go + 小程序构建“口袋指令中心”，实现远程发布控制</title><link>https://blog.91demo.top/bag-script/</link><pubDate>Wed, 04 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/bag-script/</guid><description>&lt;p&gt;我日常涉及 Hugo 博客发布、客户端打包、Nginx 运维等多种重复性脚本。每次都要 SSH 连服务器并执行命令，操作链过长，也不方便，特别是在身边没有电脑的情况。&lt;/p&gt;
&lt;p&gt;曾经想过使用HTML 远程调用，又担心安全，使用了微信公众号发消息，也特适合这个场景，但是因为微信更新，私信入口现在非常难进入。所以就想能否通过自己的小程序实现远程控制和鉴权。构建一个通用的执行引擎，通过小程序远程触发，且具备零前端修改（免于小程序上线审核）的扩展能力。&lt;/p&gt;
&lt;h2 id="系统架构设计"&gt;系统架构设计&lt;/h2&gt;
&lt;p&gt;为了实现“明天增加脚本，小程序不发版”的目标，采用了“配置驱动” 模式。即“配置在云端，指令在指尖”。通过将业务逻辑（脚本路径与名称）完全从前端小程序中解耦，实现一套代码支持无限扩展的运维能力。&lt;/p&gt;
&lt;h3 id="核心流程"&gt;核心流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;后端 (Go)：维护一个脚本配置列表（数据库或配置文件）。&lt;/li&gt;
&lt;li&gt;前端 (小程序)：启动时请求后端接口，拉取可用脚本列表。&lt;/li&gt;
&lt;li&gt;触发：使用时选择脚本名称，点击执行。&lt;/li&gt;
&lt;li&gt;鉴权：后端校验小程序 OpenID，仅允许本人指令生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="技术实现"&gt;技术实现&lt;/h2&gt;
&lt;h3 id="系统分为三层确保安全性与扩展性的统一"&gt;系统分为三层，确保安全性与扩展性的统一：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;配置层 (Go Config)：在服务器端定义脚本的 ID、名称和实际路径。&lt;/li&gt;
&lt;li&gt;鉴权层 (WeChat Auth)：利用微信小程序 OpenID 建立强一致性的身份白名单。&lt;/li&gt;
&lt;li&gt;展示层 (Mini Program UI)：动态拉取后端配置，仅负责“展示列表”与“触发指令”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="技术实现方案"&gt;技术实现方案&lt;/h3&gt;
&lt;p&gt;A. 后端：动态脚本引擎 (Go)&lt;/p&gt;
&lt;p&gt;后端不再硬编码脚本路径，而是定义一个结构体：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 脚本任务定义&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ScriptTask&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;id&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 前端传递的任务标识&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;name&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 小程序界面显示的文字&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;-&amp;#34;`&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 实际执行的脚本路径 (对前端保密)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 示例配置（可存放在 JSON 文件或数据库中）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tasks&lt;/span&gt; = []&lt;span style="color:#a6e22e"&gt;ScriptTask&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;hugo-post&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;发布 Hugo 文章&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/scripts/deploy_hugo.sh&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;build-client&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;构建客户端&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/scripts/build_mole_go.sh&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;nginx-restart&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Name&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;重启 Nginx 服务&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;systemctl restart nginx&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对外接口定义：&lt;/p&gt;</description></item><item><title>实战笔记：实现一个语音验证码远程呼叫版本</title><link>https://blog.91demo.top/voice-call/</link><pubDate>Tue, 13 Jan 2026 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/voice-call/</guid><description>&lt;p&gt;在实现了主动使用VOIP客户端拨打&lt;code&gt;8000&lt;/code&gt;号码播报语音验证码的功能后，我发现了一个最大的缺点，就是这需要用户主动去操作。这对于想使用API集成的第三方应用来说根本无法实现。&lt;/p&gt;
&lt;p&gt;在思考之后，我决定实现一个可以调用API就呼叫VOIP客户端的版本，当呼叫用户并且用户接通后，自动播报语音验证码的功能。当实现这个功能后，它的好处是显而易见的。比如，可以集成到嵌入式，集成到第三方网站，直接调用API就可以呼叫出去。&lt;/p&gt;
&lt;p&gt;那么该如何实现它呢？&lt;/p&gt;
&lt;h2 id="一-系统原理"&gt;一、 系统原理&lt;/h2&gt;
&lt;p&gt;传统的拨号方案（Dialplan）是静态的，而 ARI 允许我们动态控制。整个“API 触发呼叫并播报”的流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发阶段：第三方系统通过 API 向 Go 服务发送呼叫请求（包含目标 ID 和验证码）。&lt;/li&gt;
&lt;li&gt;呼叫发起（Originate）：Go 服务调用 Asterisk ARI 的 /channels 接口。此时 Asterisk 会尝试向 PJSIP 终端（或通过中继向手机）发起呼叫。&lt;/li&gt;
&lt;li&gt;接通监听（Stasis Start）：一旦用户接起电话，该通道会被移交给一个名为 Stasis 的应用。此时 Go 服务会收到一个“通道已接通”的 WebSocket 事件。&lt;/li&gt;
&lt;li&gt;语音合成与播放：Go 服务识别到接通后，调用播报指令（可以播放预录音文件，或对接 TTS 引擎生成的语音流）。&lt;/li&gt;
&lt;li&gt;挂断处理：播报完毕后，服务发送挂断指令，释放资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二系统架构"&gt;二、系统架构&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[第三方API] --&amp;gt; [Go 后端服务] --(REST API)--&amp;gt; [Asterisk ARI]
| |
(WebSocket) (PJSIP/IMS)
| |
[接通状态回调] &amp;lt;--- [用户终端接听]
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="三-核心代码实现-golang"&gt;三、 核心代码实现 (Golang)&lt;/h2&gt;
&lt;p&gt;这里使用了 GitHub 上的 &lt;code&gt;go-ari&lt;/code&gt; 库。&lt;/p&gt;
&lt;h3 id="1-初始化-ari-客户端"&gt;1. 初始化 ARI 客户端&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;github.com/v5&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;github.com/v5/client/native&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 连接到 Asterisk ARI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;cl&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;native&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Connect&lt;/span&gt;(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;native&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Options&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Application&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;voice-verify&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 必须与 asterisk.conf 配置一致&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Username&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;URL&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;http://localhost:8088/ari&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-实现呼叫并播报语音验证码"&gt;2. 实现呼叫并播报语音验证码&lt;/h3&gt;
&lt;p&gt;这是核心逻辑：接收参数 -&amp;gt; 发起呼叫 -&amp;gt; 监听接通 -&amp;gt; 播放语音。&lt;/p&gt;</description></item><item><title>Markdown 进化论：从小程序文章列表到游戏关卡到时间轴作品集再到项目模块集市</title><link>https://blog.91demo.top/v-article-extend/</link><pubDate>Wed, 03 Sep 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-article-extend/</guid><description>&lt;p&gt;Markdown越来越玩的纯熟，在小程序架构稳定之后，剩下的就是Markdown内容的编辑，我已经不满足于基础的标题和段落，我还会根据适当的场景使用。我必须把常用的Markdown介绍一下，感觉其设计真是奇妙，比如在一行中用三个以上的星号、减号、底线来建立一个分割线，行内不能有其它东西。也可以在星号或减号中间插入空格。这样就是一条分割线。当你完全掌握之后，就会发现我们还可以进行扩展，比如我后面的跳转公众号文章，跳转小程序。&lt;/p&gt;
&lt;p&gt;我们先来快速浏览一下除了标题和段落之外不常用但重要的Markdown语法。不感兴趣可以跳过。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;*斜体文本*
_斜体文本_
**粗体文本**
__粗体文本__
***粗斜体文本***
___粗斜体文本___
# 分割线
***
* * *
*****
- - -
---------
~~删除线~~
&amp;lt;u&amp;gt;下划线文本&amp;lt;/u&amp;gt;
[^脚注，要注明的文本]
# 无序列表
* 第一项
+ 第二项
- 第三项
# 有序列表
1. 第一项
2. 第二项
3. 第三项
# 列表嵌套需要在子列表中的选项签名添加两个或四个空格即可。
1. 第一项：
- 第一项嵌套的第一个元素
- 第一项嵌套的第二个元素
2. 第二项：
- 第二项嵌套的第一个元素
- 第二项嵌套的第二个元素
# 区块
&amp;gt; 最外层
&amp;gt; &amp;gt; 第一层嵌套
&amp;gt; &amp;gt; &amp;gt; 第二层嵌套
# 列表中使用区块，如果列表中要使用区块，请在&amp;gt;前添加四个空格即可。
* 第一项
&amp;gt; 菜鸟教程
&amp;gt; 学的不仅是技术更是梦想
* 第二项
# 链接
[链接名称](链接地址)
&amp;lt;链接地址&amp;gt;
# 图片显示
![alt 属性文本](图片地址)
![alt 属性文本](图片地址 &amp;#34;可选标题&amp;#34;)
![alt 属性文本](图片地址居中#pic_center)
![alt 属性文本](图片地址宽和高 =400x200)
# 只设置宽，会按比例进行缩放
![alt 属性文本](图片地址 =400x)
# 使用 html &amp;lt;img&amp;gt;标签。
&amp;lt;img src=&amp;#34;https://xxx.com/x.png&amp;#34; width=&amp;#34;50%&amp;#34;&amp;gt;
# 制作表格使用|来分割不同的单元格，可以使用-来分割表头和其它行。
| 表头 | 表头 |
| ---- | ---- |
| 单元格 | 单元格 |
| 单元格 | 单元格 |
# 设置表格的对齐方式使用如下符号：
-: 设置内容和标题栏居右对齐。
:- 设置内容和标题栏居左对齐。
:-: 设置内容和标题栏居中对齐。
# Markdown 使用了很多特殊符号来表示特定的意义，如果需要显示特定的符号则需要使用转义字符，Markdown 使用反斜杠转义特殊字符：
**文本加粗**
\*\* 正常显示星号 \*\*
使用 KaTex 或 MathJax 来渲染数学表达式。
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="mardown语法扩展"&gt;Mardown语法扩展&lt;/h3&gt;
&lt;p&gt;当然我还扩展了一些内容，比如链接，我在小程序中支持跳转到公众号文章，以及跳转到小程序。具体的实现就是通过前缀。&lt;/p&gt;</description></item><item><title>万物皆可 Markdown：小程序动态内容渲染通用架构设计</title><link>https://blog.91demo.top/v-article-list/</link><pubDate>Tue, 03 Jun 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-article-list/</guid><description>&lt;p&gt;在学习了微信小程序之后，我使用wxml页面可以做出唐诗的内容了。结合wxss，我可以定义好看的布局。在小程序需要备案时，因为唐诗需要资质，而我个人无法满足这些条件，所以我需要开发新的项目。恰好最近在为我的技术笔记发愁。我希望有一个可以展示我笔记内容的工具。&lt;/p&gt;
&lt;p&gt;我的最初想法非常简单：&lt;br&gt;
1，可以更新文章内容而不需要升级小程序。&lt;br&gt;
2，需要在小程序端进行展示，并且需要美观。&lt;br&gt;
3，能够根据标题或者关键字进行搜索文章内容。&lt;/p&gt;
&lt;p&gt;经过一番研究后，我决定小程序使用原生开发，界面 UI 使用 WeUI，内容渲染使用Markdown。&lt;/p&gt;
&lt;p&gt;小程序页面架构是列表+详情。列表作为首页，首页页面非常简单，一个标题和描述，用来解释小程序干什么？一个搜索框用来搜索内容，三个快捷按钮用于快速查找内容。 这个也是为了小程序审核，避免成为资讯类主题。&lt;/p&gt;
&lt;p&gt;在完成之后，我发现这套架构可以通用，用来渲染动态内容。像列表页面以及WEUI使用，熟悉前端的非常容易理解实现。下面是一些核心介绍，主要介绍Markdown的渲染实现。&lt;/p&gt;
&lt;p&gt;在小程序中，无法直接展示 Markdown 数据，我们需要将 Markdown 转换为 WXML，网上的大神很多，有多款 Markdown 转 WXML 库，我使用的是 &lt;code&gt;TOWXML&lt;/code&gt; 库。我使用Markdown主要是因为更新内容不用升级小程序，这是动态渲染的最大优点。当然它还有其它一些优势，比如容易编辑，支持数学公式，布局美观等。&lt;/p&gt;
&lt;p&gt;下面是Markdown具体实现，不感兴趣可以跳过。可以看技术下面的架构思考。&lt;br&gt;
1，按照 towxml 库的说明文档，我们将编译后的 towxml 复制到我们的项目中，然后在 app.js 文件中引入这个组件。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;App({
towxml: require(&amp;#39;./towxml/index&amp;#39;),
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2，按照首页的步骤，我们添加文章详情页面。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;pages/article/index&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 index.wxml 文件中，我们添加页面布局，这个很简单，显示渲染后的内容即可。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!--pages/article/index.wxml--&amp;gt;
&amp;lt;view class=&amp;#34;page&amp;#34;&amp;gt;
&amp;lt;!--使用towxml--&amp;gt;
&amp;lt;towxml nodes=&amp;#34;{{article}}&amp;#34; /&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;3，注意，我们还需要在 index.json 中添加组件引入。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
&amp;#34;usingComponents&amp;#34;: {
&amp;#34;towxml&amp;#34;: &amp;#34;../../towxml/towxml&amp;#34;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;4，在 js 文件中，我们需要调用 Markdown 数据。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;onLoad(options) {
const _ts = this;
wx.showLoading({
title: &amp;#39;加载中&amp;#39;,
})
httpGet(&amp;#39;/artd&amp;#39;, {
uuid: options.guid,
}).then((res) =&amp;gt; {
const result = res.data;
if (result.code == 1) {
let content = result.data;
let obj = app.towxml(content, &amp;#39;markdown&amp;#39;, {
theme: &amp;#39;light&amp;#39;,
events: {
tap: (e) =&amp;gt; {
console.log(&amp;#39;tap&amp;#39;, e);
}
}
});
_ts.setData({
article: obj,
});
wx.hideLoading({
success: (res) =&amp;gt; { },
})
} else {
wx.hideLoading()
}
}).catch((err) =&amp;gt; {
console.log(err);
wx.hideLoading()
wx.showToast({
title: &amp;#39;网络异常请重试&amp;#39;,
})
})
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们在 onLoad 中加载函数，这样页面启动就会自动网络请求，然后渲染 Markdown 数据。在请求数据时，我们注意到 guid，这个参数是上一个页面即首页，点击文章列表项带过来的数据。 你也可以使用其它参数，它能唯一代表从服务器拉取哪篇文章的Markdown内容。&lt;/p&gt;</description></item><item><title>Mosquitto 安全加固从匿名访问到多用户 ACL 细粒度权限控制</title><link>https://blog.91demo.top/mosquitto-config/</link><pubDate>Wed, 01 Jan 2025 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/mosquitto-config/</guid><description>记录 Mosquitto 代理服务的安全配置过程，包括用户认证、动态 ACL 权限控制以及在边缘计算场景下的安全实践。</description></item><item><title>文章上传命令行客户端升级版本使用Cobra支持更多命令</title><link>https://blog.91demo.top/v-upart-cobra/</link><pubDate>Sun, 22 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-upart-cobra/</guid><description>&lt;p&gt;对于添加更多的命令，使用 flag，就有点麻烦了，这次我们使用一个更高级的库 cobra。同时，我们使用 viper 替换 ini 库，这个库可以读取多种格式的配置文件，可以读取环境变量。&lt;/p&gt;
&lt;p&gt;要实现的功能如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上传文章&lt;/li&gt;
&lt;li&gt;文章删除&lt;/li&gt;
&lt;li&gt;更新文章标题&lt;/li&gt;
&lt;li&gt;更新文章关键字&lt;/li&gt;
&lt;li&gt;更新文章内容&lt;/li&gt;
&lt;li&gt;文章公开开关&lt;/li&gt;
&lt;li&gt;文章加锁开关&lt;/li&gt;
&lt;li&gt;根据标题查找文章&lt;/li&gt;
&lt;li&gt;根据关键字查询文章&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在使用 cobra 实现上面的命令。&lt;/p&gt;
&lt;p&gt;首先，我想要的效果如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上传文章 gart upload title keyword filename ispub islock&lt;/li&gt;
&lt;li&gt;删除文章 gart remove uuid&lt;/li&gt;
&lt;li&gt;更新文章标题 gart updatetitle uuid title&lt;/li&gt;
&lt;li&gt;更新文章关键字 gart updatekeyword uuid keyword&lt;/li&gt;
&lt;li&gt;更新文章内容 gart updatecontent uuid filename&lt;/li&gt;
&lt;li&gt;更新文章公开或不公开 gart updatepub uuid ispub&lt;/li&gt;
&lt;li&gt;更新文章加锁或不加锁 gart updatelock uuid islock&lt;/li&gt;
&lt;li&gt;根据标题或关键字查找文章 gart search content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面的 upload,remove,updatetitle,updatekeyword 等，在 cobra 中都是命令。title keyword 等都是参数。&lt;/p&gt;
&lt;p&gt;我在读 cobra 文档后，最疑惑的地方就是标志和参数，这两者最大的区别就是标志需要在命令中添加&amp;ndash;flag 然后是值，而参数是直接跟在命令后，直接就是内容的。标志可以更改程序的行为。&lt;/p&gt;</description></item><item><title>使用Go自制豆子碎片文章上传命令行客户端</title><link>https://blog.91demo.top/v-upart-go/</link><pubDate>Sat, 21 Sep 2024 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/v-upart-go/</guid><description>&lt;p&gt;这是 Golang 实现的上传文章以及管理文章的一个命令行工具。&lt;/p&gt;
&lt;h2 id="项目地址"&gt;项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gitee.com/littletow/upart-go"&gt;https://gitee.com/littletow/upart-go&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="实现一个上传文章的命令行工具"&gt;实现一个上传文章的命令行工具&lt;/h2&gt;
&lt;p&gt;这是一个最初的版本，使用 flag 和 ini 来实现文章上传功能。使用 flag 来解析命令行参数，使用 Ini 配置文件，记录识别码，以及 token。记录 token 的原因是因为每次启动命令行，都需要重新获取 token，为了减少 token 获取次数，在获取到 token 后，同时存储到 Ini 配置文件中。每次命令行启动，优先查看配置文件中的 token。&lt;/p&gt;
&lt;p&gt;开发这个工具主要有以下几个技术要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从命令行获取参数&lt;/li&gt;
&lt;li&gt;从配置文件中读取参数&lt;/li&gt;
&lt;li&gt;读取文件内容&lt;/li&gt;
&lt;li&gt;请求后端接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们通过分析后，需要定义以下几个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;title 标题&lt;/li&gt;
&lt;li&gt;keyword 关键字&lt;/li&gt;
&lt;li&gt;filename MD 文件名&lt;/li&gt;
&lt;li&gt;ispub 是否公开&lt;/li&gt;
&lt;li&gt;islock 是否加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Go 代码定义参数和结构体&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var (
title string
keyword string
filename string
ispub int // 1 pub
islock int // 1 lock
)
func init() {
flag.StringVar(&amp;amp;title, &amp;#34;b&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;文章题目&amp;#34;)
flag.StringVar(&amp;amp;keyword, &amp;#34;k&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;文章关键字&amp;#34;)
flag.StringVar(&amp;amp;filename, &amp;#34;f&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;MD文件&amp;#34;)
flag.IntVar(&amp;amp;islock, &amp;#34;l&amp;#34;, 0, &amp;#34;是否加锁&amp;#34;)
flag.IntVar(&amp;amp;ispub, &amp;#34;p&amp;#34;, 0, &amp;#34;是否开放&amp;#34;)
}
func main(){
flag.Parse()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;init() 在 main 函数前调用。需要在 main 函数中调用 flag.Parse()，这一步非常关键。之后就可以使用变量了。&lt;/p&gt;</description></item><item><title>实战笔记：实现一个语音验证码自助拨打版本</title><link>https://blog.91demo.top/voice-code/</link><pubDate>Fri, 19 Aug 2022 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/voice-code/</guid><description>&lt;p&gt;自助语音验证码是通过拨打VOIP电话自动获取验证码，验证码可用来豆子笔记网站认证。这个纯粹是自己的兴趣驱动的。掌握了此项技术，你也可以应用于其它地方。&lt;/p&gt;
&lt;p&gt;我是使用Asterisk实现的，Asterisk 是一个流行的开源通信平台，它提供了构建各种通信应用的灵活而强大的解决方案。它被广泛应用于企业内部电话系统、呼叫中心、语音邮件等场景。Asterisk 支持多种通信协议，包括 SIP、H.323、MGCP 和 SCCP，并且能够与传统的 PSTN 线路深度兼容。此外，Asterisk 还支持通过 Inter-Asterisk eXchange (IAX)协议进行语音 IP 传输，允许数据和语音同时在网络上传输。&lt;/p&gt;
&lt;h2 id="自助语音验证码使用说明"&gt;自助语音验证码使用说明&lt;/h2&gt;
&lt;p&gt;你需要准备：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;软电话，或者支持 VOIP 的电话机&lt;/li&gt;
&lt;li&gt;AST 账户，AST 账户从豆子工具小程序中获取，功能菜单为获取 AST 账户。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：该功能目前已经下架，这里仅作为备忘记录。&lt;/p&gt;
&lt;p&gt;首先，使用获取到的账户配置你的电话。然后拨打 8000，即可自动获取验证码。如果您有什么问题，欢迎关注公众号【技术源泉】私信我。&lt;/p&gt;
&lt;h2 id="技术实现"&gt;技术实现&lt;/h2&gt;
&lt;p&gt;它的工作原理是：用户通过 VOIP 电话连接到 Asterisk，当拨打固定的数字 8000时，Asterisk 将调用 AGI 接口获取服务端的API生成验证码，然后将获取到的验证码拆分通过语音文件播报给用户，播报完毕后自动挂断。&lt;/p&gt;
&lt;p&gt;我们根据自助语音验证码原理，将自助验证码实现分解为以下几个技术要点，只要我们解决以下技术要点，就可以实现自助语音验证码。&lt;/p&gt;
&lt;p&gt;技术要点：&lt;/p&gt;
&lt;p&gt;1，如何播放音频文件？音频文件从哪里来？如何播放动态数据？&lt;br&gt;
2，如何存储用户信息？如何使用数据库存储用户信息？&lt;br&gt;
3，如何获取验证码？&lt;/p&gt;
&lt;p&gt;下面内容是我对各个技术点的对应解决方案。&lt;/p&gt;
&lt;p&gt;1，Asterisk 自带 Playback 应用，可以通过它播放音频文件。音频文件需要我们提前录制好，并且转换为对应的音频格式，最简单的方法就是使用手机上的录音机。当在拨号计划中多次执行 Playback 应用，Asterisk 会将音频流自动连接起来。所以我们可以使用循环多次执行 Playback 应用即可。&lt;br&gt;
2，Asterisk 使用配置文件写入 SIP 用户信息，但当写入新的 SIP 用户信息后，需要重新加载配置文件。为了方便和第三方对接，我们推荐使用数据库。Asterisk 支持数据库，并且可以实时获取用户信息。&lt;br&gt;
3，Asterisk 支持 AGI 接口，我们可以使用 AGI 获取第三方应用的验证码，获取后和提前录制好的文件结合起来进行播放。&lt;/p&gt;
&lt;p&gt;当掌握了这些技术点后，我们就可以灵活应用到其它解决方案。&lt;/p&gt;
&lt;p&gt;下面我们来看具体实现：&lt;br&gt;
1，音频文件处理。我们使用手机上的录音机来录制音频文件。Android 录音机录制的音频文件格式为 mp3，如果是 amr 格式，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。IOS 录音机录制的音频文件格式为 m4a，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能也可以在豆子工具中使用。&lt;/p&gt;</description></item><item><title>实战笔记：当我想查端口却没装 Telnet 时，我决定自己写个工具</title><link>https://blog.91demo.top/check-port/</link><pubDate>Mon, 05 Feb 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/check-port/</guid><description>&lt;p&gt;作为开发者，你一定遇到过这种“抓狂”的时刻：&lt;/p&gt;
&lt;p&gt;某次我急需确认服务器上的一个端口是否正常开启，习惯性地打开 Windows 命令行准备敲下 &lt;code&gt;telnet&lt;/code&gt; 命令。结果弹出的却是：“telnet 不是内部或外部命令”。&lt;/p&gt;
&lt;p&gt;由于系统刚重装，这个组件还没勾选。当我从控制面板找到它、安装、甚至可能还要重启系统时，这套复杂的操作让我陷入了反思：&lt;strong&gt;为了检测一个端口，至于这么麻烦吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更进一步想，如果我手头没有电脑，只有一部手机，我该如何快速判断服务器防火墙是不是忘了开？&lt;/p&gt;
&lt;h3 id="痛点环境依赖与生态限制"&gt;痛点：环境依赖与生态限制&lt;/h3&gt;
&lt;p&gt;在实现这个功能之前，我有过两个思考维度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;环境依赖&lt;/strong&gt;：无论是 Windows 的 Telnet 还是 Linux 的 &lt;code&gt;nc&lt;/code&gt;，都依赖当前操作系统的环境配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小程序限制&lt;/strong&gt;：微信小程序虽然有网络请求能力，但它必须配置服务器域名白名单。如果你想直接用小程序前端去探测一个随机的 IP 或端口，微信的底层安全机制是不允许的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="方案go-后端代劳小程序只做遥控器"&gt;方案：Go 后端代劳，小程序只做“遥控器”&lt;/h3&gt;
&lt;p&gt;为了绕过这些限制，我在“豆子工具”里实现了一个&lt;strong&gt;远程端口检测&lt;/strong&gt;功能。它的逻辑非常直接且高效：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端交互&lt;/strong&gt;：用户只需在小程序里输入目标服务器的 &lt;strong&gt;IP/域名&lt;/strong&gt; 和 &lt;strong&gt;端口号&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端核心&lt;/strong&gt;：数据提交到我用 &lt;strong&gt;Go&lt;/strong&gt; 编写的后端服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟连接&lt;/strong&gt;：后端服务代替用户执行 TCP 连接探测。Go 语言的 &lt;code&gt;net.DialTimeout&lt;/code&gt; 在这里非常实用，可以精准控制探测时间，避免由于网络超时导致的页面死等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果反馈&lt;/strong&gt;：后端将“连接成功”或“连接失败”的结果返回给小程序展示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="服务端口检测" loading="lazy" src="https://blog.91demo.top/images/wander/tool-chkport.webp"&gt;&lt;/p&gt;
&lt;h3 id="价值随时随地的运维眼"&gt;价值：随时随地的“运维眼”&lt;/h3&gt;
&lt;p&gt;自从这个功能上线后，我解决了很多尴尬的场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;排查防火墙&lt;/strong&gt;：在云厂商后台改了安全组策略后，掏出手机点一下，秒知是否生效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现场交付&lt;/strong&gt;：在客户现场没有电脑时，快速确认后端服务是否已经拉起。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零部署成本&lt;/strong&gt;：再也不用担心当前电脑有没有装 Telnet 或 Netcat。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;这里的难点在于服务器端实现，小程序端仅仅提交FORM表单就可以了。下面我们看看服务端的Go的核心实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 检测端口是否被打开？
func IsPortOpened(ip string, port int) bool {
addr := net.JoinHostPort(ip, fmt.Sprintf(&amp;#34;%d&amp;#34;, port))
conn, err := net.DialTimeout(&amp;#34;tcp&amp;#34;, addr, time.Second*3)
if err != nil {
return false
}
if conn != nil {
conn.Close()
return true
}
return false
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里仅能探测TCP端口，UDP端口我还没有思路，等有思路了我再在这篇文章里补充。&lt;/p&gt;</description></item><item><title>实战笔记：为了管好那堆记不住的密码，我给自己写了个全加密密码本</title><link>https://blog.91demo.top/tool-pwbook/</link><pubDate>Thu, 11 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/tool-pwbook/</guid><description>&lt;p&gt;虽然现在的 App 普遍支持手机号或微信一键登录，但对于开发者来说，GitHub、服务器、各类海外服务的登录依然离不开“邮箱+密码”的传统模式。&lt;/p&gt;
&lt;p&gt;我曾经历过一段漫长的“密码管理进化史”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Excel 时代&lt;/strong&gt;：最早用一个加密的 Excel 表格记录，在电脑端还凑合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flutter App 时代&lt;/strong&gt;：为了手机查看方便，我曾写过一个简单的 Flutter App，支持指纹查看，但功能简陋，只有新增功能，连“修改”功能都没有。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;直到有一次，GitHub 要求我重新登录，由于我频繁重置密码且新旧密码不能重复，导致我彻底记混了。在另一台电脑前尝试了无数次失败后，我意识到：&lt;strong&gt;我需要一个足够安全、随时可用、功能完整的个人密码管理工具。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;于是，“豆子工具”里的&lt;strong&gt;小程序版密码本&lt;/strong&gt;诞生了。&lt;/p&gt;
&lt;p&gt;&lt;img alt="我的密码本" loading="lazy" src="https://blog.91demo.top/images/wander/tool-pwbook.webp"&gt;&lt;/p&gt;
&lt;h3 id="1-设计核心绝对的隐私与自由"&gt;1. 设计核心：绝对的隐私与自由&lt;/h3&gt;
&lt;p&gt;在设计之初，我就定下了两个原则：&lt;strong&gt;不联网、重加密。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯离线运行&lt;/strong&gt;：为了打消用户（包括我自己）对隐私的顾虑，我砍掉了所有联网备份功能。所有的密码数据仅存储在手机本地，备份只能通过聊天文件导出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三重加密体系&lt;/strong&gt;：这是我花费心血最多的地方。主密码（Master Password）不存储在设备上。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一步&lt;/strong&gt;：用主密码解密 &lt;strong&gt;RSA 私钥&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二步&lt;/strong&gt;：用私钥解密 &lt;strong&gt;AES 密钥&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三步&lt;/strong&gt;：用 AES 密钥解密具体的 &lt;strong&gt;JSON 序列化记录&lt;/strong&gt;。&lt;br&gt;
这种混合加密机制确保了即使手机丢失，只要主密码不泄露，数据依然是安全的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-攻克开发中的大山"&gt;2. 攻克开发中的“大山”&lt;/h3&gt;
&lt;p&gt;这个功能的开发过程远比我想象中痛苦。尤其是在小程序环境下处理&lt;strong&gt;文件缓存、二进制流转换和加解密逻辑&lt;/strong&gt;，经常一个 Bug 就要调试好几天。&lt;/p&gt;
&lt;p&gt;中途我好几次想过放弃，觉得用 Excel 也可以凑合。但每次想到反复重置密码的痛苦，还是咬牙坚持了下来。为了稳定性，我将原本不稳定的二进制存储改为了 UTF-8 编码的 JSON 序列化，最终实现了丝滑的操作体验，这都是心血的教训。&lt;/p&gt;
&lt;h3 id="3-功能完善不仅仅是存一下"&gt;3. 功能完善：不仅仅是“存一下”&lt;/h3&gt;
&lt;p&gt;相比之前的 Flutter 版本，这次我补全了所有短板：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增删改查&lt;/strong&gt;：支持搜索功能，输入标题就能快速定位账号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;备份恢复&lt;/strong&gt;：加入了完整的备份机制，更换手机时可以轻松迁移数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量操作&lt;/strong&gt;：支持导入和导出，方便从其他平台平替过来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密码提示&lt;/strong&gt;：为了防范“忘记主密码”这个终极灾难，我加入了密码提示功能（建议设一个只有自己懂的暗语）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-字段灵活满足多样需求"&gt;4. 字段灵活，满足多样需求&lt;/h3&gt;
&lt;p&gt;每一条记录都包含了：&lt;strong&gt;标题、URL、用户名、密码、备注&lt;/strong&gt;。&lt;br&gt;
无论是一个服务器的 SSH 密码，还是一个冷门网站的登录信息，都能井井有条地分类存放。&lt;/p&gt;
&lt;h3 id="5-核心代码展示"&gt;5. 核心代码展示&lt;/h3&gt;
&lt;p&gt;纯小程序本地原生实现，首先是密码本初始化，它会根据用户输入的主密码，生成密码本文件。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 第一次使用设置方法
async setupFirstTime(masterPassword, passwordHint) {
try {
// 1. 检查是否已有密码本
if (!passwordManager.hasPasswordBook()) {
console.log(&amp;#39;1. 创建空密码本...&amp;#39;);
const createResult = await passwordManager.createEmptyPasswordBook(masterPassword, passwordHint);
if (createResult.success) {
console.log(&amp;#39;✓&amp;#39;, createResult.message);
} else {
console.log(&amp;#39;创建失败:&amp;#39;, createResult.error);
wx.showToast({
title: createResult.error,
icon: &amp;#39;none&amp;#39;
});
return;
}
}
} catch (err) {
console.log(&amp;#39;创建密码本错误，&amp;#39;, err)
}
// 保存主密码安装和提示
wx.setStorageSync(&amp;#39;pwbookInstall&amp;#39;, new Date().toLocaleString());
wx.setStorageSync(&amp;#39;passwordHint&amp;#39;, passwordHint || &amp;#39;&amp;#39;);
wx.setStorageSync(&amp;#39;pwbookBakTime&amp;#39;, getSecTs());
this.setData({
isFirstTime: false,
isUnlocked: true,
showPasswordDialog: false
});
wx.showToast({
title: &amp;#39;初始化成功&amp;#39;,
icon: &amp;#39;success&amp;#39;
});
this.loadPasswordList();
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;创建密码本文件核心代码：&lt;/p&gt;</description></item><item><title>实战笔记：为了不再漏掉任何一个域名到期提醒，我做了个自动化检测工具</title><link>https://blog.91demo.top/check-domain/</link><pubDate>Wed, 10 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/check-domain/</guid><description>&lt;p&gt;记录如何通过 Go 语言实现多域名 SSL 证书到期自动巡检，并通过钉钉/企业微信机器人实现精准预警。&lt;/p&gt;
&lt;p&gt;在运维工作中，有一类事故极其低级却又杀伤力巨大：&lt;strong&gt;SSL 证书过期。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="痛点被动挨打的救火模式"&gt;痛点：被动挨打的“救火”模式&lt;/h3&gt;
&lt;p&gt;由于业务需要，我手里管理着大量客户的域名。每个客户购买证书的渠道各异（阿里云、腾讯云或其他厂商），证书下来后通过微信或邮件发给运维，再手动配置到服务器上。&lt;/p&gt;
&lt;p&gt;这套流程在客户少的时候还算正常。但随着客户增多，问题出现了：&lt;strong&gt;证书到期时间不一，全靠人工记忆。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;总会有那么一两次，因为忙碌或者交接疏忽，某个域名证书悄悄过期了。直到客户反馈 APP 无法访问、浏览器弹出红色的安全告警，我们才急忙去“救火”。这种被动的局面不仅影响专业度，也给客户带来了实际损失。&lt;/p&gt;
&lt;h3 id="方案主动出击的自动化巡检"&gt;方案：主动出击的自动化巡检&lt;/h3&gt;
&lt;p&gt;为了彻底根治这个“心病”，我决定做一个自动化的域名证书检测工具。&lt;/p&gt;
&lt;p&gt;我的设想很简单：&lt;strong&gt;变“人找信息”为“信息找人”。&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="1-实现原理"&gt;1. 实现原理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置简单化&lt;/strong&gt;：将所有需要监测的域名汇总成一个 &lt;code&gt;txt&lt;/code&gt; 文件，每行一个域名，管理起来极其方便。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心引擎（Go）&lt;/strong&gt;：使用 Go 语言开发后端服务，利用 &lt;code&gt;cron&lt;/code&gt; 库开启定时任务，设定每天固定时间（如凌晨 4 点）执行一次巡检。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测逻辑&lt;/strong&gt;：程序自动循环读取域名列表，通过 TLS 握手获取证书的有效载荷，计算当前的剩余天数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精准预警&lt;/strong&gt;：我设定了一个“7天阈值”。一旦发现有域名将在 7 天内过期，程序会立即将这些域名汇总，并通过企微或者钉钉送达。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2-消息触达为什么选择机器人"&gt;2. 消息触达：为什么选择机器人？&lt;/h4&gt;
&lt;p&gt;在小程序中，邮箱属于敏感隐私资料，审核往往比较严格。为了避开这个麻烦，同时也为了让通知更具实时性，我选择了&lt;strong&gt;钉钉机器人&lt;/strong&gt;和&lt;strong&gt;企业微信机器人&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;管理员或运营人员只需将机器人的 &lt;code&gt;Webhook&lt;/code&gt; 地址配置好，每天早上一上班，就能在手机上收到一份清晰的到期清单。&lt;/p&gt;
&lt;p&gt;&lt;img alt="域名证书检测" loading="lazy" src="https://blog.91demo.top/images/wander/tool-chkdomain.webp"&gt;&lt;/p&gt;
&lt;h3 id="价值买到了最宝贵的时间"&gt;价值：买到了最宝贵的“时间”&lt;/h3&gt;
&lt;p&gt;这个功能上线后，我们最直接的收获就是**“沟通时间”**：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提前预判&lt;/strong&gt;：有了 7 天的缓冲期，运营人员可以气定神闲地与客户沟通续费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提前操作&lt;/strong&gt;：运维人员有了充裕的时间更换新证书，彻底杜绝了“半夜修证书”的尴尬。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;整个域名检测分为两部分：小程序端和服务端，小程序端负责提交域名信息以及Webhook（钉钉、企微）信息。以及查看导入的域名。服务端需要存储域名以及Webhook。并在每天的固定时刻进行域名巡检，当域名证书有效期小于7天时，将发送通知给管理员。&lt;/p&gt;
&lt;p&gt;小程序端主要是常规的信息提交，我们来看下后端的Go代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 域名证书检测计划任务，每天固定6点执行
func CronCertCheck() {
logger.Info(&amp;#34;启动域名证书检测计划任务&amp;#34;)
// 获取用户webhook，有webhook则进行域名检测
webhooks, err := dao.GetAllWebhook()
if err != nil {
logger.Error(&amp;#34;计划任务域名证书检测加载Webhooks错误，&amp;#34;, err)
return
}
// 循环扫描
for _, v := range webhooks {
uid := v.Uid
url := v.Url
ht := v.HookType
// 获取单个用户的域名
dbDomains, err := dao.GetWDomainByUid(uid)
if err != nil {
logger.Error(uid, &amp;#34;计划任务域名证书检测获取域名列表错误，&amp;#34;, err)
continue
}
if len(dbDomains) == 0 {
logger.Error(uid, &amp;#34;计划任务域名证书检测用户域名数量为0&amp;#34;)
continue
}
// 解密URL地址为WEBHOOK
password := fmt.Sprintf(&amp;#34;domain:%d:%d&amp;#34;, uid, ht)
webhook, err := cryptoutil.AesDecryptWithPassword(url, password)
if err != nil {
logger.Error(uid, &amp;#34;计划任务域名证书检测webhook解密失败，&amp;#34;, err)
continue
}
// 获取域名列表，需要去重，用户是可以上传重复域名的。
domains := make([]string, 0)
domainMap := make(map[string]bool, 0)
for _, n := range dbDomains {
name := n.Name
isExist := domainMap[name]
if isExist {
continue
}
domains = append(domains, name)
domainMap[name] = true
}
// 开始批次检测，并将检测结果返回
chkResult := CheckDomainsCert(domains, nil)
// 如果结果不为空，则发送webhook通知
if len(chkResult) &amp;gt; 0 {
notifyCfg := NotifyConfig{
Type: ht,
URL: webhook,
RetryTimes: 3,
RetryDelay: 5,
}
result := NotifyContent{
Title: &amp;#34;以下域名的SSL证书将在7天内过期:&amp;#34;,
Domains: chkResult,
}
go SendWebhookNotificationWithRetry(notifyCfg, result)
}
// 休息1秒
time.Sleep(1 * time.Second)
}
logger.Info(&amp;#34;域名证书检测处理：&amp;#34;, len(webhooks))
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在小程序上线后，用户使用极少，我不知道具体原因，可能是不信任功能，Webhook不想暴露，域名不想暴露？我又开发了客户端工具，纯本地运行，使用Wails3开发，是图形化桌面客户端，域名导入和通知都是在本地运行。并且增加了很多功能。例如支持HTTP/3，支持系统托盘，支持开启启动，支持导入更多域名，支持更多Webhook通知，监控面板更精细等。&lt;/p&gt;</description></item><item><title>实战笔记：为了不再发错下载链接，我给工具箱加了“扫码鉴定”</title><link>https://blog.91demo.top/tool-scan/</link><pubDate>Tue, 09 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/tool-scan/</guid><description>&lt;p&gt;小工具可以解决大问题。在软件发布流程中，最尴尬的事情莫过于：宣传图已经发出去了，用户扫码后却发现链接打不开，或者下载了一个旧版本的安装包。&lt;/p&gt;
&lt;p&gt;我就亲身经历过这么一次“翻车”事故。&lt;/p&gt;
&lt;h3 id="事故现场一个-url-引起的麻烦"&gt;事故现场：一个 URL 引起的麻烦&lt;/h3&gt;
&lt;p&gt;有一次，我在更新软件下载页面后，由于换了一个新的 URL 地址，在生成二维码阶段没有进行严格校验，就直接把二维码发布了出去。&lt;/p&gt;
&lt;p&gt;结果当用户兴冲冲地扫码下载时，发现软件运行异常。我对比了半天才发现，生成二维码时填写的 URL 链接版本号写错了一个字符，导致用户下载到了一个有问题的版本，这是偶然中的极端。&lt;/p&gt;
&lt;p&gt;虽然这只是一个低级错误，但它让我意识到：&lt;strong&gt;在二维码挂载后、正式发布前，必须有一个极简的“内容鉴定”环节。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="痛点如何快速无痛地校验"&gt;痛点：如何快速、无痛地校验？&lt;/h3&gt;
&lt;p&gt;通常我们校验二维码，习惯性地直接拿手机扫一下。但如果二维码指向的是一个大容量的 APP 下载包或复杂的跳转链接，扫码后手机会自动触发下载或进入复杂的网页路径。&lt;/p&gt;
&lt;p&gt;我其实并不想真的下载并安装测试（那是后续的 QA 环节），我只是想&lt;strong&gt;一眼看到二维码里到底写了什么字符&lt;/strong&gt;，确定那个 URL 是不是我想要的那一个。&lt;/p&gt;
&lt;h3 id="实现借力小程序的原生能力"&gt;实现：借力小程序的原生能力&lt;/h3&gt;
&lt;p&gt;既然发现了痛点，解决起来就非常顺手了。得益于微信生态对扫码的深度支持，我在“豆子工具”里实现了一个“二维码识别”功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑&lt;/strong&gt;：直接调用小程序的 &lt;code&gt;wx.scanCode&lt;/code&gt; 接口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能表现&lt;/strong&gt;：不仅支持扫描实物二维码，还支持从手机相册里直接读取生成的二维码图片，还支持小程序码扫描，支持查看页面路径和scene值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果反馈&lt;/strong&gt;：系统不会触发自动跳转，而是将二维码包含的原始文本、URL、一维码内容直接以&lt;strong&gt;纯文本&lt;/strong&gt;的形式展示在屏幕上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="价值多看一眼少出一次错"&gt;价值：多看一眼，少出一次错&lt;/h3&gt;
&lt;p&gt;现在，每当我生成一个新的二维码，无论是用于软件下载、文档分享还是活动跳转，我都会习惯性地用自己的工具“扫一下”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认 URL 参数是否完整。&lt;/li&gt;
&lt;li&gt;确认是否有肉眼难以察觉的拼写错误。&lt;/li&gt;
&lt;li&gt;确认二维码的类型（一维码还是二维码）是否符合预期。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="识别二维码" loading="lazy" src="https://blog.91demo.top/images/wander/tool-scan.webp"&gt;&lt;/p&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;使用小程序纯原生实现，仅仅做一个页面展示即可。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;scanQRCode: function () {
const that = this;
wx.showLoading({
title: &amp;#39;识别中...&amp;#39;,
mask: true,
})
wx.scanCode({
onlyFromCamera: false,
scanType: [&amp;#39;qrCode&amp;#39;, &amp;#39;barCode&amp;#39;, &amp;#39;datamatrix&amp;#39;, &amp;#39;pdf417&amp;#39;, &amp;#39;wxCode&amp;#39;],
success: (res) =&amp;gt; {
wx.hideLoading()
if(res.scanType==&amp;#39;WX_CODE&amp;#39;){
that.setData({
scanResult: res.path
});
}else{
that.setData({
scanResult: res.result
});
}
},
fail: (err) =&amp;gt; {
wx.hideLoading()
console.error(err);
}
});
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;需要注意的是，小程序码和其它码的结果字段有点不同，所以需要区分，以便取到正确的值。&lt;/p&gt;</description></item><item><title>实战笔记：从一个按钮到全能生成器，随机数功能的“进化论”</title><link>https://blog.91demo.top/tool-random/</link><pubDate>Sun, 07 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/tool-random/</guid><description>&lt;p&gt;这个工具是我的图腾，还记得我之前提到的吗？2018 年，“豆子工具”的初版只有一个功能：点一下按钮，生成一个随机数。&lt;/p&gt;
&lt;p&gt;那时候的代码逻辑极其简单，甚至算不上一个“工具”。但随着这几年自己在开发、运维过程中不断遇到“需要一个复杂密码”、“需要一个 16 位纯金钥”或者“需要一个全大写 ID”等实际场景，我发现&lt;strong&gt;简单的随机数其实并不简单&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="痛点单一随机数的局限"&gt;痛点：单一随机数的局限&lt;/h3&gt;
&lt;p&gt;早期的随机数功能非常死板，生成的格式往往不是我想要的。每次生成后，我可能还需要手动去改长度、改大小写，甚至手动补上特殊字符。&lt;/p&gt;
&lt;p&gt;作为一个开发者，如果一个工具不能让我“一键到位”，那就是不合格的。&lt;/p&gt;
&lt;h3 id="进化高度自定义的融合模式"&gt;进化：高度自定义的“融合模式”&lt;/h3&gt;
&lt;p&gt;于是，我把这些年所有关于“随机”的需求全部揉碎、重组，将它升级成了一个全能的生成器。&lt;/p&gt;
&lt;p&gt;现在的随机数功能，不再是盲目地随机，而是&lt;strong&gt;基于规则的精准生成&lt;/strong&gt;。我为其设计了一套组合逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;长度自定义&lt;/strong&gt;：不再局限于几位数字，用户可以根据需求自由输入长度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字符集自由组合&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯数字&lt;/strong&gt;：适用于验证码类场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯字母&lt;/strong&gt;：适用于临时 ID 或变量命名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字母+数字&lt;/strong&gt;：兼顾强度与易读性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含特殊字符&lt;/strong&gt;：专门为高强度随机密码设计。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果格式化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;支持结果一键&lt;strong&gt;转大写&lt;/strong&gt;或&lt;strong&gt;转小写&lt;/strong&gt;。这在配置某些特定系统的 API Key 时非常有用，省去了手动切换输入法的麻烦。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我经常使用它生成一些随机数作为密码，密钥。我不用去数长度，或者生成后，我简单调整一下，让它有一点意义，我再去使用。&lt;/p&gt;
&lt;p&gt;它使用小程序本地实现，下面是一段核心代码，描述了如何去实现随机数：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 生成随机字符串
genRndStr(rtype) {
const that = this;
if (utils.isEmpty(that.data.input1)) {
wx.showToast({
title: &amp;#39;内容为空&amp;#39;,
})
return
}
let result = &amp;#39;&amp;#39;;
switch (rtype) {
case 1:
// 纯数字
result = utils.randNumber(that.data.input1);
break;
case 2:
// 纯字母
result = utils.randLetter(that.data.input1);
break;
case 3:
// 字母和数字
result = utils.randStr(that.data.input1);
break;
case 4:
// 含特殊字符
result = utils.randSymbol(that.data.input1);
break;
}
that.setData({
result1: result
})
},
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="思考小功能里的产品观"&gt;思考：小功能里的“产品观”&lt;/h3&gt;
&lt;p&gt;虽然随机数在技术实现上只是简单的字符数组随机采样，但从产品角度看，它体现了一种**“减少用户操作”**的原则。&lt;/p&gt;</description></item><item><title>实战笔记：知识真的就是金钱，聊聊我的局域网调试工具</title><link>https://blog.91demo.top/debug-tcp/</link><pubDate>Sat, 06 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/debug-tcp/</guid><description>&lt;p&gt;在“豆子工具”的所有功能中，&lt;strong&gt;局域网调试工具（TCP/UDP/WebSocket）&lt;/strong&gt; 并不是受众最广的，但它却是我最引以为傲的一个。&lt;/p&gt;
&lt;p&gt;因为它让我真切地体会到了一句话：&lt;strong&gt;知识就是力量，知识就是金钱。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="缘起把调试器揣进口袋"&gt;缘起：把调试器揣进口袋&lt;/h3&gt;
&lt;p&gt;做嵌入式开发或者网络协议调试的人都知道，以往测试局域网通信，必须背着电脑，接上串口线或网线，蹲在机柜旁守着。我当时就在想：既然微信小程序已经开放了网络通信的能力，为什么我不能做一个随身携带的调试器呢？&lt;/p&gt;
&lt;p&gt;于是，我深度调用了小程序的 &lt;code&gt;wx.createTCPSocket&lt;/code&gt;、&lt;code&gt;wx.createUDPSocket&lt;/code&gt; 和 &lt;code&gt;WebSocket&lt;/code&gt; API，在“豆子工具”里构建了一个完整的网络测试模块。它支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;十六进制（Hex）与 ASCII 码切换&lt;/li&gt;
&lt;li&gt;实时数据日志展示&lt;/li&gt;
&lt;li&gt;局域网内稳定的数据收发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="TCP调试工具" loading="lazy" src="https://blog.91demo.top/images/wander/tool-tcp.webp"&gt;&lt;/p&gt;
&lt;h3 id="变现从技术分享到第一桶金"&gt;变现：从技术分享到“第一桶金”&lt;/h3&gt;
&lt;p&gt;工具做成后，我并没有藏着掖着，而是把实现原理和核心逻辑整理成文，发布到了&lt;strong&gt;微信开发者社区&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;没过多久，一位用户通过那篇文章联系到了我。他在工业自动化领域遇到了一个难题：需要一套能够定制化采集设备数据的微信小程序工具，而协议正是基于 &lt;strong&gt;Modbus/TCP&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于我的“豆子工具”已经打好了坚实的底层基础，我对原有的 TCP 调试模块进行了针对性的修改，迅速就适配出了满足他实际业务场景的采集方案。&lt;/p&gt;
&lt;p&gt;虽然这笔订单带来的“第一桶金”数额并不算惊人，但它对我的意义非凡。它验证了一个逻辑：&lt;strong&gt;当你把一个细分领域的工具做到极致，并愿意分享出去时，价值自然会找上门来。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="技术实现"&gt;技术实现&lt;/h3&gt;
&lt;p&gt;这个工具是纯小程序实现，它调用微信小程序的网络API。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;doConnServer() {
const that = this;
wx.getNetworkType({
success(res) {
const networkType = res.networkType
if (networkType !== &amp;#39;wifi&amp;#39;) {
wx.showToast({
title: &amp;#39;请打开WIFI&amp;#39;,
})
return
}
that.connTcp();
}
});
},
connTcp() {
const that = this;
if (!utils.isEmpty(t)) {
t.offMessage()
t.offConnect()
t.offError()
t.offClose()
t.close()
t = null;
that.setData({
isConn: false,
tips: &amp;#34;关闭成功&amp;#34;
})
wx.showToast({
title: &amp;#39;关闭成功&amp;#39;,
})
return
} else {
let ip = that.data.ip;
let port = that.data.port;
if (!utils.isValidIP(ip)) {
wx.showToast({
title: &amp;#39;无效IP地址&amp;#39;,
})
return
}
if (!utils.isPrivateIP(ip)) {
wx.showToast({
title: &amp;#39;请使用私有IP&amp;#39;,
})
return
}
if (utils.containInvalidPort(port)) {
wx.showToast({
title: &amp;#39;非法端口&amp;#39;,
})
return
}
t = wx.createTCPSocket()
t.onError(that.tErr)
t.onConnect(that.tConn)
t.onMessage(that.tMsg)
t.onClose(that.tClose)
t.connect({
address: ip,
port: port,
timeout: 3,
})
}
},
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这是部分核心TCP代码，它是TCP创建连接的核心代码。完整代码请从下面地址中的豆子碎片小程序内下载：&lt;a href="https://91demo.top/tools/"&gt;https://91demo.top/tools/&lt;/a&gt;&lt;/p&gt;</description></item><item><title>实战笔记：为了省下服务器流量费，我给小程序加上了 Webp 转换</title><link>https://blog.91demo.top/image-convert/</link><pubDate>Fri, 05 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/image-convert/</guid><description>&lt;p&gt;在“流量贵如油”的今天，作为一名自己买服务器、撸代码的站长，如何给服务器“减负”是每天都要思考的必修课。&lt;/p&gt;
&lt;h3 id="痛点博客网站的流量杀手"&gt;痛点：博客网站的流量杀手&lt;/h3&gt;
&lt;p&gt;我有一个运行多年的个人博客。在分析服务器日志时发现，最大的流量开销并不是文字，而是文章里那些高清的图片。&lt;/p&gt;
&lt;p&gt;虽然 JPG 和 PNG 已经很普及，但随着屏幕分辨率越来越高，原图动辄几 MB，对于按带宽付费或者流量计费的服务器来说，这都是白花花的银子。&lt;/p&gt;
&lt;h3 id="方案webp-格式的降维打击"&gt;方案：Webp 格式的“降维打击”&lt;/h3&gt;
&lt;p&gt;现在主流浏览器（Chrome, Safari, Edge 等）已经全面支持 &lt;strong&gt;Webp&lt;/strong&gt; 格式。相比于传统的 JPG 和 PNG，Webp 可以在保证肉眼看不出画质损失的前提下，将文件体积&lt;strong&gt;压缩 30% 到 70%&lt;/strong&gt;。这是一个非常惊人的数据。这意味着原本 100GB 的图床流量，换成 Webp 后可能只需要 30GB。&lt;/p&gt;
&lt;p&gt;为了方便处理博客素材，我决定把这个需求也集成到“豆子工具”里。&lt;/p&gt;
&lt;p&gt;&lt;img alt="图片格式转换" loading="lazy" src="https://blog.91demo.top/images/wander/image-convert.webp"&gt;&lt;/p&gt;
&lt;h3 id="实现基于-google-官方工具链"&gt;实现：基于 Google 官方工具链&lt;/h3&gt;
&lt;p&gt;图片转换的实现原理与我之前的“音频转换”方案异曲同工，主打一个&lt;strong&gt;稳定与高效&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;核心引擎&lt;/strong&gt;：在服务端安装了 Google 官方出品的 &lt;code&gt;webp&lt;/code&gt; 命令行工具链（&lt;code&gt;cwebp&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端调度&lt;/strong&gt;：依然由 &lt;strong&gt;Go&lt;/strong&gt; 语言担任“指挥官”，接收小程序上传的 PNG/JPG 原图，调用外部 &lt;code&gt;cwebp&lt;/code&gt; 进程进行压缩。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数优化&lt;/strong&gt;：在后端我预设了平衡性最好的质量参数，确保图片在压缩后的清晰度依然能够满足博客展示的需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;闭环应用&lt;/strong&gt;：现在我写每一篇博客前，都会先用小程序把配图过一遍，转成 Webp 后再上传。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是核心代码片段的注解，这是小程序端：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;page-meta root-font-size=&amp;#34;system&amp;#34; /&amp;gt;
&amp;lt;view class=&amp;#34;page&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;page__hd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;page__title&amp;#34;&amp;gt;图片格式转换&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;page__desc&amp;#34;&amp;gt;可将图片转换为WebP格式，用于Web项目。&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;page__bd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cells weui-cells_radio&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell weui-cell_uploader&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell__bd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__hd&amp;#34;&amp;gt;
&amp;lt;view aria-role=&amp;#34;option&amp;#34; class=&amp;#34;weui-uploader__overview&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__title&amp;#34;&amp;gt;图片信息&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__tips&amp;#34;&amp;gt;
请选取5M内的图片，目前仅支持一张
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__bd&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__files&amp;#34; id=&amp;#34;uploaderFiles&amp;#34;&amp;gt;
&amp;lt;block wx:for=&amp;#34;{{files}}&amp;#34; wx:key=&amp;#34;*this&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__file&amp;#34; bindtap=&amp;#34;previewImage&amp;#34; id=&amp;#34;{{item}}&amp;#34;&amp;gt;
&amp;lt;image class=&amp;#34;weui-uploader__img&amp;#34; src=&amp;#34;{{item}}&amp;#34; mode=&amp;#34;aspectFill&amp;#34; /&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/block&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-uploader__input-box&amp;#34;&amp;gt;
&amp;lt;view aria-role=&amp;#34;button&amp;#34; aria-label=&amp;#34;选择图片&amp;#34; class=&amp;#34;weui-uploader__input&amp;#34; bindtap=&amp;#34;chooseImage&amp;#34;&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view wx:if=&amp;#34;{{headImageExist}}&amp;#34;&amp;gt;
&amp;lt;image style=&amp;#34;width: 200px; height: 200px; background-color: #eeeeee;&amp;#34; mode=&amp;#34;aspectFit&amp;#34; src=&amp;#34;{{headImage}}&amp;#34; bindtap=&amp;#34;previewMakeImage&amp;#34;&amp;gt;&amp;lt;/image&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view wx:else&amp;gt;&amp;lt;text&amp;gt;转换完成的图片会展示在这里&amp;lt;/text&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;button-sp-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_primary weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;imageConvert&amp;#34;&amp;gt;开始转换&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_warn weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;saveToPhotosAlbum&amp;#34;&amp;gt;保存到相册&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面是界面骨架，现在说下它的灵魂。首先通过小程序的上传文件API，wx.uploadfile将图片文件上传到服务器，然后使用Go调用cwebp进行格式转换，转换成功后，下载文件到小程序本地，然后删除服务器端的文件。&lt;/p&gt;</description></item><item><title>实战笔记：我把 FFmpeg 搬进小程序，搞定了音频格式转换</title><link>https://blog.91demo.top/audio-convert/</link><pubDate>Thu, 04 Jan 2018 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/audio-convert/</guid><description>&lt;p&gt;在“豆子工具”众多的功能里，&lt;strong&gt;音频转换（m4a 转 mp3）&lt;/strong&gt; 是我使用频率最高、也最具有“个人救赎”色彩的一个。&lt;/p&gt;
&lt;h3 id="起因被软件更新背刺后的郁闷"&gt;起因：被软件更新“背刺”后的郁闷&lt;/h3&gt;
&lt;p&gt;这个功能的由来非常接地气：有一段时间，我需要频繁地将苹果手机录音产生的 &lt;code&gt;m4a&lt;/code&gt; 格式文件转换成 &lt;code&gt;mp3&lt;/code&gt;，因为当时某个必须使用的业务软件只认 &lt;code&gt;mp3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那时候我找遍了各种转换工具。最后发现某款主流音乐软件自带的转换功能挺好用。然而，好景不长，在一次软件自动更新后，这个功能竟然被砍掉了。我去搜老版本安装包，却发现根本找不到安全的下载路径。&lt;/p&gt;
&lt;p&gt;那种“被绑架”的无奈感，相信每个工具控都深有体会。&lt;/p&gt;
&lt;h3 id="进阶大名鼎鼎的-ffmpeg"&gt;进阶：大名鼎鼎的 ffmpeg&lt;/h3&gt;
&lt;p&gt;郁闷之后，我转向了技术人的终极方案——&lt;strong&gt;ffmpeg&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;命令行虽然硬核，但确实强大到无以复加。一条简单的指令就能解决所有问题：&lt;br&gt;
&lt;code&gt;ffmpeg -i input.m4a output.mp3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用了很长一段时间的命令行后，新的问题又来了，一次在外面，突然用户需要转换音频，我总不能随时随地都带着电脑吧？于是，我动了把 ffmpeg 搬进“豆子工具”的心思。&lt;/p&gt;
&lt;h3 id="实现极简架构下的随时随地"&gt;实现：极简架构下的“随时随地”&lt;/h3&gt;
&lt;p&gt;在小程序里实现这个功能，原理其实并不复杂，核心在于&lt;strong&gt;后端调度&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;前端上传&lt;/strong&gt;：小程序端选择 &lt;code&gt;m4a&lt;/code&gt; 文件，上传至服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端处理&lt;/strong&gt;：后端使用 &lt;strong&gt;Go&lt;/strong&gt; 语言接收文件，通过 &lt;code&gt;exec&lt;/code&gt; 模块调用服务器系统环境中的 &lt;code&gt;ffmpeg&lt;/code&gt; 进程进行转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时试听&lt;/strong&gt;：为了保证体验，我在小程序里集成了一个音频播放器。转换前可以听一下是否选对了文件，转换后也可以即时试听确认效果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;即用即删&lt;/strong&gt;：转换生成的文件在用户下载后会立即从服务器删除，既保护了隐私，也完全不占用宝贵的服务器存储空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是核心代码片段的注解，这是小程序端：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;page-meta root-font-size=&amp;#34;system&amp;#34; /&amp;gt;
&amp;lt;view class=&amp;#34;page&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-form&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__text-area&amp;#34;&amp;gt;
&amp;lt;h2 class=&amp;#34;weui-form__title&amp;#34;&amp;gt;音频格式转MP3&amp;lt;/h2&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__desc&amp;#34;&amp;gt;因微信限制，保存文件即为分享文件，可通过微信文件传输助手下载文件。目前仅支持以下格式（M4A、WAV、AMR）文件转换为MP3文件。&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__control-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cells__group weui-cells__group_form&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cells weui-cells_radio&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell__hd&amp;#34;&amp;gt;&amp;lt;label class=&amp;#34;weui-label&amp;#34;&amp;gt;文件&amp;lt;/label&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell__bd&amp;#34;&amp;gt;
&amp;lt;input class=&amp;#34;weui-cell__control weui-cell__control_flex weui-input&amp;#34; type=&amp;#34;text&amp;#34; placeholder=&amp;#34;请选取5M内的音频&amp;#34; placeholder-class=&amp;#34;weui-input__placeholder&amp;#34; value=&amp;#34;{{filename}}&amp;#34; /&amp;gt;
&amp;lt;view aria-role=&amp;#34;button&amp;#34; class=&amp;#34;weui-cell__control weui-btn weui-btn_default weui-vcode-btn&amp;#34; bindtap=&amp;#34;chooseMedia&amp;#34;&amp;gt;选择&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view wx:if=&amp;#34;{{audioExist}}&amp;#34;&amp;gt;
&amp;lt;audio name=&amp;#34;{{filename}}&amp;#34; src=&amp;#34;{{filepath}}&amp;#34; id=&amp;#34;myAudio&amp;#34; controls&amp;gt;&amp;lt;/audio&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view wx:else&amp;gt;&amp;lt;text&amp;gt;选择音频文件后可以在这里试听&amp;lt;/text&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-cell&amp;#34;&amp;gt;
&amp;lt;view wx:if=&amp;#34;{{makeAudioExist}}&amp;#34;&amp;gt;
&amp;lt;audio name=&amp;#34;{{makeAudioName}}&amp;#34; src=&amp;#34;{{makeAudioPath}}&amp;#34; id=&amp;#34;myAudio&amp;#34; controls&amp;gt;&amp;lt;/audio&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view wx:else&amp;gt;&amp;lt;text&amp;gt;转换完成的文件会展示在这里&amp;lt;/text&amp;gt;&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-form__opr-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;button-sp-area&amp;#34;&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_primary weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;audioConvert&amp;#34;&amp;gt;开始转换&amp;lt;/view&amp;gt;
&amp;lt;view class=&amp;#34;weui-btn weui-btn_warn weui-wa-hotarea&amp;#34; aria-role=&amp;#34;button&amp;#34; bindtap=&amp;#34;saveFile&amp;#34;&amp;gt;保存文件&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&amp;lt;/view&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面是界面骨架，现在说下它的灵魂。首先通过小程序的上传文件API，wx.uploadfile将音频文件上传到服务器，然后使用Go调用ffmpeg进行格式转换，转换成功后，下载文件到小程序本地，然后删除服务器端的文件。&lt;/p&gt;</description></item></channel></rss>