实战笔记:为了不再发错下载链接,我给工具箱加了“扫码鉴定”

小工具可以解决大问题。在软件发布流程中,最尴尬的事情莫过于:宣传图已经发出去了,用户扫码后却发现链接打不开,或者下载了一个旧版本的安装包。 我就亲身经历过这么一次“翻车”事故。 事故现场:一个 URL 引起的麻烦 有一次,我在更新软件下载页面后,由于换了一个新的 URL 地址,在生成二维码阶段没有进行严格校验,就直接把二维码发布了出去。 结果当用户兴冲冲地扫码下载时,发现软件运行异常。我对比了半天才发现,生成二维码时填写的 URL 链接版本号写错了一个字符,导致用户下载到了一个有问题的版本,这是偶然中的极端。 虽然这只是一个低级错误,但它让我意识到:在二维码挂载后、正式发布前,必须有一个极简的“内容鉴定”环节。 痛点:如何快速、无痛地校验? 通常我们校验二维码,习惯性地直接拿手机扫一下。但如果二维码指向的是一个大容量的 APP 下载包或复杂的跳转链接,扫码后手机会自动触发下载或进入复杂的网页路径。 我其实并不想真的下载并安装测试(那是后续的 QA 环节),我只是想一眼看到二维码里到底写了什么字符,确定那个 URL 是不是我想要的那一个。 实现:借力小程序的原生能力 既然发现了痛点,解决起来就非常顺手了。得益于微信生态对扫码的深度支持,我在“豆子工具”里实现了一个“二维码识别”功能: 核心逻辑:直接调用小程序的 wx.scanCode 接口。 功能表现:不仅支持扫描实物二维码,还支持从手机相册里直接读取生成的二维码图片,还支持小程序码扫描,支持查看页面路径和scene值。 结果反馈:系统不会触发自动跳转,而是将二维码包含的原始文本、URL、一维码内容直接以纯文本的形式展示在屏幕上。 价值:多看一眼,少出一次错 现在,每当我生成一个新的二维码,无论是用于软件下载、文档分享还是活动跳转,我都会习惯性地用自己的工具“扫一下”: 确认 URL 参数是否完整。 确认是否有肉眼难以察觉的拼写错误。 确认二维码的类型(一维码还是二维码)是否符合预期。 技术实现 使用小程序纯原生实现,仅仅做一个页面展示即可。 scanQRCode: function () { const that = this; wx.showLoading({ title: '识别中...', mask: true, }) wx.scanCode({ onlyFromCamera: false, scanType: ['qrCode', 'barCode', 'datamatrix', 'pdf417', 'wxCode'], success: (res) => { wx.hideLoading() if(res.scanType=='WX_CODE'){ that.setData({ scanResult: res.path }); }else{ that.setData({ scanResult: res.result }); } }, fail: (err) => { wx.hideLoading() console.error(err); } }); }, 需要注意的是,小程序码和其它码的结果字段有点不同,所以需要区分,以便取到正确的值。 ...

2018-01-09 · 1 min · Eagle

实战笔记:从一个按钮到全能生成器,随机数功能的“进化论”

