记一次使用egui 构建的轻量小程序码生成工具

最近要使用小程序码生成工具,捡起了以前开发的使用 Rust + native-windows-gui 打造极致轻量的小程序工具。 我发现这个工具最大的痛点就是每次打开都需要重复输入appid和appsecret。我决定修复它,我没有重新开始该项目,是因为我准备使用最近很流行的egui库。使用这个库的另一个原因是我为开发ESP网关客户端做技术储备。 在开发的中间,我还想使用iced,但是开发了一段时间放弃了,不是iced不好,是因为我没有精力。它适合长期的大型的项目。我现在想快速开发完成,所以egui的界面我忍受了。 开发完成的初版界面如下: 它极致轻量,只有一个小巧的运行文件,无需安装臃肿的运行库。响应迅速毫秒级启动,操作如丝般顺滑,没有多余的动效和加载等待。 下载工具后,放入你要生成小程序码的文件夹中,打开工具,填入小程序参数(AppID、路径等),一键获取高清太阳码,小程序码图片就会输出到本文件夹中。 最为暖心的是,应用重新启动后,不用重新输入appid和appsecret,并且会自动获取token。 下面看看核心代码片段: // --- 主应用状态 --- pub struct BeanApp { store: AppConfig, // 保存信息 appid_raw: String, // appid secret_raw: String, // appsecret access_token: Option<String>, // 存储 Token expires_in: u64, // 存储过期时间戳 (秒) output_dir: String, // 小程序码输出目录 is_pro: bool, // 是否开启了高级功能 active_tab: usize, // 0: 单张模式, 1: 批量模式 page_path: String, // 页面路径 scene_value: String, // 场景值 qr_size: u32, // 导出尺寸 show_crop_line: bool, // 是否显示裁剪线 preview_texture: Option<TextureHandle>, // 预览图句柄 status_msg: String, // 状态信息 show_password: bool, // 是否显示密码 tx: Sender<AppMessage>, // 发送端 rx: Receiver<AppMessage>, // 接收端 ctx: egui::Context, // 保存上下文,用于异步刷新 is_loading_token: bool, // 防止重复请求 } 这是最核心的内容,egui就靠这个结构体,让UI和业务逻辑进行交互。本质就是UI交互区修改这个结构体中的一些内容,然后调用业务逻辑去处理内容,然后再返回信息到这个结构体,然后UI读取这些值的内容再显示出来。 ...

2026-05-12 · 3 min · Eagle

一步一步将ESP8266 WIFI 配网搞定并丝滑体验

准备入手ESP网关项目了,手头有一块esp8266的开发小板子,就先从esp8266开始入手。白天搞定了ESP8266 的UDP配网编码和小程序端的控制编码。就一个功能模块给ESP8266配置WIFI网络。这是必须的入门操作,因为WIFI 的SSID和密码不能真的写入到代码中。 网络配置有很多种方式,我选择了SoftAP配网,虽然配置有点繁琐,但这是一种很可靠的配网方案,兼容性极高,不依赖手机硬件的特殊协议。以前做过HTTP配网,这是2025年时做的,到现在已经忘的差不多了。现在捡起来再回忆一下。 准确来说,这不应该算是一个项目,但因为它具有通用性和实用性。我决定把它记录下来,可以方便地应用到我的其它项目中。 Soft配网原理 初始化模式:ESP8266 启动后进入 WIFI_AP_STA 模式。它会开启一个无密码(或已知密码)的热点(AP),并运行一个轻量级的 Web 服务器(HTTP Server)。 通道建立:手机通过小程序或系统设置连接到该热点。此时手机与 ESP8266 处于同一个局域网内。 数据交互:小程序通过 HTTP Post 请求将目标 WiFi 的 SSID 和 Password 发送给 ESP8266 的固定接口(如 /config)。 校验与切换:ESP8266 收到参数后,尝试作为客户端(STA)连接路由器。如果连接成功,则关闭 AP 热点,保存参数到 Flash;如果失败,则返回并重置到AP状态。 ESP8266端核心代码(Arduino IDE)和小程序端代码 在Arduino IDE打开项目后,需要安装ESP8266 WebServer库。这个库提供了Web服务,可以接收其它HTTP客户端的连接。 它的核心是实现了一个非常简单的Web服务,它监听/config接口并能够接收WIFI信息,以及接收之后可以本地存储,实现动态配置的功能。 下面是Arduino端的代码片段: #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> // 定义AP热点的名称 const char* ap_ssid = "ESP8266_Config_Device"; ESP8266WebServer server(80); void handleConfig() { // 处理SSID和密码逻辑 } void setup() { // 设置为AP+STA模式 WiFi.mode(WIFI_AP_STA); WiFi.softAP(ap_ssid); // 注册接口 server.on("/config", HTTP_POST, handleConfig); server.begin(); } void loop() { server.handleClient(); if (WiFi.status() == WL_CONNECTED) { // 如果连接成功,可以根据需要在这里处理业务逻辑 } } 完整代码可以从https://91demo.top/tools/下载。小程序端则充当了HTTP客户端,它用来作为输入WIFI信息的终端。这个界面非常简单,一个WiFi信息提交表单,包含了SSID信息和密码信息,以及一个提交按钮。 ...