这个工具是我的图腾,还记得我之前提到的吗?2018 年,“豆子工具”的初版只有一个功能:点一下按钮,生成一个随机数。 那时候的代码逻辑极其简单,甚至算不上一个“工具”。但随着这几年自己在开发、运维过程中不断遇到“需要一个复杂密码”、“需要一个 16 位纯金钥”或者“需要一个全大写 ID”等实际场景,我发现简单的随机数其实并不简单。 痛点:单一随机数的局限 早期的随机数功能非常死板,生成的格式往往不是我想要的。每次生成后,我可能还需要手动去改长度、改大小写,甚至手动补上特殊字符。 作为一个开发者,如果一个工具不能让我“一键到位”,那就是不合格的。 进化:高度自定义的“融合模式” 于是,我把这些年所有关于“随机”的需求全部揉碎、重组,将它升级成了一个全能的生成器。 现在的随机数功能,不再是盲目地随机,而是基于规则的精准生成。我为其设计了一套组合逻辑: 长度自定义:不再局限于几位数字,用户可以根据需求自由输入长度。 字符集自由组合: 纯数字:适用于验证码类场景。 纯字母:适用于临时 ID 或变量命名。 字母+数字:兼顾强度与易读性。 含特殊字符:专门为高强度随机密码设计。 结果格式化: 支持结果一键转大写或转小写。这在配置某些特定系统的 API Key 时非常有用,省去了手动切换输入法的麻烦。 我经常使用它生成一些随机数作为密码,密钥。我不用去数长度,或者生成后,我简单调整一下,让它有一点意义,我再去使用。 它使用小程序本地实现,下面是一段核心代码,描述了如何去实现随机数: // 生成随机字符串 genRndStr(rtype) { const that = this; if (utils.isEmpty(that.data.input1)) { wx.showToast({ title: '内容为空', }) return } let result = ''; 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 }) }, 思考:小功能里的“产品观” 虽然随机数在技术实现上只是简单的字符数组随机采样,但从产品角度看,它体现了一种**“减少用户操作”**的原则。 ...

2018-01-07 · 1 min · Eagle

实战笔记:知识真的就是金钱,聊聊我的局域网调试工具

在“豆子工具”的所有功能中,局域网调试工具(TCP/UDP/WebSocket) 并不是受众最广的,但它却是我最引以为傲的一个。 因为它让我真切地体会到了一句话:知识就是力量,知识就是金钱。 缘起:把调试器揣进口袋 做嵌入式开发或者网络协议调试的人都知道,以往测试局域网通信,必须背着电脑,接上串口线或网线,蹲在机柜旁守着。我当时就在想:既然微信小程序已经开放了网络通信的能力,为什么我不能做一个随身携带的调试器呢? 于是,我深度调用了小程序的 wx.createTCPSocket、wx.createUDPSocket 和 WebSocket API,在“豆子工具”里构建了一个完整的网络测试模块。它支持: 十六进制(Hex)与 ASCII 码切换 实时数据日志展示 局域网内稳定的数据收发 变现:从技术分享到“第一桶金” 工具做成后,我并没有藏着掖着,而是把实现原理和核心逻辑整理成文,发布到了微信开发者社区。 没过多久,一位用户通过那篇文章联系到了我。他在工业自动化领域遇到了一个难题:需要一套能够定制化采集设备数据的微信小程序工具,而协议正是基于 Modbus/TCP。 由于我的“豆子工具”已经打好了坚实的底层基础,我对原有的 TCP 调试模块进行了针对性的修改,迅速就适配出了满足他实际业务场景的采集方案。 虽然这笔订单带来的“第一桶金”数额并不算惊人,但它对我的意义非凡。它验证了一个逻辑:当你把一个细分领域的工具做到极致,并愿意分享出去时,价值自然会找上门来。 技术实现 这个工具是纯小程序实现,它调用微信小程序的网络API。 doConnServer() { const that = this; wx.getNetworkType({ success(res) { const networkType = res.networkType if (networkType !== 'wifi') { wx.showToast({ title: '请打开WIFI', }) 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: "关闭成功" }) wx.showToast({ title: '关闭成功', }) return } else { let ip = that.data.ip; let port = that.data.port; if (!utils.isValidIP(ip)) { wx.showToast({ title: '无效IP地址', }) return } if (!utils.isPrivateIP(ip)) { wx.showToast({ title: '请使用私有IP', }) return } if (utils.containInvalidPort(port)) { wx.showToast({ title: '非法端口', }) 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, }) } }, 这是部分核心TCP代码,它是TCP创建连接的核心代码。完整代码请从下面地址中的豆子碎片小程序内下载:https://91demo.top/tools/ ...

2018-01-06 · 1 min · Eagle

实战笔记:为了省下服务器流量费,我给小程序加上了 Webp 转换

在“流量贵如油”的今天,作为一名自己买服务器、撸代码的站长,如何给服务器“减负”是每天都要思考的必修课。 痛点:博客网站的流量杀手 我有一个运行多年的个人博客。在分析服务器日志时发现,最大的流量开销并不是文字,而是文章里那些高清的图片。 虽然 JPG 和 PNG 已经很普及,但随着屏幕分辨率越来越高,原图动辄几 MB,对于按带宽付费或者流量计费的服务器来说,这都是白花花的银子。 方案:Webp 格式的“降维打击” 现在主流浏览器(Chrome, Safari, Edge 等)已经全面支持 Webp 格式。相比于传统的 JPG 和 PNG,Webp 可以在保证肉眼看不出画质损失的前提下,将文件体积压缩 30% 到 70%。这是一个非常惊人的数据。这意味着原本 100GB 的图床流量,换成 Webp 后可能只需要 30GB。 为了方便处理博客素材,我决定把这个需求也集成到“豆子工具”里。 实现:基于 Google 官方工具链 图片转换的实现原理与我之前的“音频转换”方案异曲同工,主打一个稳定与高效: 核心引擎:在服务端安装了 Google 官方出品的 webp 命令行工具链(cwebp)。 后端调度:依然由 Go 语言担任“指挥官”,接收小程序上传的 PNG/JPG 原图,调用外部 cwebp 进程进行压缩。 参数优化:在后端我预设了平衡性最好的质量参数,确保图片在压缩后的清晰度依然能够满足博客展示的需求。 闭环应用:现在我写每一篇博客前,都会先用小程序把配图过一遍,转成 Webp 后再上传。 下面是核心代码片段的注解,这是小程序端: <page-meta root-font-size="system" /> <view class="page"> <view class="page__hd"> <view class="page__title">图片格式转换</view> <view class="page__desc">可将图片转换为WebP格式,用于Web项目。</view> </view> <view class="page__bd"> <view class="weui-cells weui-cells_radio"> <view class="weui-cell weui-cell_uploader"> <view class="weui-cell__bd"> <view class="weui-uploader"> <view class="weui-uploader__hd"> <view aria-role="option" class="weui-uploader__overview"> <view class="weui-uploader__title">图片信息</view> </view> <view class="weui-uploader__tips"> 请选取5M内的图片,目前仅支持一张 </view> </view> <view class="weui-uploader__bd"> <view class="weui-uploader__files" id="uploaderFiles"> <block wx:for="{{files}}" wx:key="*this"> <view class="weui-uploader__file" bindtap="previewImage" id="{{item}}"> <image class="weui-uploader__img" src="{{item}}" mode="aspectFill" /> </view> </block> </view> <view class="weui-uploader__input-box"> <view aria-role="button" aria-label="选择图片" class="weui-uploader__input" bindtap="chooseImage"></view> </view> </view> </view> </view> </view> <view class="weui-cell"> <view wx:if="{{headImageExist}}"> <image style="width: 200px; height: 200px; background-color: #eeeeee;" mode="aspectFit" src="{{headImage}}" bindtap="previewMakeImage"></image> </view> <view wx:else><text>转换完成的图片会展示在这里</text></view> </view> <view class="weui-cell"> <view class="button-sp-area"> <view class="weui-btn weui-btn_primary weui-wa-hotarea" aria-role="button" bindtap="imageConvert">开始转换</view> <view class="weui-btn weui-btn_warn weui-wa-hotarea" aria-role="button" bindtap="saveToPhotosAlbum">保存到相册</view> </view> </view> </view> </view> </view> 上面是界面骨架,现在说下它的灵魂。首先通过小程序的上传文件API,wx.uploadfile将图片文件上传到服务器,然后使用Go调用cwebp进行格式转换,转换成功后,下载文件到小程序本地,然后删除服务器端的文件。 ...

2018-01-05 · 2 min · Eagle

实战笔记:我把 FFmpeg 搬进小程序,搞定了音频格式转换