2026-04-29 · 3 min · Eagle

小程序激励广告防刷演进史:从前端校验到“挑战模式”

在开发“模块集市”时,用户通过观看激励广告获取源码下载地址。虽然对接广告并不难,但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调(Server-to-Server),奖励的发放完全依赖前端触发,这给了一些“羊毛党”可乘之机。 在接口防护的过程中,我经历了四个阶段: 第一阶段:身份鉴权(解决匿名刷取) 我的获取奖励接口是getAdReward。调用它就可以获取奖励,后台进行发放。最初,由于经验不足,我没有对它做任何防护。这样任何人任何客户端都可以进行访问。了解小程序微信账号体系后,我添加了鉴权,只有携带Token的用户可以访问。这样可以保证只有能够调用wx.login的用户才可以访问。 这解决了一部分问题,但是又有了新的问题,它防不住“真实用户”,只要用户登录后拦截并解析出接口地址和 Token,就可以跳过广告手动调用了getAdReward接口,没有观看广告就获取了奖励。 第二阶段:共享密钥加密(解决接口暴露) 为了防止直接调用接口,我引入了对称加密。前端与后端约定一个硬编码的共享密钥,在小程序端和服务端同时使用这个共享密钥加密随机数和时间戳进行校验,匹配则发放奖励。这样可以防止fidder等工具直接拦截调用接口。 它正常运行了一段时间,我发现又有了新的情况。用户可能通过反编译或者其它方式获取到了共享密钥。这在一段时间内对我造成困扰,除了定期更新共享密钥,我没有好的办法,但更新共享密钥需要升级发布小程序。直到当我了解到小程序提供加密网络通道时,我发现这是一个好的解决方案。 第三阶段:微信加密网络通道(解决密钥安全) 首先,我们来了解一下加密网络通道,它是微信为小程序提供的一套加密KEY机制,它可以同时在微信小程序端和服务器端获取相同的密钥。由于密钥由微信官方通道生成且动态更新,反编译源码也拿不到密钥。除非攻击者能拦截微信的网络通道,否则无法伪造解密过程。同时,配合时间戳校验,有效防御了初级的重放攻击。这对于我来说,是非常好的利好消息,我不需要在源码中硬编码共享密钥了。 我们可以参考获取短信验证码,短信验证码之所以安全,是因为应用和短信验证码是两个不相同的通道。同样的这个加密KEY,在小程序获取和服务端获取都是通过微信的网络通道,和自己的应用通道也不是一个。对于我的小程序稍加改造就可以了。在获取奖励的接口中,首先使用wx.getUserEncryptKey获取微信的加密KEY,然后使用自定义的AES加密方法,将要传递的时间戳和Nonce等参数加密后,将加密数据提交给服务器就可以了。当数据到达服务器,服务器同样调用微信的接口获取微信的加密KEY,然后进行AES解密。这样就可以防止获取共享密钥的弊端。 好了,我们来看看具体如何使用?为了避免小程序与开发者后台通信时数据被截取和篡改,微信侧维护了一个用户维度的可靠key,用于小程序和后台通信时进行加密和签名。开发者可以分别通过小程序前端和微信后台提供的接口,获取用户的加密 key。 1,小程序端获取: const somedata = 'xxxxx' 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) } }) } }) 其中,someAESEncryptMethod 和 someAESDEcryptMethod 分别为加解密函数,由开发者自行引入加解密库来实现,基础库暂时不提供加解密能力。 2,服务端获取: 在开发者服务端,可以调用getUserEncryptKey后台接口获取用户最近三次的key。在获取key的同时,接口会携带version信息,开发者可以比较version版本来选择使用对应的key对数据进行加解密。 curl -X POST "https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=ACCESS_TOKEN&openid=OPENID&signature=SIGNATURE&sig_method=hmac_sha256" 其中,openid是用户的openid,signature用sessionkey对空字符串签名得到的结果。即 signature = hmac_sha256(session_key, “"),sig_method为签名方法,固定为hmac_sha256。 这个是核心,获取激励广告奖励基于它再添加一些参数进行加解密。 第四阶段:后端“挑战模式”(解决重放与逻辑伪造) 我在参数中添加的Nonce,它是一个噪音参数,它是小程序端本地生成的,还有时间戳也是本地生成的,增加它们是为了增加解密难度。在实际运行中,我发现硬核玩家会通过修改本地时间戳和 Nonce(随机数)来绕过检测,或者使用相同的参数进行重放攻击,或者不够15s(广告正常播放最短时间)调用多次接口。 为了解决这个问题,我将逻辑升级为“挑战模式”:首先,小程序端在广告开始前进行一次预请求,前端必须先调用 getNonce 接口获取Nonce然后使用这个Nonce进行加密,getNonce是一个新增接口,用来生成Nonce,并记录生成时间和关联openid,服务端存入缓存(如 Redis),完成服务端打标。当前端再次调用getAdReward接口时,如果差值小于广告常规时长(如 15s),则判定为非正常观看,直接拒绝。 ...

2026-04-07 · 1 min · Eagle

我是如何设计和实现模块源码集市的?

当我开发完51单片机温度采集并转换Modbus RTU后,我一度想分享出去,但是我又不想草率的去分享。我一直在思考,如何能让这些代码发挥更大的价值,而不仅仅是躺在个人仓库里。作为一名摸爬滚打多年的开发者,我手头积累了大量来自实践的代码、模块和解决方案。它们散落在硬盘的各个角落,像一颗颗未经打磨的珍珠。 于是,我萌生了做一个“模块源码集市”小程序的想法——一个可以直接浏览、查阅、并一键下载优质源码的平台。 经过最近一段时间的集中开发,这个小程序终于和大家见面了。今天,我想和大家分享其背后的设计与思考,希望能为有相似想法的朋友提供一些参考。这个小程序的核心功能简洁而实用: 首页全景浏览:所有上架的代码模块都在首页清晰展示,你可以快速了解其名称、简介和分类,对全站资源一目了然。 沉浸式详情阅读:点击任一模块,即可进入详情页。这里我使用了Markdown渲染引擎,来展示模块的详细介绍、使用说明、技术要点等。Markdown的优雅排版能让技术文档的阅读体验大幅提升。 一键获取源码:在详情页,点击获取模块源码链接按钮,即可轻松获取模块的完整源码压缩包。 为了让这个简单的流程稳定、安全、可扩展,我在几个关键环节做了一些设计和取舍: 动态资源与安全通道源码文件并非直接打包在小程序内,而是存储在后台的MinIO对象存储中。当用户点击下载时,服务端会动态生成一个具有时效性的访问链接返回给小程序。这样做的好处是资源可以随时更新。同时,为了维持后端服务器持续运行,我在列表及详情中接入了少量广告。为了保护下载接口不被恶意刷取,我集成了激励式视频广告。用户观看一则简短的广告,即可解锁下载权限。这不仅是简单的广告接入,更重要的是,我为此构建了一套防刷机制,我使用了微信的加密网络通道,对关键数据进行加密传输和验证,极大地增加了自动化盗刷的成本和难度,保护了服务器资源。 灵活的沟通与通知我深知,一个“活”的产品需要与用户保持沟通。因此,小程序内设置了系统消息中心,我可以在这里向所有用户下发重要的系统公告、模块上新以及更新日志。更重要的是,我为每个模块的上新功能,接入了微信服务通知。当有新的、优秀的代码模块入库时,订阅了的用户会在早上8点后收到一条微信消息提醒,能让你随时跟上“仓库”的更新节奏。 持续生长的内容生态目前上架的或正在上架的模块,覆盖了我擅长的多个领域:小程序、Go语言后端工具、Rust系统编程实践、Web前端片段、嵌入式软硬件代码,包含音视频、网络、加密等技术要点,形式包括客户端、命令行工具、固件、小程序、网页等。这只是一个开始。我的计划是将其作为一个长期项目,持续将我实践中验证过的、有价值的代码沉淀、整理并开源出来。未来,我还会在项目详情页中,增加关联的公众号技术文章跳转,或是有功能关联的其他小程序跳转,形成一个立体化的技术知识网络。 坦率地说,这个小程序的开发过程远超我最初的预期。从架构设计到具体实现,尤其是几个关键的技术卡点上,耗费了我大量的精力。 与广告盗刷的攻防:我的服务器每天都有人在扫描,所以在实现激励广告防刷机制时,我花了大量时间加强防刷机制,其实这个在微信小程序社区中也可以看到很多类似的问题。我这里研究了一种解决方案。就是使用加密网络通道。在对接的过程中,我与各种加密错误、签名错误(如常见的40079错误)作斗争。例如,处理会话密钥时,发现不需要进行Base64编码,而是直接以空字符串的字节形式处理;在URL拼接的细节上,POST与GET方法的误用也会导致失败;而加解密环节更是陷阱重重——加密密钥需要从Base64解码,IV却是一个直接的16位字符串,加密数据则需要从十六进制格式解码……这些细枝末节,任何一个出错都会导致整个流程崩掉,往往需要清空Token重新登录来排查。这个过程让我对网络数据安全有了更深刻的认识。这些错误和经验本想专开一篇文章介绍,但是细想一下,可能一段话就能总结,但是其中的辛酸和感悟无法表达在纸面上。索性后期整理出来开源这个模块,减少其他开发者的一些坎坷。 存储方案的抉择:在对象存储的选择上,也有一段小插曲。我最熟悉的是Mino,但是在我了解到MinIO版本已归档,便尝试了rustfs等其他方案进行探索。但经过综合对比在可靠性、API友好度以及与现有技术栈的契合度后,最终还是选择了功能完备、文档清晰的MinIO作为存储后端,事实证明这个选择是稳定高效的。 所幸,在这个艰难的过程中,AI编程助手给予了我巨大的帮助(但是也让我吃了很多苦头,因为它上面的资料都比较旧)。许多代码片段的生成、调试过程中错误信息的解读、API文档的快速查询,都因AI的介入而效率倍增。如果没有它,单靠我自己,恐怕几个月也难见成果,绝无可能在十几天内推动项目成型上线。 做这个小程序,并非为了追逐热点,这个小程序刚刚上线,还需要时间去细细打磨。开发其初心很简单:整理自己的知识资产,并以一种对开发者最友好、最便捷的方式分享出去。我希望它不仅仅是一个下载站点,更可以成为一个高质量的、经过实战检验的“代码片段集市”和“工具箱”。路漫漫其修远兮。在代码世界的探索中,每个人都是学生,也是老师。我在这里抛砖引玉,期待这些代码模块能真正帮助到在具体问题上寻找解决方案的你。也欢迎你常来看看,这些项目模块,会和我一样,在技术的道路上不断生长。 扫码体验:

2026-03-26 · 1 min · Eagle

在51单片机实现 Modbus RTU 协议过程中的踩坑和思考

在上篇原理文章介绍后,我开始实现代码,硬件使用现成的51开发板。原本以为只是简单的串口收发,结果却在 Modbus Poll 软件中疯狂循环 Checksum Error 和 Timeout。 下面是我在普中科技 51 开发板上,使用STC89C52芯片接入工业标准的 Modbus RTU 协议,从最基础的串口打印到实现动态温度采集,并成功通过 Modbus 协议回传的完整过程。 环境准备 硬件: 普中 51-A2 开发板 (STC89C52RC)、DS18B20 温度传感器、USB 转串口线。 软件: Keil uVision5、STC-ISP、Modbus Poll (主机仿真器)、SSCOM 串口调试助手。 核心挑战:为什么 Modbus Poll 总是报 Checksum Error? 这是我耗时最长的地方,也是我头疼的地方。如果你也遇到数据看着对但校验不过,请检查以下三点: 查表法的陷阱 因为51单片机的性能问题,我使用了查表法,CRC表中的数据是采摘网上教程的。在解决了之后,发现是自己设置的表中的数值不对。要知道Modbus 的 CRC16 校验非常严苛。为了方便以后的开发者,我把正确的CRC表和获取函数都贴出来。 // 高位表 unsigned char code aucCRCHi[] = { 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 }; // 低位表 unsigned char code aucCRCLo[] = { 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 }; // 获取CRC unsigned int GetCRC16(unsigned char *pData, unsigned char len) { unsigned char uIndex; unsigned char crch = 0xFF; unsigned char crcl = 0xFF; while (len--) { uIndex = crcl ^ *pData++; crcl = crch ^ aucCRCHi[uIndex]; crch = aucCRCLo[uIndex]; } return (unsigned int)(crch << 8 | crcl); } 字节序与 Quantity 的对齐 这里也是一个坑,自己概念不清,在设置quantity时设置错误,错把他们1比1进行换算。还有Modbus 规定 低字节在前,高字节在后。 ...

2026-03-18 · 5 min · Eagle

记一次因直接使用unwrap导致Rust上传工具崩溃问题

最近,更新了上传文章工具 rust 版本,这个版本使用的库为 nwg,即 native-windows-gui 库,该库支持老的 Windows GUI。 昨天,当我准备在另一台电脑上录制视频时,发现运行不起来,GUI 界面闪现一下,就退出了。以为版本太老,我先升级了 Rust 版本到最新版本(😭,不应该的原因,固定版本反而更可靠稳定一些)。 rustup update 升级完成后,cargo clean ,再 cargo run 重新运行。 郁闷,还是报错。 将 main.rs 文件中该行注释,#![windows_subsystem = "windows"],可以在命令行中查看报错信息,提示是找不到文件。我想了下,只有图片是文件,然后我将图片的路径修改,发现修改之后,编辑器还提示错误,找不到文件。重新改回去后,编辑器提示错误消失。说明不是这里的错误。 我真的晕了。从网上查找问题原因,没有找到此类问题的解决方法,郁闷。在查找时,发现了另一个 Windows 官方支持的 rust 库,就叫 windows,打算有时间了用这个库重写一下。 今天,我又查看了下另一台电脑上的这个项目,发现可以正常运行,将 rust 升级到最新版本后,还是可以运行。在看到配置文件后,我才恍然大悟,原来是查找的是这个配置文件。我赶紧扒拉代码确认,发现确实是这个问题。 let conf = Ini::load_from_file("conf.ini").unwrap(); 上面是使用文件的地方,找不到文件 panic。我这里优化了一下,让错误提醒的更明显一些。这样我就瞬间能知道问题原因了。 优化后的代码如下: let conf = Ini::load_from_file("conf.ini").expect("please config conf.ini file"); 有些问题其实很简单,但由于时间长了,还是会一头懵,如果报错信息提示完善一点,会很快定位到问题。

2024-01-01 · 1 min · Eagle

使用Rust基于 Native-Windows-GUI 构建的轻量小程序码生成工具

早于官方后台功能的桌面实践:探索使用 Rust + native-windows-gui 打造极致轻量的小程序工具。 在沉浸于 Wails 3 开发 Mole 客户端的过程中,我不禁想起了我在 2023 年 11 月完成的一个小项目——mp-qrcode-gen。 虽然现在微信小程序后台已经集成了生成小程序码的功能,但在 2023 年,开发者想要快速、批量或者自定义参数生成小程序码,往往需要自己写脚本。于是,我用 Rust 编写了这款极致轻量的桌面工具。 为什么在 2023 年做这个工具? 当时,自己需要根据页面去生成一些小程序码,线下张贴使用。每次使用服务端修改代码感觉非常不便利。于是做了一个这样的生成小程序码的工具。 当完成后,我还发了视频号进行了推广,发现很多人还下载使用了。其实运营人员和开发者在准备线下物料(如海报)时,通常会面临一个痛点: 接口调用门槛:微信官方提供的是 API 接口,非技术人员无法直接使用。 参数复杂:小程序码分为“受限”和“不受限”两种,参数限制各异,手动拼凑 URL 极易出错。 批量需求:线下场景往往需要针对不同页面、不同场景值生成大量图片,网页端操作效率低下。 技术选型:Rust + native-windows-gui 在那个时期,我并没有选择 Electron 这种庞然大物,而是选择了Rust,保证了极高的运行效率和内存安全。和native-windows-gui (NWG),这是一个非常纯粹的 Windows 原生 GUI 库。它不包含浏览器内核,直接调用 Windows API,生成的 .exe 文件体积非常小。选择Rust还有一个原因就是我在学习它,需要一个项目练手。 核心功能拆解 做的工具界面非常简单直观,配置完 AppID 和 AppSecret 后,就可以使用它生成小程序码了。点击生成按钮后,工具会直接调用微信接口并处理返回的二进制流,将其保存为本地图片文件。无需打开浏览器,所见即所得。 根据微信官方的接口,我实现了两种模式,分别为受限模式和不受限模式。 1. 数量受限模式 (API A/B) 特点:支持输入完整的、带有超长 URL 参数的页面路径。 场景:适用于对码量要求不高(总计 10 万个以内),但需要精准携带复杂参数的场景。 2. 数量不受限模式 (API C) 特点:页面路径较短,但支持独立的 Scene(场景值)字段。 场景:这是线下物料最常用的模式,可以无限次生成,通过场景值来区分不同的线下投放点。 开源和思考 虽然几个月后,微信官方后台也逐步完善了类似的功能,但 mp-qrcode-gen 的意义在于:在需求未被完全满足的真空期,个人开发者可以用技术手段快速提供解决方案。 ...

2023-11-02 · 1 min · Eagle

构建高可靠 APK 上传工具,彻底终结网络超时与手误引发的生产事故

记录一个未公开的 Rust 实战小工具,分享如何通过技术手段解决网络超时与手误操作带来的生产事故。 在自学 Rust 的那段日子里,我除了开发开源项目,还为日常工作量身定制了一些不公开的内部工具。其中最令我印象深刻的,是一个看似简单的 APK 文件上传小工具。 虽然它只是一个 Rust + native-windows-gui (NWG) 编写的单一界面应用,但它解决的两个核心生产问题,至今仍对我有着深远的影响。 工具画像:极简与高效 这个工具的界面极其克制: 交互区:一个醒目的文件拖拽控件,文案提示“请把 APK 文件拖拽到这里”。 表单区:版本号、Version Code、更新描述、是否强制升级(开关)。 执行区:一个大大的“上传”按钮。 它的核心逻辑非常纯粹:校验必填项 -> 调用内部上传 API -> 上传文件 -> 返回成功或失败的对话框。 故事一:消失的 60 秒与隐藏的超时限制 工具上线初期,一切运行平稳。由于办公室带宽充足,APK 的上传时间通常维持在 40 秒左右。 事故发生: APK 在集成广告后,经常会发生失败的情况,客户端弹出“超时”的报错。 排查过程: 我首先检查了后端服务,发现后端接口完全没有超时限制,服务器日志显示连接是被客户端主动断开的。 多次重复上传文件使用有线和无线对比,经排查是文件变大,由于公司无线网络波动,上传速度时好时坏,就会偶尔出现失败的情况。对比排查,发现失败的情况上传时间都超过了 60 秒。 回到 Rust 代码中,我发现当时为了追求简洁,直接使用了 HTTP 客户端的默认配置。而这个默认配置的 Timeout 为 60 秒。 解决方案: 在那个瞬间我意识到,工具不能只考虑“理想状态”。我手动将本地超时时间放宽至 5 分钟,并增加了状态提示。上传时不会再出现超时的情况了。 教训:永远不要依赖默认的超时设置,尤其是在处理文件上传这类长耗时操作时,本地的容错范围必须根据业务场景精细化配置。 故事二:包名校验——把人为失误挡在门外 这是这个工具最有价值的一次功能进化。 事故发生: 有一次,同事不小心将应用 A 的打包产物当作应用 B 上传到了官网,导致用户下载后发现软件张冠李戴。这属于严重的生产事故。 排查过程: 这个客户端软件调用内部API后,会自动将上传的文件名修改为固定的软件下载文件名,显示在网站上。经测试,如果将应用A的打包产物使用应用B上传,在官网就会出现相同的情况。解决这个方法也非常简单,就是上传的时候小心细致一点,应用A使用A客户端软件,应用B使用B客户端软件。但是人总会犯错,尤其是在面对多个长相相似的 .apk 文件时。通过肉眼观察文件名来区分应用,是极其不可靠的。 ...

2023-01-02 · 1 min · Eagle