在“豆子工具”众多的功能里,音频转换(m4a 转 mp3) 是我使用频率最高、也最具有“个人救赎”色彩的一个。 起因:被软件更新“背刺”后的郁闷 这个功能的由来非常接地气:有一段时间,我需要频繁地将苹果手机录音产生的 m4a 格式文件转换成 mp3,因为当时某个必须使用的业务软件只认 mp3。 那时候我找遍了各种转换工具。最后发现某款主流音乐软件自带的转换功能挺好用。然而,好景不长,在一次软件自动更新后,这个功能竟然被砍掉了。我去搜老版本安装包,却发现根本找不到安全的下载路径。 那种“被绑架”的无奈感,相信每个工具控都深有体会。 进阶:大名鼎鼎的 ffmpeg 郁闷之后,我转向了技术人的终极方案——ffmpeg。 命令行虽然硬核,但确实强大到无以复加。一条简单的指令就能解决所有问题: ffmpeg -i input.m4a output.mp3 用了很长一段时间的命令行后,新的问题又来了,一次在外面,突然用户需要转换音频,我总不能随时随地都带着电脑吧?于是,我动了把 ffmpeg 搬进“豆子工具”的心思。 实现:极简架构下的“随时随地” 在小程序里实现这个功能,原理其实并不复杂,核心在于后端调度: 前端上传:小程序端选择 m4a 文件,上传至服务器。 后端处理:后端使用 Go 语言接收文件,通过 exec 模块调用服务器系统环境中的 ffmpeg 进程进行转换。 实时试听:为了保证体验,我在小程序里集成了一个音频播放器。转换前可以听一下是否选对了文件,转换后也可以即时试听确认效果。 即用即删:转换生成的文件在用户下载后会立即从服务器删除,既保护了隐私,也完全不占用宝贵的服务器存储空间。 下面是核心代码片段的注解,这是小程序端: <page-meta root-font-size="system" /> <view class="page"> <view class="weui-form"> <view class="weui-form__text-area"> <h2 class="weui-form__title">音频格式转MP3</h2> <view class="weui-form__desc">因微信限制,保存文件即为分享文件,可通过微信文件传输助手下载文件。目前仅支持以下格式(M4A、WAV、AMR)文件转换为MP3文件。</view> </view> <view class="weui-form__control-area"> <view class="weui-cells__group weui-cells__group_form"> <view class="weui-cells weui-cells_radio"> <view class="weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap"> <view class="weui-cell__hd"><label class="weui-label">文件</label></view> <view class="weui-cell__bd"> <input class="weui-cell__control weui-cell__control_flex weui-input" type="text" placeholder="请选取5M内的音频" placeholder-class="weui-input__placeholder" value="{{filename}}" /> <view aria-role="button" class="weui-cell__control weui-btn weui-btn_default weui-vcode-btn" bindtap="chooseMedia">选择</view> </view> </view> <view class="weui-cell"> <view wx:if="{{audioExist}}"> <audio name="{{filename}}" src="{{filepath}}" id="myAudio" controls></audio> </view> <view wx:else><text>选择音频文件后可以在这里试听</text></view> </view> <view class="weui-cell"> <view wx:if="{{makeAudioExist}}"> <audio name="{{makeAudioName}}" src="{{makeAudioPath}}" id="myAudio" controls></audio> </view> <view wx:else><text>转换完成的文件会展示在这里</text></view> </view> </view> </view> </view> <view class="weui-form__opr-area"> <view class="button-sp-area"> <view class="weui-btn weui-btn_primary weui-wa-hotarea" aria-role="button" bindtap="audioConvert">开始转换</view> <view class="weui-btn weui-btn_warn weui-wa-hotarea" aria-role="button" bindtap="saveFile">保存文件</view> </view> </view> </view> </view> 上面是界面骨架,现在说下它的灵魂。首先通过小程序的上传文件API,wx.uploadfile将音频文件上传到服务器,然后使用Go调用ffmpeg进行格式转换,转换成功后,下载文件到小程序本地,然后删除服务器端的文件。 ...

2018-01-04 · 2 min · Eagle