[{"content":"在开发“模块集市”时，用户通过观看激励广告获取源码下载地址。虽然对接广告并不难，但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调（Server-to-Server），奖励的发放完全依赖前端触发，这给了一些“羊毛党”可乘之机。\n在接口防护的过程中，我经历了四个阶段：\n第一阶段：身份鉴权（解决匿名刷取） 我的获取奖励接口是getAdReward。调用它就可以获取奖励，后台进行发放。最初，由于经验不足，我没有对它做任何防护。这样任何人任何客户端都可以进行访问。了解小程序微信账号体系后，我添加了鉴权，只有携带Token的用户可以访问。这样可以保证只有能够调用wx.login的用户才可以访问。\n这解决了一部分问题，但是又有了新的问题，它防不住“真实用户”，只要用户登录后拦截并解析出接口地址和 Token，就可以跳过广告手动调用了getAdReward接口，没有观看广告就获取了奖励。\n第二阶段：共享密钥加密（解决接口暴露） 为了防止直接调用接口，我引入了对称加密。前端与后端约定一个硬编码的共享密钥，在小程序端和服务端同时使用这个共享密钥加密随机数和时间戳进行校验，匹配则发放奖励。这样可以防止fidder等工具直接拦截调用接口。\n它正常运行了一段时间，我发现又有了新的情况。用户可能通过反编译或者其它方式获取到了共享密钥。这在一段时间内对我造成困扰，除了定期更新共享密钥，我没有好的办法，但更新共享密钥需要升级发布小程序。直到当我了解到小程序提供加密网络通道时，我发现这是一个好的解决方案。\n第三阶段：微信加密网络通道（解决密钥安全） 首先，我们来了解一下加密网络通道，它是微信为小程序提供的一套加密KEY机制，它可以同时在微信小程序端和服务器端获取相同的密钥。由于密钥由微信官方通道生成且动态更新，反编译源码也拿不到密钥。除非攻击者能拦截微信的网络通道，否则无法伪造解密过程。同时，配合时间戳校验，有效防御了初级的重放攻击。这对于我来说，是非常好的利好消息，我不需要在源码中硬编码共享密钥了。\n我们可以参考获取短信验证码，短信验证码之所以安全，是因为应用和短信验证码是两个不相同的通道。同样的这个加密KEY，在小程序获取和服务端获取都是通过微信的网络通道，和自己的应用通道也不是一个。对于我的小程序稍加改造就可以了。在获取奖励的接口中，首先使用wx.getUserEncryptKey获取微信的加密KEY，然后使用自定义的AES加密方法，将要传递的时间戳和Nonce等参数加密后，将加密数据提交给服务器就可以了。当数据到达服务器，服务器同样调用微信的接口获取微信的加密KEY，然后进行AES解密。这样就可以防止获取共享密钥的弊端。\n好了，我们来看看具体如何使用？为了避免小程序与开发者后台通信时数据被截取和篡改，微信侧维护了一个用户维度的可靠key，用于小程序和后台通信时进行加密和签名。开发者可以分别通过小程序前端和微信后台提供的接口，获取用户的加密 key。\n1，小程序端获取：\nconst somedata = \u0026#39;xxxxx\u0026#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) } }) } }) 其中，someAESEncryptMethod 和 someAESDEcryptMethod 分别为加解密函数，由开发者自行引入加解密库来实现，基础库暂时不提供加解密能力。\n2，服务端获取：\n在开发者服务端，可以调用getUserEncryptKey后台接口获取用户最近三次的key。在获取key的同时，接口会携带version信息，开发者可以比较version版本来选择使用对应的key对数据进行加解密。\ncurl -X POST \u0026#34;https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=ACCESS_TOKEN\u0026amp;openid=OPENID\u0026amp;signature=SIGNATURE\u0026amp;sig_method=hmac_sha256\u0026#34; 其中，openid是用户的openid，signature用sessionkey对空字符串签名得到的结果。即 signature = hmac_sha256(session_key, \u0026ldquo;\u0026quot;)，sig_method为签名方法，固定为hmac_sha256。\n这个是核心，获取激励广告奖励基于它再添加一些参数进行加解密。\n第四阶段：后端“挑战模式”（解决重放与逻辑伪造） 我在参数中添加的Nonce，它是一个噪音参数，它是小程序端本地生成的，还有时间戳也是本地生成的，增加它们是为了增加解密难度。在实际运行中，我发现硬核玩家会通过修改本地时间戳和 Nonce（随机数）来绕过检测，或者使用相同的参数进行重放攻击，或者不够15s（广告正常播放最短时间）调用多次接口。\n为了解决这个问题，我将逻辑升级为“挑战模式”：首先，小程序端在广告开始前进行一次预请求，前端必须先调用 getNonce 接口获取Nonce然后使用这个Nonce进行加密，getNonce是一个新增接口，用来生成Nonce，并记录生成时间和关联openid，服务端存入缓存（如 Redis），完成服务端打标。当前端再次调用getAdReward接口时，如果差值小于广告常规时长（如 15s），则判定为非正常观看，直接拒绝。\n通过这种“加密网络通道 + 后端挑战模式”的组合拳，我成功将防刷成本提高到了用户收益之上。虽然世上没有绝对的安全，但当破解成本远超收益价值时，系统就是安全的。\n这就是我的广告防刷经历，后续我会将这套小程序加密网络通道模块的源码整理上线到“模块集市”，希望能帮到有同样困扰的开发者。\n","permalink":"https://blog.91demo.top/web/crypto_link.html","summary":"\u003cp\u003e在开发“模块集市”时，用户通过观看激励广告获取源码下载地址。虽然对接广告并不难，但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调（Server-to-Server），奖励的发放完全依赖前端触发，这给了一些“羊毛党”可乘之机。\u003c/p\u003e\n\u003cp\u003e在接口防护的过程中，我经历了四个阶段：\u003c/p\u003e\n\u003ch2 id=\"第一阶段身份鉴权解决匿名刷取\"\u003e第一阶段：身份鉴权（解决匿名刷取）\u003c/h2\u003e\n\u003cp\u003e我的获取奖励接口是\u003ccode\u003egetAdReward\u003c/code\u003e。调用它就可以获取奖励，后台进行发放。最初，由于经验不足，我没有对它做任何防护。这样任何人任何客户端都可以进行访问。了解小程序微信账号体系后，我添加了鉴权，只有携带Token的用户可以访问。这样可以保证只有能够调用wx.login的用户才可以访问。\u003c/p\u003e\n\u003cp\u003e这解决了一部分问题，但是又有了新的问题，它防不住“真实用户”，只要用户登录后拦截并解析出接口地址和 Token，就可以跳过广告手动调用了\u003ccode\u003egetAdReward\u003c/code\u003e接口，没有观看广告就获取了奖励。\u003c/p\u003e\n\u003ch2 id=\"第二阶段共享密钥加密解决接口暴露\"\u003e第二阶段：共享密钥加密（解决接口暴露）\u003c/h2\u003e\n\u003cp\u003e为了防止直接调用接口，我引入了对称加密。前端与后端约定一个硬编码的共享密钥，在小程序端和服务端同时使用这个共享密钥加密随机数和时间戳进行校验，匹配则发放奖励。这样可以防止fidder等工具直接拦截调用接口。\u003c/p\u003e\n\u003cp\u003e它正常运行了一段时间，我发现又有了新的情况。用户可能通过反编译或者其它方式获取到了共享密钥。这在一段时间内对我造成困扰，除了定期更新共享密钥，我没有好的办法，但更新共享密钥需要升级发布小程序。直到当我了解到小程序提供加密网络通道时，我发现这是一个好的解决方案。\u003c/p\u003e\n\u003ch2 id=\"第三阶段微信加密网络通道解决密钥安全\"\u003e第三阶段：微信加密网络通道（解决密钥安全）\u003c/h2\u003e\n\u003cp\u003e首先，我们来了解一下加密网络通道，它是微信为小程序提供的一套加密KEY机制，它可以同时在微信小程序端和服务器端获取相同的密钥。由于密钥由微信官方通道生成且动态更新，反编译源码也拿不到密钥。除非攻击者能拦截微信的网络通道，否则无法伪造解密过程。同时，配合时间戳校验，有效防御了初级的重放攻击。这对于我来说，是非常好的利好消息，我不需要在源码中硬编码共享密钥了。\u003c/p\u003e\n\u003cp\u003e我们可以参考获取短信验证码，短信验证码之所以安全，是因为应用和短信验证码是两个不相同的通道。同样的这个加密KEY，在小程序获取和服务端获取都是通过微信的网络通道，和自己的应用通道也不是一个。对于我的小程序稍加改造就可以了。在获取奖励的接口中，首先使用\u003ccode\u003ewx.getUserEncryptKey\u003c/code\u003e获取微信的加密KEY，然后使用自定义的AES加密方法，将要传递的时间戳和Nonce等参数加密后，将加密数据提交给服务器就可以了。当数据到达服务器，服务器同样调用微信的接口获取微信的加密KEY，然后进行AES解密。这样就可以防止获取共享密钥的弊端。\u003c/p\u003e\n\u003cp\u003e好了，我们来看看具体如何使用？为了避免小程序与开发者后台通信时数据被截取和篡改，微信侧维护了一个用户维度的可靠key，用于小程序和后台通信时进行加密和签名。开发者可以分别通过小程序前端和微信后台提供的接口，获取用户的加密 key。\u003c/p\u003e\n\u003cp\u003e1，小程序端获取：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003econst somedata = \u0026#39;xxxxx\u0026#39;\nconst userCryptoManager = wx.getUserCryptoManager()\nuserCryptoManager.getLatestUserKey({\n    success({encryptKey, iv, version, expireTime}) {\n        const encryptedData = someAESEncryptMethod(encryptKey, iv, somedata)\n        wx.request({\n           data: encryptedData,\n           success(res) {\n                const decryptedData = someAESDEcryptMethod(encryptKey, iv, res.data)\n                console.log(decryptedData)\n           }\n        })\n    }\n})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e其中，someAESEncryptMethod 和 someAESDEcryptMethod 分别为加解密函数，由开发者自行引入加解密库来实现，基础库暂时不提供加解密能力。\u003c/p\u003e\n\u003cp\u003e2，服务端获取：\u003cbr\u003e\n在开发者服务端，可以调用getUserEncryptKey后台接口获取用户最近三次的key。在获取key的同时，接口会携带version信息，开发者可以比较version版本来选择使用对应的key对数据进行加解密。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -X POST \u0026#34;https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=ACCESS_TOKEN\u0026amp;openid=OPENID\u0026amp;signature=SIGNATURE\u0026amp;sig_method=hmac_sha256\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e其中，openid是用户的openid，signature用sessionkey对空字符串签名得到的结果。即 signature = hmac_sha256(session_key, \u0026ldquo;\u0026quot;)，sig_method为签名方法，固定为hmac_sha256。\u003c/p\u003e\n\u003cp\u003e这个是核心，获取激励广告奖励基于它再添加一些参数进行加解密。\u003c/p\u003e\n\u003ch2 id=\"第四阶段后端挑战模式解决重放与逻辑伪造\"\u003e第四阶段：后端“挑战模式”（解决重放与逻辑伪造）\u003c/h2\u003e\n\u003cp\u003e我在参数中添加的Nonce，它是一个噪音参数，它是小程序端本地生成的，还有时间戳也是本地生成的，增加它们是为了增加解密难度。在实际运行中，我发现硬核玩家会通过修改本地时间戳和 Nonce（随机数）来绕过检测，或者使用相同的参数进行重放攻击，或者不够15s（广告正常播放最短时间）调用多次接口。\u003c/p\u003e\n\u003cp\u003e为了解决这个问题，我将逻辑升级为“挑战模式”：首先，小程序端在广告开始前进行一次预请求，前端必须先调用 getNonce 接口获取Nonce然后使用这个Nonce进行加密，\u003ccode\u003egetNonce\u003c/code\u003e是一个新增接口，用来生成Nonce，并记录生成时间和关联openid，服务端存入缓存（如 Redis），完成服务端打标。当前端再次调用\u003ccode\u003egetAdReward\u003c/code\u003e接口时，如果差值小于广告常规时长（如 15s），则判定为非正常观看，直接拒绝。\u003c/p\u003e","title":"小程序激励广告防刷演进史：从前端校验到“挑战模式”"},{"content":"当我开发完51单片机温度采集并转换Modbus RTU后，我一度想分享出去，但是我又不想草率的去分享。我一直在思考，如何能让这些代码发挥更大的价值，而不仅仅是躺在个人仓库里。作为一名摸爬滚打多年的开发者，我手头积累了大量来自实践的代码、模块和解决方案。它们散落在硬盘的各个角落，像一颗颗未经打磨的珍珠。 于是，我萌生了做一个“模块源码集市”小程序的想法——一个可以直接浏览、查阅、并一键下载优质源码的平台。\n经过最近一段时间的集中开发，这个小程序终于和大家见面了。今天，我想和大家分享其背后的设计与思考，希望能为有相似想法的朋友提供一些参考。这个小程序的核心功能简洁而实用：\n首页全景浏览：所有上架的代码模块都在首页清晰展示，你可以快速了解其名称、简介和分类，对全站资源一目了然。 沉浸式详情阅读：点击任一模块，即可进入详情页。这里我使用了Markdown渲染引擎，来展示模块的详细介绍、使用说明、技术要点等。Markdown的优雅排版能让技术文档的阅读体验大幅提升。 一键获取源码：在详情页，点击获取模块源码链接按钮，即可轻松获取模块的完整源码压缩包。 为了让这个简单的流程稳定、安全、可扩展，我在几个关键环节做了一些设计和取舍：\n动态资源与安全通道源码文件并非直接打包在小程序内，而是存储在后台的MinIO对象存储中。当用户点击下载时，服务端会动态生成一个具有时效性的访问链接返回给小程序。这样做的好处是资源可以随时更新。同时，为了维持后端服务器持续运行，我在列表及详情中接入了少量广告。为了保护下载接口不被恶意刷取，我集成了激励式视频广告。用户观看一则简短的广告，即可解锁下载权限。这不仅是简单的广告接入，更重要的是，我为此构建了一套防刷机制，我使用了微信的加密网络通道，对关键数据进行加密传输和验证，极大地增加了自动化盗刷的成本和难度，保护了服务器资源。\n灵活的沟通与通知我深知，一个“活”的产品需要与用户保持沟通。因此，小程序内设置了系统消息中心，我可以在这里向所有用户下发重要的系统公告、模块上新以及更新日志。更重要的是，我为每个模块的上新功能，接入了微信服务通知。当有新的、优秀的代码模块入库时，订阅了的用户会在早上8点后收到一条微信消息提醒，能让你随时跟上“仓库”的更新节奏。\n持续生长的内容生态目前上架的或正在上架的模块，覆盖了我擅长的多个领域：小程序、Go语言后端工具、Rust系统编程实践、Web前端片段、嵌入式软硬件代码，包含音视频、网络、加密等技术要点，形式包括客户端、命令行工具、固件、小程序、网页等。这只是一个开始。我的计划是将其作为一个长期项目，持续将我实践中验证过的、有价值的代码沉淀、整理并开源出来。未来，我还会在项目详情页中，增加关联的公众号技术文章跳转，或是有功能关联的其他小程序跳转，形成一个立体化的技术知识网络。\n坦率地说，这个小程序的开发过程远超我最初的预期。从架构设计到具体实现，尤其是几个关键的技术卡点上，耗费了我大量的精力。\n与广告盗刷的攻防：我的服务器每天都有人在扫描，所以在实现激励广告防刷机制时，我花了大量时间加强防刷机制，其实这个在微信小程序社区中也可以看到很多类似的问题。我这里研究了一种解决方案。就是使用加密网络通道。在对接的过程中，我与各种加密错误、签名错误（如常见的40079错误）作斗争。例如，处理会话密钥时，发现不需要进行Base64编码，而是直接以空字符串的字节形式处理；在URL拼接的细节上，POST与GET方法的误用也会导致失败；而加解密环节更是陷阱重重——加密密钥需要从Base64解码，IV却是一个直接的16位字符串，加密数据则需要从十六进制格式解码……这些细枝末节，任何一个出错都会导致整个流程崩掉，往往需要清空Token重新登录来排查。这个过程让我对网络数据安全有了更深刻的认识。这些错误和经验本想专开一篇文章介绍，但是细想一下，可能一段话就能总结，但是其中的辛酸和感悟无法表达在纸面上。索性后期整理出来开源这个模块，减少其他开发者的一些坎坷。\n存储方案的抉择：在对象存储的选择上，也有一段小插曲。我最熟悉的是Mino，但是在我了解到MinIO版本已归档，便尝试了rustfs等其他方案进行探索。但经过综合对比在可靠性、API友好度以及与现有技术栈的契合度后，最终还是选择了功能完备、文档清晰的MinIO作为存储后端，事实证明这个选择是稳定高效的。\n所幸，在这个艰难的过程中，AI编程助手给予了我巨大的帮助（但是也让我吃了很多苦头，因为它上面的资料都比较旧）。许多代码片段的生成、调试过程中错误信息的解读、API文档的快速查询，都因AI的介入而效率倍增。如果没有它，单靠我自己，恐怕几个月也难见成果，绝无可能在十几天内推动项目成型上线。\n做这个小程序，并非为了追逐热点，这个小程序刚刚上线，还需要时间去细细打磨。开发其初心很简单：整理自己的知识资产，并以一种对开发者最友好、最便捷的方式分享出去。我希望它不仅仅是一个下载站点，更可以成为一个高质量的、经过实战检验的“代码片段集市”和“工具箱”。路漫漫其修远兮。在代码世界的探索中，每个人都是学生，也是老师。我在这里抛砖引玉，期待这些代码模块能真正帮助到在具体问题上寻找解决方案的你。也欢迎你常来看看，这些项目模块，会和我一样，在技术的道路上不断生长。\n扫码体验：\n","permalink":"https://blog.91demo.top/web/carrier.html","summary":"\u003cp\u003e当我开发完51单片机温度采集并转换Modbus RTU后，我一度想分享出去，但是我又不想草率的去分享。我一直在思考，如何能让这些代码发挥更大的价值，而不仅仅是躺在个人仓库里。作为一名摸爬滚打多年的开发者，我手头积累了大量来自实践的代码、模块和解决方案。它们散落在硬盘的各个角落，像一颗颗未经打磨的珍珠。 于是，我萌生了做一个“模块源码集市”小程序的想法——一个可以直接浏览、查阅、并一键下载优质源码的平台。\u003c/p\u003e\n\u003cp\u003e经过最近一段时间的集中开发，这个小程序终于和大家见面了。今天，我想和大家分享其背后的设计与思考，希望能为有相似想法的朋友提供一些参考。这个小程序的核心功能简洁而实用：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e首页全景浏览：所有上架的代码模块都在首页清晰展示，你可以快速了解其名称、简介和分类，对全站资源一目了然。\u003c/li\u003e\n\u003cli\u003e沉浸式详情阅读：点击任一模块，即可进入详情页。这里我使用了Markdown渲染引擎，来展示模块的详细介绍、使用说明、技术要点等。Markdown的优雅排版能让技术文档的阅读体验大幅提升。\u003c/li\u003e\n\u003cli\u003e一键获取源码：在详情页，点击获取模块源码链接按钮，即可轻松获取模块的完整源码压缩包。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e为了让这个简单的流程稳定、安全、可扩展，我在几个关键环节做了一些设计和取舍：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e动态资源与安全通道源码文件并非直接打包在小程序内，而是存储在后台的MinIO对象存储中。当用户点击下载时，服务端会动态生成一个具有时效性的访问链接返回给小程序。这样做的好处是资源可以随时更新。同时，为了维持后端服务器持续运行，我在列表及详情中接入了少量广告。为了保护下载接口不被恶意刷取，我集成了激励式视频广告。用户观看一则简短的广告，即可解锁下载权限。这不仅是简单的广告接入，更重要的是，我为此构建了一套防刷机制，我使用了微信的加密网络通道，对关键数据进行加密传输和验证，极大地增加了自动化盗刷的成本和难度，保护了服务器资源。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e灵活的沟通与通知我深知，一个“活”的产品需要与用户保持沟通。因此，小程序内设置了系统消息中心，我可以在这里向所有用户下发重要的系统公告、模块上新以及更新日志。更重要的是，我为每个模块的上新功能，接入了微信服务通知。当有新的、优秀的代码模块入库时，订阅了的用户会在早上8点后收到一条微信消息提醒，能让你随时跟上“仓库”的更新节奏。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e持续生长的内容生态目前上架的或正在上架的模块，覆盖了我擅长的多个领域：小程序、Go语言后端工具、Rust系统编程实践、Web前端片段、嵌入式软硬件代码，包含音视频、网络、加密等技术要点，形式包括客户端、命令行工具、固件、小程序、网页等。这只是一个开始。我的计划是将其作为一个长期项目，持续将我实践中验证过的、有价值的代码沉淀、整理并开源出来。未来，我还会在项目详情页中，增加关联的公众号技术文章跳转，或是有功能关联的其他小程序跳转，形成一个立体化的技术知识网络。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e坦率地说，这个小程序的开发过程远超我最初的预期。从架构设计到具体实现，尤其是几个关键的技术卡点上，耗费了我大量的精力。\u003c/p\u003e\n\u003cp\u003e与广告盗刷的攻防：我的服务器每天都有人在扫描，所以在实现激励广告防刷机制时，我花了大量时间加强防刷机制，其实这个在微信小程序社区中也可以看到很多类似的问题。我这里研究了一种解决方案。就是使用加密网络通道。在对接的过程中，我与各种加密错误、签名错误（如常见的40079错误）作斗争。例如，处理会话密钥时，发现不需要进行Base64编码，而是直接以空字符串的字节形式处理；在URL拼接的细节上，POST与GET方法的误用也会导致失败；而加解密环节更是陷阱重重——加密密钥需要从Base64解码，IV却是一个直接的16位字符串，加密数据则需要从十六进制格式解码……这些细枝末节，任何一个出错都会导致整个流程崩掉，往往需要清空Token重新登录来排查。这个过程让我对网络数据安全有了更深刻的认识。这些错误和经验本想专开一篇文章介绍，但是细想一下，可能一段话就能总结，但是其中的辛酸和感悟无法表达在纸面上。索性后期整理出来开源这个模块，减少其他开发者的一些坎坷。\u003c/p\u003e\n\u003cp\u003e存储方案的抉择：在对象存储的选择上，也有一段小插曲。我最熟悉的是Mino，但是在我了解到MinIO版本已归档，便尝试了rustfs等其他方案进行探索。但经过综合对比在可靠性、API友好度以及与现有技术栈的契合度后，最终还是选择了功能完备、文档清晰的MinIO作为存储后端，事实证明这个选择是稳定高效的。\u003c/p\u003e\n\u003cp\u003e所幸，在这个艰难的过程中，AI编程助手给予了我巨大的帮助（但是也让我吃了很多苦头，因为它上面的资料都比较旧）。许多代码片段的生成、调试过程中错误信息的解读、API文档的快速查询，都因AI的介入而效率倍增。如果没有它，单靠我自己，恐怕几个月也难见成果，绝无可能在十几天内推动项目成型上线。\u003c/p\u003e\n\u003cp\u003e做这个小程序，并非为了追逐热点，这个小程序刚刚上线，还需要时间去细细打磨。开发其初心很简单：整理自己的知识资产，并以一种对开发者最友好、最便捷的方式分享出去。我希望它不仅仅是一个下载站点，更可以成为一个高质量的、经过实战检验的“代码片段集市”和“工具箱”。路漫漫其修远兮。在代码世界的探索中，每个人都是学生，也是老师。我在这里抛砖引玉，期待这些代码模块能真正帮助到在具体问题上寻找解决方案的你。也欢迎你常来看看，这些项目模块，会和我一样，在技术的道路上不断生长。\u003c/p\u003e\n\u003cp\u003e扫码体验：\u003cimg src=\"/images/visit.webp\" width=\"200\" alt=\"豆子碎片小程序\"\u003e\u003c/p\u003e","title":"我是如何设计和实现模块源码集市的？"},{"content":"查询日志里调用最多的5个接口名称\ngrep -o \u0026#39;path: [^ ]*\u0026#39; golog.log | awk \u0026#39;{print $2}\u0026#39; | sort | uniq -c | sort -nr | head -n 5 查询接口调用最多的5个IP\ngrep \u0026#34;path: /getUserById\u0026#34; golog.log | grep -o \u0026#39;ip: [0-9.]*\u0026#39; | awk \u0026#39;{print $2}\u0026#39; | sort | uniq -c | sort -nr | head -n 5 统计每分钟发送量（Top 分钟）\n# cut -c 7-22: 提取 2026-03-19T00:19 这部分 grep \u0026#34;APP 消息\u0026#34; your_log.log | cut -c 7-22 | sort | uniq -c | sort -nr | head -n 10 统计每秒发送量（最频繁的秒）\n截取到第 19 位（即 \u0026hellip;00:19:41）：\ngrep \u0026#34;APP 消息\u0026#34; your_log.log | cut -c 7-25 | sort | uniq -c | sort -nr | head -n 10 查询特定用户每分发送的消息数量\n如果你已经锁定了某个可疑用户（假设他的特征是日志里某个 ID 或 IP），想看他每一分钟的活动轨迹：\n# 假设可疑特征是 userID grep \u0026#34;userID\u0026#34; your_log.log | grep \u0026#34;APP 消息\u0026#34; | cut -c 7-22 | sort | uniq -c 自动识别“异常秒发”用户\n如果你想直接找出哪些用户在哪一秒发送超过了 2 条：\ngrep \u0026#34;APP 消息\u0026#34; your_log.log | awk -F\u0026#39;msg=\u0026#34;\u0026#39; \u0026#39;{print $1, $2}\u0026#39; | cut -c 7-25,30-60 | sort | uniq -c | awk \u0026#39;$1 \u0026gt; 2\u0026#39; ","permalink":"https://blog.91demo.top/wiki/searchlog.html","summary":"\u003cp\u003e查询日志里调用最多的5个接口名称\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egrep -o \u0026#39;path: [^ ]*\u0026#39; golog.log | awk \u0026#39;{print $2}\u0026#39; | sort | uniq -c | sort -nr | head -n 5\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查询接口调用最多的5个IP\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egrep \u0026#34;path: /getUserById\u0026#34; golog.log | grep -o \u0026#39;ip: [0-9.]*\u0026#39; | awk \u0026#39;{print $2}\u0026#39; | sort | uniq -c | sort -nr | head -n 5\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e统计每分钟发送量（Top 分钟）\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# cut -c 7-22: 提取 2026-03-19T00:19 这部分\ngrep \u0026#34;APP 消息\u0026#34; your_log.log | cut -c 7-22 | sort | uniq -c | sort -nr | head -n 10\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e统计每秒发送量（最频繁的秒）\u003cbr\u003e\n截取到第 19 位（即 \u0026hellip;00:19:41）：\u003c/p\u003e","title":"日志查询常用方法"},{"content":"在上篇原理文章介绍后，我开始实现代码，硬件使用现成的51开发板。原本以为只是简单的串口收发，结果却在 Modbus Poll 软件中疯狂循环 Checksum Error 和 Timeout。\n下面是我在普中科技 51 开发板上，使用STC89C52芯片接入工业标准的 Modbus RTU 协议，从最基础的串口打印到实现动态温度采集，并成功通过 Modbus 协议回传的完整过程。\n环境准备 硬件： 普中 51-A2 开发板 (STC89C52RC)、DS18B20 温度传感器、USB 转串口线。 软件： Keil uVision5、STC-ISP、Modbus Poll (主机仿真器)、SSCOM 串口调试助手。 核心挑战：为什么 Modbus Poll 总是报 Checksum Error？ 这是我耗时最长的地方，也是我头疼的地方。如果你也遇到数据看着对但校验不过，请检查以下三点：\n查表法的陷阱 因为51单片机的性能问题，我使用了查表法，CRC表中的数据是采摘网上教程的。在解决了之后，发现是自己设置的表中的数值不对。要知道Modbus 的 CRC16 校验非常严苛。为了方便以后的开发者，我把正确的CRC表和获取函数都贴出来。\n// 高位表 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 \u0026lt;\u0026lt; 8 | crcl); } 字节序与 Quantity 的对齐 这里也是一个坑，自己概念不清，在设置quantity时设置错误，错把他们1比1进行换算。还有Modbus 规定 低字节在前，高字节在后。\n// 读保持寄存器03响应 void Response03() { unsigned int crc; unsigned char i; g_send_buf[0] = 0x01; g_send_buf[1] = 0x03; g_send_buf[2] = 0x02;// 这个地方需要和Modbus poll中的quantity对应，它们是2倍的关系，即Quantity为1，这里就得填2。 g_send_buf[3] = (unsigned char)(g_temp_val \u0026gt;\u0026gt; 8); g_send_buf[4] = (unsigned char)(g_temp_val \u0026amp; 0xFF); crc = GetCRC16(g_send_buf, 5); g_send_buf[5] = crc \u0026amp; 0xFF; g_send_buf[6] = (crc \u0026gt;\u0026gt; 8) \u0026amp; 0xFF; for (i = 0; i \u0026lt; 7; i++) { SendByte(g_send_buf[i]); } } 串口停止位（Stop Bits） 我这里设置的正确，要和Modbus RTU 标准匹配，要求“无校验”模式必须配 2 个停止位。很多时候 Checksum Error 不是代码错了，而是软件设置里 1 位和 2 位停止位的微小采样差异，这个可以调整一下进行测试。还有就是晶振频率，也需要适配波特率。\n从“死等”到状态机 (State Machine) 最初使用的循环计时，但是这样会有一个问题，串口来了数据无法响应导致超时。为了解决 DS18B20 读取耗时（约 750ms）与 Modbus 实时响应之间的冲突，我弃用了简单的 Delay 函数，改用了基于 定时器 0 的时间片轮询架构。\n实现思路如下：\n中断只立 Flag： 串口中断只负责接收计数，不直接回传。 主循环分发任务： 处理 Modbus 响应优先级最高，传感器采样每 1 秒触发一次。 下面是示例代码，请谨慎采用：\nvoid main() { UartInit(); // 串口初始化 Timer0Init(); // 定时器0初始化 while (1){ if(g_flag_modbus){ // 串口中断 Response03(); // 返回读取的温度Modbus响应 g_flag_modbus = 0; // 重新计数 } if(g_flag_1s){ // 计时中断 g_flag_1s = 0; // 重新计时 UpdateTemp(); // 采集温度 } } } 攻克 DS18B20 的动态干扰 在加入温度传感器后，我遇到了 “偶尔跳负数” 和 “Modbus 偶发无法响应” 的问题。在长时间分析之后，发现是中断影响了时序，要知道 DS18B20 的单总线时序（1-Wire）不能被中断打断。\n解决方案： 我们需要实施“原子级”的中断开关保护。在读写 Bit 的关键微秒内 EA = 0，一旦完成立刻 EA = 1。这样既保护了温度准确度，又给了串口中断“换气”的时间。\n这是示例代码，请谨慎采用：\n// 我在这里设置中断开关保护，是考虑了效率和稳定性，如果追求极致，可以在读写字节内部使用中断开关保护。 void UpdateTemp() { unsigned char LSB, MSB; // 温度高低字节 int raw; // 转换后温度 EA = 0; // 关闭中断 if(DS18B20_Init()==0){ DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); } EA = 1; // 打开中断 EA = 0; if (DS18B20_Init() == 0) { DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); LSB = DS18B20_ReadByte(); MSB = DS18B20_ReadByte(); } EA = 1; raw = (MSB \u0026lt;\u0026lt; 8) | LSB; if(raw != 0x0550) { // 初始值 g_temp_val = (int)(raw * 5 \u0026gt;\u0026gt; 3); // 这里相当于 * 0.625 } } 目前温度被放大 10 倍存储为整数（25.5℃ -\u0026gt; 255），在 Modbus Poll 中通过 Multiplier = 0.1 即可完美还原。\n通过这次调试，可以采用下面的思路来快速定位和解决问题：\n先通后精： 先用固定变量跑通 Modbus 协议，再加动态传感器。 善用工具： Modbus Poll 的 Communication 窗口是抓包的神器，不要盲目猜错。 中断管理： 时序敏感的操作（如单总线）一定要配合 EA 开关。 ","permalink":"https://blog.91demo.top/embedded/sensemodbus.html","summary":"\u003cp\u003e在上篇原理文章介绍后，我开始实现代码，硬件使用现成的51开发板。原本以为只是简单的串口收发，结果却在 Modbus Poll 软件中疯狂循环 Checksum Error 和 Timeout。\u003c/p\u003e\n\u003cp\u003e下面是我在普中科技 51 开发板上，使用STC89C52芯片接入工业标准的 Modbus RTU 协议，从最基础的串口打印到实现动态温度采集，并成功通过 Modbus 协议回传的完整过程。\u003c/p\u003e\n\u003ch2 id=\"环境准备\"\u003e环境准备\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e硬件： 普中 51-A2 开发板 (STC89C52RC)、DS18B20 温度传感器、USB 转串口线。\u003c/li\u003e\n\u003cli\u003e软件： Keil uVision5、STC-ISP、Modbus Poll (主机仿真器)、SSCOM 串口调试助手。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"51开发板\" loading=\"lazy\" src=\"/images/embedded/51board.webp\"\u003e\u003c/p\u003e\n\u003ch2 id=\"核心挑战为什么-modbus-poll-总是报-checksum-error\"\u003e核心挑战：为什么 Modbus Poll 总是报 Checksum Error？\u003c/h2\u003e\n\u003cp\u003e这是我耗时最长的地方，也是我头疼的地方。如果你也遇到数据看着对但校验不过，请检查以下三点：\u003c/p\u003e\n\u003ch3 id=\"查表法的陷阱\"\u003e查表法的陷阱\u003c/h3\u003e\n\u003cp\u003e因为51单片机的性能问题，我使用了查表法，CRC表中的数据是采摘网上教程的。在解决了之后，发现是自己设置的表中的数值不对。要知道Modbus 的 CRC16 校验非常严苛。为了方便以后的开发者，我把正确的CRC表和获取函数都贴出来。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 高位表\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e code aucCRCHi[] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 低位表\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e code aucCRCLo[] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x00\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x01\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC3\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x03\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x02\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC6\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x06\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x07\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC7\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x05\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC5\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC4\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x04\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0xCC\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x0C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x0D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xCD\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x0F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xCF\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xCE\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x0E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x0A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xCA\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xCB\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x0B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC9\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x09\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x08\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xC8\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0xD8\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x18\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x19\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD9\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x1B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xDB\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xDA\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x1A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x1E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xDE\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xDF\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x1F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xDD\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x1D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x1C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xDC\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x14\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD4\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD5\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x15\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD7\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x17\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x16\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD6\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x12\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x13\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD3\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x11\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xD0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x10\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0xF0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x30\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x31\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x33\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF3\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x32\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x36\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF6\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF7\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x37\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF5\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x35\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x34\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF4\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x3C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xFC\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xFD\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x3D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xFF\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x3F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x3E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xFE\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xFA\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x3A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x3B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xFB\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x39\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF9\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xF8\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x38\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x28\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE8\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE9\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x29\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xEB\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x2B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x2A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xEA\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xEE\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x2E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x2F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xEF\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x2D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xED\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xEC\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x2C\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0xE4\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x24\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x25\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE5\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x27\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE7\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE6\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x26\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x22\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE3\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x23\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x21\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x20\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xE0\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0xA0\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x60\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x61\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x63\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA3\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x62\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x66\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA6\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA7\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x67\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA5\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x65\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x64\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA4\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x6C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xAC\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xAD\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x6D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xAF\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x6F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x6E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xAE\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xAA\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x6A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x6B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xAB\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x69\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA9\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xA8\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x68\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x78\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB8\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB9\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x79\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xBB\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x7B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x7A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xBA\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xBE\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x7E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x7F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xBF\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x7D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xBD\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xBC\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x7C\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0xB4\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x74\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x75\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB5\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x77\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB7\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB6\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x76\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x72\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB3\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x73\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB1\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x71\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x70\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0xB0\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x50\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x90\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x91\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x51\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x93\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x53\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x52\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x92\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x96\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x56\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x57\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x97\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x55\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x95\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x94\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x54\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x9C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x5C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x5D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x9D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x5F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x9F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x9E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x5E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x5A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x9A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x9B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x5B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x99\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x59\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x58\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x98\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x88\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x48\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x49\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x89\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x4B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x8B\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x8A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x4A\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x4E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x8E\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x8F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x4F\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x8D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x4D\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x4C\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x8C\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#ae81ff\"\u003e0x44\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x84\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x85\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x45\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x87\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x47\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x46\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x86\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x82\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x42\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x43\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x83\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x41\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x81\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x80\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0x40\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 获取CRC\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eGetCRC16\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003epData, \u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e len) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e uIndex;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e crch \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0xFF\u003c/span\u003e; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003echar\u003c/span\u003e crcl \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0xFF\u003c/span\u003e; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (len\u003cspan style=\"color:#f92672\"\u003e--\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        uIndex \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e crcl \u003cspan style=\"color:#f92672\"\u003e^\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003epData\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e;     \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        crcl \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e crch \u003cspan style=\"color:#f92672\"\u003e^\u003c/span\u003e aucCRCHi[uIndex];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        crch \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e aucCRCLo[uIndex];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003eunsigned\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e)(crch \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e8\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e|\u003c/span\u003e crcl);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"字节序与-quantity-的对齐\"\u003e字节序与 Quantity 的对齐\u003c/h3\u003e\n\u003cp\u003e这里也是一个坑，自己概念不清，在设置quantity时设置错误，错把他们1比1进行换算。还有Modbus 规定 低字节在前，高字节在后。\u003c/p\u003e","title":"在51单片机实现 Modbus RTU 协议过程中的踩坑和思考"},{"content":"豆子域名管家是一款基于 Wails 3 开发的超轻量级单文件客户端。它不仅拥有 Naive UI 打造的精致界面，更集成了强大的域名监控能力：\n全方位监测：主面板表格直观展示域名列表、SSL 证书剩余天数、Whois 到期时间。 智能告警：支持自定义告警阈值，状态（正常/警告/错误）一目了然。 多渠道通知：深度集成钉钉与企业微信，支持配置调度时间与通知频次，确保告警不漏报、不打扰。 静默守护：支持随系统自动启动，后台默默守护您的域名安全。 为什么需要版本提醒，为什么不是自动更新？ 在开发 豆子域名管家 时，Wails 3 就提供了自动集成的更新方案，但经过深思熟虑，我选择了“版本提醒 + 手动覆盖”的策略：主要考虑到个人服务器的带宽限制，避免高并发下载造成服务器宕机。单文件二进制直接覆盖即可完成升级，无需复杂的安装程序。\n在之前的版本中，我主要通过公众号发布新版本并提供下载链接进行手动升级。它的问题是用户需要关注公众号并手动前往蓝奏云下载。为了进一步优化体验，我决定在新版本中引入版本对比提醒：\n红点微标提醒：客户端启动后会自动对比本地与云端版本号。若有新版，左下角版本号将出现灵动的红点徽标，提醒而不打扰。 交互式遮罩弹窗：点击徽标即可弹出基于 Naive UI 开发的漂亮窗口，清晰展示： 当前版本 vs 最新版本 详细的更新日志 保姆级使用方法说明 极简升级路径：点击“复制下载链接”，直接在浏览器中下载最新的二进制文件，覆盖旧文件即可完成升级。 版本检测核心实现思路 这不仅仅是一个简单的弹窗，背后凝聚了对安全和灵活性的思考。为了确保版本检测既高效又安全，我们采用了以下技术路径：\n1，我没有在前端 JS 中硬编码版本号。使用了Go 后端驱动，客户端通过 Wails 3 的原生桥接功能，调用后端 Go 方法获取本地编译时的版本标识。\n2，防爬虫机制：在向云端请求最新版本信息时，配置了特定的 Custom Header 校验，有效拦截恶意脚本和不必要的扫描，保护服务器资源。\n3，设计了灵活的云端数据结构。我们的版本接口不只是返回一个数字，而是返回一个包含三个核心维度的 JSON 对象：\n最新版本号：用于精准比对。 下载地址：支持动态变更下载镜像，防止因链接失效导致的无法更新。 更新日志：让用户第一时间了解修复了哪些 Bug 或新增了哪些功能。 4，极致的 UI 交互（基于 Naive UI）\n当本地版本低于云端时，左下角会亮起精美的红点徽标。点击后的交互逻辑非常人性化：\n信息全透明：对比当前版本与新版本，展示完整的更新列表和使用指南。 尊重用户选择：弹窗配备了清晰的“取消/稍后再说”按钮，绝不强制升级。 快捷操作：提供“复制下载链接”按钮，点击后自动写入剪切板。用户只需打开浏览器粘贴，即可从蓝奏云等高速渠道下载，覆盖即升级。免去了前期的复杂步骤。 界面功能效果预览 徽标提示\n弹窗说明\n工具如何开始使用？ 无论您是拥有几十个域名的运维大拿，还是只有几个小站的个人玩家，豆子域名管家都是您的不二之选。\n导入域名：支持批量导入，快速建立监控列表。 配置通知：填入您的 Webhook 机器人，设置好告警阈值。 放心运营：剩下的交给工具，证书即将到期前，您的手机会准时收到提醒。 常见问题 (FAQ) Q: 为什么我点击下载链接没反应？\nA: 建议使用“复制下载链接”按钮，手动在 Chrome 或 Edge 浏览器中打开，以确保护盾类插件不会拦截下载请求。\nQ: 我的数据安全吗？\nA: 豆子域名管家是本地客户端工具，所有配置（如 Webhook 地址、域名列表）均保存在本地磁盘，不会上传到任何云端服务器。\nQ: 如何反馈建议或 Bug？\nA: 您可以通过客户端“帮助”页面的公众号二维码联系开发者。\n立即下载体验，告别证书过期焦虑！\n点击前往下载页面\n","permalink":"https://blog.91demo.top/go/btaddvercheck.html","summary":"\u003cp\u003e豆子域名管家是一款基于 Wails 3 开发的超轻量级单文件客户端。它不仅拥有 Naive UI 打造的精致界面，更集成了强大的域名监控能力：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e全方位监测：主面板表格直观展示域名列表、SSL 证书剩余天数、Whois 到期时间。\u003c/li\u003e\n\u003cli\u003e智能告警：支持自定义告警阈值，状态（正常/警告/错误）一目了然。\u003c/li\u003e\n\u003cli\u003e多渠道通知：深度集成钉钉与企业微信，支持配置调度时间与通知频次，确保告警不漏报、不打扰。\u003c/li\u003e\n\u003cli\u003e静默守护：支持随系统自动启动，后台默默守护您的域名安全。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"为什么需要版本提醒为什么不是自动更新\"\u003e为什么需要版本提醒，为什么不是自动更新？\u003c/h2\u003e\n\u003cp\u003e在开发 豆子域名管家 时，Wails 3 就提供了自动集成的更新方案，但经过深思熟虑，我选择了“版本提醒 + 手动覆盖”的策略：主要考虑到个人服务器的带宽限制，避免高并发下载造成服务器宕机。单文件二进制直接覆盖即可完成升级，无需复杂的安装程序。\u003c/p\u003e\n\u003cp\u003e在之前的版本中，我主要通过公众号发布新版本并提供下载链接进行手动升级。它的问题是用户需要关注公众号并手动前往蓝奏云下载。为了进一步优化体验，我决定在新版本中引入版本对比提醒：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e红点微标提醒：客户端启动后会自动对比本地与云端版本号。若有新版，左下角版本号将出现灵动的红点徽标，提醒而不打扰。\u003c/li\u003e\n\u003cli\u003e交互式遮罩弹窗：点击徽标即可弹出基于 Naive UI 开发的漂亮窗口，清晰展示：\n\u003cul\u003e\n\u003cli\u003e当前版本 vs 最新版本\u003c/li\u003e\n\u003cli\u003e详细的更新日志\u003c/li\u003e\n\u003cli\u003e保姆级使用方法说明\u003c/li\u003e\n\u003cli\u003e极简升级路径：点击“复制下载链接”，直接在浏览器中下载最新的二进制文件，覆盖旧文件即可完成升级。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"版本检测核心实现思路\"\u003e版本检测核心实现思路\u003c/h2\u003e\n\u003cp\u003e这不仅仅是一个简单的弹窗，背后凝聚了对安全和灵活性的思考。为了确保版本检测既高效又安全，我们采用了以下技术路径：\u003c/p\u003e\n\u003cp\u003e1，我没有在前端 JS 中硬编码版本号。使用了Go 后端驱动，客户端通过 Wails 3 的原生桥接功能，调用后端 Go 方法获取本地编译时的版本标识。\u003c/p\u003e\n\u003cp\u003e2，防爬虫机制：在向云端请求最新版本信息时，配置了特定的 Custom Header 校验，有效拦截恶意脚本和不必要的扫描，保护服务器资源。\u003c/p\u003e\n\u003cp\u003e3，设计了灵活的云端数据结构。我们的版本接口不只是返回一个数字，而是返回一个包含三个核心维度的 JSON 对象：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e最新版本号：用于精准比对。\u003c/li\u003e\n\u003cli\u003e下载地址：支持动态变更下载镜像，防止因链接失效导致的无法更新。\u003c/li\u003e\n\u003cli\u003e更新日志：让用户第一时间了解修复了哪些 Bug 或新增了哪些功能。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e4，极致的 UI 交互（基于 Naive UI）\u003cbr\u003e\n当本地版本低于云端时，左下角会亮起精美的红点徽标。点击后的交互逻辑非常人性化：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e信息全透明：对比当前版本与新版本，展示完整的更新列表和使用指南。\u003c/li\u003e\n\u003cli\u003e尊重用户选择：弹窗配备了清晰的“取消/稍后再说”按钮，绝不强制升级。\u003c/li\u003e\n\u003cli\u003e快捷操作：提供“复制下载链接”按钮，点击后自动写入剪切板。用户只需打开浏览器粘贴，即可从蓝奏云等高速渠道下载，覆盖即升级。免去了前期的复杂步骤。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"界面功能效果预览\"\u003e界面功能效果预览\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e徽标提示\u003cbr\u003e\n\u003cimg alt=\"alt 徽标提示\" loading=\"lazy\" src=\"/images/go/vercheck-tips.png\"\u003e\u003c/li\u003e\n\u003cli\u003e弹窗说明\u003cbr\u003e\n\u003cimg alt=\"alt 弹窗说明\" loading=\"lazy\" src=\"/images/go/vercheck-window.png\"\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"工具如何开始使用\"\u003e工具如何开始使用？\u003c/h2\u003e\n\u003cp\u003e无论您是拥有几十个域名的运维大拿，还是只有几个小站的个人玩家，豆子域名管家都是您的不二之选。\u003c/p\u003e","title":"为豆子域名管家实现版本检测功能的技术实践"},{"content":"背景与问题描述 Cloudreve是一个自托管文件管理与共享系统，支持多存储提供商。\n在部署 Cloudreve 存储策略时，我们需要集成内网环境下的 MinIO S3 服务。由于 MinIO 部署在内网并配合内网穿透暴露至公网，且使用了自签名证书，我们在配置完成后发现客户端无法正常上传文件。\n初步排查： 确认内网穿透链路正常。 使用 MinIO 客户端工具 mc 测试，发现必须带上 \u0026ndash;insecure 参数才能正常访问，明确了核心矛盾在于 SSL 证书验证失败。 尝试将证书放置在自定义目录并使用 mc \u0026ndash;config-dir 指定，虽能连接成功，但 Cloudreve 无法直接复用该配置，且即便在同目录下放置证书也未能生效。 最终方案： 将自定义 CA 证书导入系统信任库，使 Cloudreve（Go 环境）能够原生识别。\n解决方案：在 CentOS 系统中安装并激活自定义证书 1，检查证书是否PEM格式？ Go 程序通常识别PEM格式的证书。\n使用以下命令检查证书文件（如public.crt）：\ncat public.crt 如果开头是 \u0026mdash;\u0026ndash;BEGIN CERTIFICATE\u0026mdash;\u0026ndash;，则是 PEM 格式。 如果是一堆乱码，则是 DER 格式。 需要转换为PEM格式：\nopenssl x509 -inform der -in public.crt -out public.pem 2，放置证书 在CentOS Linux中，需要将证书文件放在/etc/pki/ca-trust/source/anchors/目录下，并且后缀必须为.crt。如果文件名为public.pem，需要改为public.crt。\n确认文件是否放置正确？\nls /etc/pki/ca-trust/source/anchors/ 输出中应包含我们的证书。\n3，激活证书 执行以下命令激活，让系统重新扫描目录并生成全局信任束，依次执行：\nupdate-ca-trust extract update-ca-trust 4，验证证书是否激活成功 检查系统全局证书合集ca-bundle.crt是否已包含你的证书信息：\ntail -n 20 /etc/pki/tls/certs/ca-bundle.crt 查看是否是自己的证书，如果还没有成功，执行命令追加：\ncat public.crt | sudo tee -a /etc/pki/tls/certs/ca-bundle.crt 最后，使用 grep 搜索证书中关键的信息\ngrep -i \u0026#34;域名信息\u0026#34; /etc/pki/tls/certs/ca-bundle.crt 这个时候应该可以搜索了。\n5，验证在系统中使用证书 在Cloudreve所在的服务器上使用curl验证，我们使用命令检验Minio是否可以使用自签名证书\ncurl -v https://yourdomain.com:9000 如果输出SSL证书信息，并显示Access 相关信息，则成功了。如果没有，则需要再次检查证书生成信息是否正确？例如，是否包含了正确的域名？\n6，重启Cloudreve网盘服务 最后一定要重启Cloudreve网盘，只有重启之后才能加载系统自定义证书。\n# 根据你的部署方式重启，例如： systemctl restart cloudreve 在处理自托管服务的 TLS 问题时，系统级信任往往比应用级配置更高效。针对 Cloudreve 这种基于 Go 编写的应用，只要底层操作系统信任了 CA 证书，上层的 S3 连接问题便迎刃而解。\n","permalink":"https://blog.91demo.top/devops/cloudreves3.html","summary":"\u003ch2 id=\"背景与问题描述\"\u003e背景与问题描述\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/cloudreve/cloudreve\"\u003eCloudreve\u003c/a\u003e是一个自托管文件管理与共享系统，支持多存储提供商。\u003c/p\u003e\n\u003cp\u003e在部署 Cloudreve 存储策略时，我们需要集成内网环境下的 MinIO S3 服务。由于 MinIO 部署在内网并配合内网穿透暴露至公网，且使用了自签名证书，我们在配置完成后发现客户端无法正常上传文件。\u003c/p\u003e\n\u003ch3 id=\"初步排查\"\u003e初步排查：\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e确认内网穿透链路正常。\u003c/li\u003e\n\u003cli\u003e使用 MinIO 客户端工具 mc 测试，发现必须带上 \u0026ndash;insecure 参数才能正常访问，明确了核心矛盾在于 SSL 证书验证失败。\u003c/li\u003e\n\u003cli\u003e尝试将证书放置在自定义目录并使用 mc \u0026ndash;config-dir 指定，虽能连接成功，但 Cloudreve 无法直接复用该配置，且即便在同目录下放置证书也未能生效。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"最终方案\"\u003e最终方案：\u003c/h3\u003e\n\u003cp\u003e将自定义 CA 证书导入系统信任库，使 Cloudreve（Go 环境）能够原生识别。\u003c/p\u003e\n\u003ch2 id=\"解决方案在-centos-系统中安装并激活自定义证书\"\u003e解决方案：在 CentOS 系统中安装并激活自定义证书\u003c/h2\u003e\n\u003ch3 id=\"1检查证书是否pem格式\"\u003e1，检查证书是否PEM格式？\u003c/h3\u003e\n\u003cp\u003eGo 程序通常识别PEM格式的证书。\u003cbr\u003e\n使用以下命令检查证书文件（如public.crt）：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecat public.crt\n\u003c/code\u003e\u003c/pre\u003e\u003cul\u003e\n\u003cli\u003e如果开头是 \u0026mdash;\u0026ndash;BEGIN CERTIFICATE\u0026mdash;\u0026ndash;，则是 PEM 格式。\u003c/li\u003e\n\u003cli\u003e如果是一堆乱码，则是 DER 格式。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e需要转换为PEM格式：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eopenssl x509 -inform der -in public.crt -out public.pem\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2放置证书\"\u003e2，放置证书\u003c/h3\u003e\n\u003cp\u003e在CentOS Linux中，需要将证书文件放在/etc/pki/ca-trust/source/anchors/目录下，并且后缀必须为.crt。如果文件名为public.pem，需要改为public.crt。\u003c/p\u003e\n\u003cp\u003e确认文件是否放置正确？\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003els /etc/pki/ca-trust/source/anchors/\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e输出中应包含我们的证书。\u003c/p\u003e\n\u003ch3 id=\"3激活证书\"\u003e3，激活证书\u003c/h3\u003e\n\u003cp\u003e执行以下命令激活，让系统重新扫描目录并生成全局信任束，依次执行：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eupdate-ca-trust extract\nupdate-ca-trust\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"4验证证书是否激活成功\"\u003e4，验证证书是否激活成功\u003c/h3\u003e\n\u003cp\u003e检查系统全局证书合集\u003ccode\u003eca-bundle.crt\u003c/code\u003e是否已包含你的证书信息：\u003c/p\u003e","title":"解决 Cloudreve 无法通过 HTTPS 连接自签名证书 MinIO"},{"content":"数据分析是微信小程序优化和运营的重要手段。通过用户行为埋点，开发者可以深入了解用户的使用习惯和行为模式，从而进行精准的优化和推广。本文将介绍微信小程序的数据分析方法，特别是用户行为埋点的实施步骤。\n什么是用户行为埋点 用户行为埋点是指在小程序的关键节点上埋入数据采集代码，记录用户的行为数据。通过埋点数据，开发者可以分析用户的行为轨迹，了解用户在小程序中的操作习惯和偏好。\n1. 埋点规划 确定分析目标 在进行埋点之前，首先要明确数据分析的目标。例如：\n用户路径分析：了解用户在小程序中的访问路径。 功能使用分析：分析用户对各个功能的使用频率和效果。 活动效果分析：评估营销活动的效果，了解用户的参与情况。 设计埋点方案 根据分析目标，设计详细的埋点方案，包括以下内容：\n埋点位置：确定在小程序的哪些页面和操作上埋点。 埋点事件：定义具体的埋点事件，如页面访问、按钮点击、表单提交等。 埋点参数：确定需要采集的参数，如用户 ID、时间戳、页面名称、按钮名称等。 2. 实施埋点 使用微信数据分析工具 微信提供了数据分析工具，可以方便地进行埋点和数据采集。\n引入数据分析 SDK 在小程序的 app.js 文件中引入微信数据分析 SDK：\nApp({ onLaunch: function () { // 引入腾讯分析 SDK var mta = require(\u0026#34;mta-wechat-analysis\u0026#34;); // 初始化 SDK mta.App.init({ appID: \u0026#34;YOUR_APP_ID\u0026#34;, eventID: \u0026#34;YOUR_EVENT_ID\u0026#34;, autoReport: true, statParam: true, ignoreParams: [], launched: true, }); }, }); 埋点示例 在需要埋点的页面或操作中，调用埋点方法：\nPage({ onLoad: function () { // 页面访问埋点 mta.Page.init(); }, handleButtonClick: function () { // 按钮点击埋点 mta.Event.stat(\u0026#34;button_click\u0026#34;, { button_name: \u0026#34;start_button\u0026#34; }); }, }); 使用第三方数据分析工具 除了微信数据分析工具，开发者还可以使用第三方数据分析工具（如 Google Analytics、Mixpanel 等）进行埋点和数据分析。\n引入第三方分析 SDK 根据第三方数据分析工具的文档，引入相应的 SDK 并初始化。\n埋点示例 在需要埋点的页面或操作中，调用第三方分析工具的埋点方法。\n3. 数据分析和优化 数据监控 通过数据分析工具的后台，实时监控小程序的用户行为数据。例如：\n访问数据：查看小程序的访问量、活跃用户数、使用时长等指标。 行为数据：分析用户的访问路径、功能使用频率、点击热图等数据。 转化数据：评估用户的转化率、留存率、活动参与情况等指标。 数据优化 根据数据分析结果，进行小程序的优化和改进。例如：\n功能优化：根据用户对各个功能的使用情况，优化功能设计和用户体验。 内容优化：根据用户的兴趣和偏好，优化小程序的内容和界面。 营销优化：根据活动效果分析结果，优化营销策略和推广方案。 4. 数据隐私和安全 在进行数据采集和分析时，需遵守相关的法律法规，保护用户的隐私和数据安全。例如：\n用户授权：在采集用户数据前，需获得用户的明确授权，并告知数据采集的目的和用途。 数据加密：对采集到的用户数据进行加密处理，确保数据在传输和存储过程中的安全。 隐私政策：制定并公开小程序的隐私政策，向用户说明数据采集和使用的相关信息。 通过以上方法，您可以有效进行微信小程序的数据分析，深入了解用户行为，优化小程序的功能和内容。如果在数据分析过程中遇到问题，可以查阅微信官方文档或在开发者社区寻求帮助。\n","permalink":"https://blog.91demo.top/wiki/mpanalysis.html","summary":"\u003cp\u003e数据分析是微信小程序优化和运营的重要手段。通过用户行为埋点，开发者可以深入了解用户的使用习惯和行为模式，从而进行精准的优化和推广。本文将介绍微信小程序的数据分析方法，特别是用户行为埋点的实施步骤。\u003c/p\u003e\n\u003ch2 id=\"什么是用户行为埋点\"\u003e什么是用户行为埋点\u003c/h2\u003e\n\u003cp\u003e用户行为埋点是指在小程序的关键节点上埋入数据采集代码，记录用户的行为数据。通过埋点数据，开发者可以分析用户的行为轨迹，了解用户在小程序中的操作习惯和偏好。\u003c/p\u003e\n\u003ch2 id=\"1-埋点规划\"\u003e1. 埋点规划\u003c/h2\u003e\n\u003ch3 id=\"确定分析目标\"\u003e确定分析目标\u003c/h3\u003e\n\u003cp\u003e在进行埋点之前，首先要明确数据分析的目标。例如：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e用户路径分析：了解用户在小程序中的访问路径。\u003c/li\u003e\n\u003cli\u003e功能使用分析：分析用户对各个功能的使用频率和效果。\u003c/li\u003e\n\u003cli\u003e活动效果分析：评估营销活动的效果，了解用户的参与情况。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"设计埋点方案\"\u003e设计埋点方案\u003c/h3\u003e\n\u003cp\u003e根据分析目标，设计详细的埋点方案，包括以下内容：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e埋点位置：确定在小程序的哪些页面和操作上埋点。\u003c/li\u003e\n\u003cli\u003e埋点事件：定义具体的埋点事件，如页面访问、按钮点击、表单提交等。\u003c/li\u003e\n\u003cli\u003e埋点参数：确定需要采集的参数，如用户 ID、时间戳、页面名称、按钮名称等。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"2-实施埋点\"\u003e2. 实施埋点\u003c/h2\u003e\n\u003ch3 id=\"使用微信数据分析工具\"\u003e使用微信数据分析工具\u003c/h3\u003e\n\u003cp\u003e微信提供了数据分析工具，可以方便地进行埋点和数据采集。\u003c/p\u003e\n\u003ch4 id=\"引入数据分析-sdk\"\u003e引入数据分析 SDK\u003c/h4\u003e\n\u003cp\u003e在小程序的 \u003ccode\u003eapp.js\u003c/code\u003e 文件中引入微信数据分析 SDK：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eApp\u003c/span\u003e({\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003eonLaunch\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e () {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 引入腾讯分析 SDK\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emta\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003erequire\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mta-wechat-analysis\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 初始化 SDK\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003emta\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eApp\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003einit\u003c/span\u003e({\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003eappID\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;YOUR_APP_ID\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003eeventID\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;YOUR_EVENT_ID\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003eautoReport\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003estatParam\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003eignoreParams\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e [],\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003elaunched\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    });\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e});\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"埋点示例\"\u003e埋点示例\u003c/h4\u003e\n\u003cp\u003e在需要埋点的页面或操作中，调用埋点方法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003ePage\u003c/span\u003e({\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003eonLoad\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e () {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 页面访问埋点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003emta\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ePage\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003einit\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003ehandleButtonClick\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e () {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 按钮点击埋点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003emta\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eEvent\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003estat\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;button_click\u0026#34;\u003c/span\u003e, { \u003cspan style=\"color:#a6e22e\"\u003ebutton_name\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;start_button\u0026#34;\u003c/span\u003e });\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e});\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"使用第三方数据分析工具\"\u003e使用第三方数据分析工具\u003c/h3\u003e\n\u003cp\u003e除了微信数据分析工具，开发者还可以使用第三方数据分析工具（如 Google Analytics、Mixpanel 等）进行埋点和数据分析。\u003c/p\u003e","title":"微信小程序数据分析指南（用户行为埋点）"},{"content":"我将使用经典的 STC89C12 单片机作为核心，配合 DS18B20 数字温度传感器和 MAX485 通信芯片，构建一个支持标准 Modbus-RTU 协议的感知节点。\n一、核心架构：传感器、大脑与传声筒 这个小模块的本质是一个“翻译官”。它把环境中的物理温度转化为数字信号，再按照工业标准协议通过长线传输给上位机（如 PLC 或电脑）。\n感知单元 (DS18B20)： 不同于传统的模拟热敏电阻，它是数字传感器，直接输出 12 位精度的二进制温度数据。 处理中心 (STC89C12)： 负责按照时序“读”传感器，并把数据存入内存，同时监听串口指令。 通信接口 (MAX485)： 单片机的 TTL 信号传不远，MAX485 将其转换为差分信号，实现抗干扰的长距离传输。 二、硬件链路设计 为了简化电路并验证可行性，我们将引脚定义如下：\nDS18B20 接口： 连接至 P1.0。由于 1-Wire 总线需要上拉电阻，我们在硬件上需确保 P1.0 与 VCC 之间有一个 4.7kΩ 的电阻，以维持空闲时的高电平。 MAX485 控制： UART 接口： RXD(P3.0) 接 RO，TXD(P3.1) 接 DI。 收发切换 (RE/DE)： 连接至 P3.2。RS485 是半双工的，平时 P3.2 置低电平处于“听”模式；当需要回传数据时，将其置高切换为“说”模式。 烧录接口： 仅预留 VCC、GND、TXD、RXD 四线接口，用于程序的迭代验证。 三、技术实现思路 A. 如何采集 DS18B20？ STC89C12 通过 单总线 (1-Wire) 时序 与传感器对话。由于 STC89C12 速度较慢且不支持硬件单总线，我们需要通过精准的软件延时来模拟时序：\n复位与存在检测： 单片机拉低总线 480μs 以上再释放，等待传感器回传一个低电平信号，确认传感器在线。 跳过 ROM 命令 (0xCC)： 因为总线上只有一个传感器，无需匹配唯一序列号。 启动转换 (0x44)： 命令传感器开始将模拟温度转为数字量。 读取暂存器 (0xBE)： 将转换好的 2 字节温度原始数据读回单片机寄存器。 B. 如何实现 Modbus-RTU 协议？ Modbus 协议就像是“对暗号”。单片机需要在内存中开辟一个“寄存器池”。\n帧监听： 单片机通过串口中断不停接收字节。如果总线超过 3.5 个字节的时间没有信号，则认为一帧数据接收完毕。 地址校验： 模块检查接收到的第一个字节。如果是自己的地址（例如 0x01），则继续解析；否则丢弃。 功能码匹配： 重点实现 03 功能码（读取保持寄存器）。当上位机询问“读取 0001 地址的寄存器”时，单片机就把刚刚从 DS18B20 读到的温度值填入响应帧。 CRC 校验： 这是工业级的灵魂。单片机计算整帧数据的循环冗余校验码，确保传输过程中没有错码。 Modbus-RTU 报文深度拆解 为了让上位机（如电脑上的串口助手）读到温度，双方必须按照特定的“语法”交流。假设我们的模块地址设为 01，温度寄存器地址设为 0000。\nA. 上位机请求帧（问）\n当主机想知道温度时，会发送 8 个字节：\n01：从机地址（谁在说话？）。 03：功能码（我要读寄存器内容）。 00 00：寄存器起始地址（从哪个位置开始读？）。 00 01：读取数量（读几个寄存器？1个寄存器=2字节）。 84 0A：CRC 校验码（由计算得出，确保指令在传输中没被干扰）。 B. 单片机响应帧（答）\n单片机收到上述指令后，计算 CRC 无误且地址匹配，就会回传：\n01：我的地址是 01。 03：响应读指令。 02：数据字节数（1 个寄存器包含 2 个字节）。 00 FA：温度数据（假设 0x00FA 换算为十进制是 250，代表 25.0℃）。 39 93：单片机计算出的 CRC 校验码。 CRC16-Modbus 算法 的实现逻辑 在工业现场，变频器、电机会产生大量的电磁干扰。我了保证数据的准确性，我们需要研究 CRC16-Modbus 算法，这是保证数据准确性的一种方案。它是通过一串复杂的位运算（异或与移位），为每一帧数据生成一个独一无二的“指纹”。\n由于 STC89C12 的运算能力有限，在实际编写时，通常会采用查表法或循环运算法来实现这个校验。\n从 DS18B20 的微观时序，到 STC89C12 的逻辑中转，再到 Modbus-RTU 的宏观协议。当你看到串口助手上跳出正确的温度数值时，你已经打通了工业物联网最底层的“神经末梢”。\n工业级产品之所以昂贵，是因为它们在保证这套逻辑运行的基础上，投入了大量的成本在稳定性上：比如使用能在 -40℃ 到 85℃ 正常工作的宽温元器件、增加 TBU 防雷组件，以及编写极其健壮的判错机制。但万变不离其宗，核心永远是这段精巧的时序与协议交换。\n只要理解了“时序采集 + 协议封装”这两步，你手中的 STC89C12 就不再是玩具，而是一个符合工业标准的智能传感器原型。\n⚠️ 免责声明（重要）\n特别说明： 本方案及相关思路仅用于个人学习、教学演示及原理验证。\n由于 STC89C12 硬件性能限制（如缺乏硬件掉电保护、时钟漂移等）以及本设计未添加工业级必备的电气隔离、防浪涌、防静电（ESD）及抗电磁干扰（EMC）外围电路，请勿将此方案直接应用于工业生产、医疗设备、易燃易爆环境或任何可能造成人身伤害及财产损失的正式生产环境中。\n若违反此规定自行将其用于生产，由此产生的一切安全问题、经济损失或法律后果均由使用者自行承担，作者对此不承担任何法律责任。\n四、PCB布局建议 针对该模块的 PCB 布局建议（例如 MAX485 与 DS18B20 的走线如何规避干扰），以便真正开始动手焊接这个小电路板？\n为了让这个小模块在物理层面上更接近“工业级”表现，PCB 布局（Layout）不仅仅是把线连通，更关乎热隔离与信号抗干扰。以下是针对该方案的布局建议：\n1. 核心布局策略：动静分区 我们将电路板分为 感知区（DS18B20） 和 通信/逻辑区（STC89C12 + MAX485）。\n热隔离设计：\nDS18B20 必须远离发热源。 单片机和 MAX485 在工作时会产生微量热量，如果离得太近，传感器读到的就是 PCB 板温而非环境气温。 开槽处理： 在 DS18B20 与单片机之间，可以在 PCB 上挖一道空槽（物理隔离），减少热传导。 避开大面积铺铜： 传感器引脚下方不要铺大面积的地线铜箔，以免 PCB 本身的热容影响测温灵敏度。 RS485 通信优化：\n差分走线： MAX485 输出的 A、B 两根线应平行走线，长度尽量保持一致，以提高共模干扰抑制能力。 短残桩（Short Stubs）： 如果是从主线上引出的分支，分支线要越短越好，避免阻抗不匹配产生反射。 2. 关键元器件放置建议 去耦电容（Decoupling Capacitor）： 在 STC89C12 和 MAX485 的 VCC 与 GND 引脚附近，必须放置一个 0.1μF 的瓷片电容。这能过滤掉高频噪声，防止单片机在 485 切换瞬间复位。 上拉电阻： DS18B20 的 P1.0 引脚需要一个 4.7kΩ 的电阻连接到 VCC。建议将其靠近单片机端放置，确保数据线空闲时电平稳定。 终端电阻（预留）： 在 RS485 的 A、B 线末端，通常需要一个 120Ω 的匹配电阻。在你的小板子上，可以预留两个焊盘，如果不处于总线的最末端则不焊接。 3. 干扰规避技巧 地线回路： 使用“星形接地”或大面积地平面。避免信号线绕成大圈，因为闭合环路就像天线，会吸收空间中的电磁干扰。 烧录接口位置： 预留的重新刷芯片接口（TXD/RXD/VCC/GND）应放置在边缘，且远离 MAX485 的 A/B 线，防止烧录线在测试时引入杂波。 4. 工业级改进： 电源保护： 增加防雷浪涌保护、极性反接保护。 信号隔离： 使用光耦或磁耦将单片机与 485 总线电气隔离，防止外部高压烧毁芯片。 时钟精度： 工业级可能使用高精度外部晶振或自带补偿的芯片（如 STC8 系列），确保高温/低温环境下通信波特率不漂移。 ⚠️ 免责声明\n再次提醒： 以上 PCB 布局建议仅适用于学习和实验目的。\n本方案未包含工业级模块中必备的光耦隔离、TVS 防浪涌管及 PPTC 自恢复保险丝等保护电路。严禁将此电路直接用于强电干扰严重或对可靠性有极高要求的工业生产环境。如因忽略防护设计导致硬件损坏或安全事故，后果自负。\n","permalink":"https://blog.91demo.top/embedded/sensetemp.html","summary":"\u003cp\u003e我将使用经典的 STC89C12 单片机作为核心，配合 DS18B20 数字温度传感器和 MAX485 通信芯片，构建一个支持标准 Modbus-RTU 协议的感知节点。\u003c/p\u003e\n\u003ch2 id=\"一核心架构传感器大脑与传声筒\"\u003e一、核心架构：传感器、大脑与传声筒\u003c/h2\u003e\n\u003cp\u003e这个小模块的本质是一个“翻译官”。它把环境中的物理温度转化为数字信号，再按照工业标准协议通过长线传输给上位机（如 PLC 或电脑）。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e感知单元 (DS18B20)： 不同于传统的模拟热敏电阻，它是数字传感器，直接输出 12 位精度的二进制温度数据。\u003c/li\u003e\n\u003cli\u003e处理中心 (STC89C12)： 负责按照时序“读”传感器，并把数据存入内存，同时监听串口指令。\u003c/li\u003e\n\u003cli\u003e通信接口 (MAX485)： 单片机的 TTL 信号传不远，MAX485 将其转换为差分信号，实现抗干扰的长距离传输。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"二硬件链路设计\"\u003e二、硬件链路设计\u003c/h2\u003e\n\u003cp\u003e为了简化电路并验证可行性，我们将引脚定义如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDS18B20 接口： 连接至 P1.0。由于 1-Wire 总线需要上拉电阻，我们在硬件上需确保 P1.0 与 VCC 之间有一个 4.7kΩ 的电阻，以维持空闲时的高电平。\u003c/li\u003e\n\u003cli\u003eMAX485 控制：\n\u003cul\u003e\n\u003cli\u003eUART 接口： RXD(P3.0) 接 RO，TXD(P3.1) 接 DI。\u003c/li\u003e\n\u003cli\u003e收发切换 (RE/DE)： 连接至 P3.2。RS485 是半双工的，平时 P3.2 置低电平处于“听”模式；当需要回传数据时，将其置高切换为“说”模式。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e烧录接口： 仅预留 VCC、GND、TXD、RXD 四线接口，用于程序的迭代验证。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三技术实现思路\"\u003e三、技术实现思路\u003c/h2\u003e\n\u003ch3 id=\"a-如何采集-ds18b20\"\u003eA. 如何采集 DS18B20？\u003c/h3\u003e\n\u003cp\u003eSTC89C12 通过 单总线 (1-Wire) 时序 与传感器对话。由于 STC89C12 速度较慢且不支持硬件单总线，我们需要通过精准的软件延时来模拟时序：\u003c/p\u003e","title":"自制可使用 Modbus 采集的 RS485 温度传感器（原理验证版）"},{"content":"在运营技术社群时，一个核心矛盾始终存在：既要开放接纳真正的同行者，又要坚决拦截泛滥的广告与营销机器人。传统的QQ群“固定答案”验证方式，在初期简单有效，但随着时间推移，其静态特性成为了最大的安全漏洞——答案一旦被爬取或泄露，广告机器人便可大规模、自动化地入侵，使群管理陷入疲于应付的被动局面。\n本文介绍一套我们实践并升级的解决方案，其核心思想是：将静态的进群壁垒，转变为一次性的、由用户主动完成的有广告成本的动态验证流程，并将管理后台智能化、移动化。\n一、旧模式的风险：固定答案的“失效时钟” 过去，许多社群采用在群资料页设置一个固定问题（如：“我们的工具叫什么？”），并配上一个不变的答案。\n这种方式存在固有缺陷：\n可被遍历破解：如果放在公开页面（如引导页 91demo.top），机器人脚本可以爬取答案。\n无用户成本：如果设置场景答案（如：“我们的工具叫什么？”），人工获取答案可以零成本，批量QQ号码申请加群。\n管理滞后：管理员在广告账号进群后发现漏洞，然后手动踢人，更改答案，过程繁琐且效果不好，用户进群没有成本，并且永远慢攻击一步。\n二、新模式的核心思想：验证流程动态化与管理终端化 我们的新体系围绕两个核心理念重构：\n1. 验证流程动态化与成本化 我们不再让答案“躺”在某个公开页面。流程变为：\n引导：用户从 91demo.top或其它入口获知，需通过微信小程序获取一次性的进群答案。\n交互：用户扫描小程序码进入小程序。小程序首先展示社群介绍、规则，建立初步认知与信任。\n验证与获取：用户点击“支持作者”等按钮，触发观看一则激励式视频广告。广告播放完毕后，小程序后端会为该次会话生成一个动态验证码，并显示给用户。\n入群：用户在QQ加群申请中，手动输入或粘贴此动态码完成验证。\n这一流程的精妙之处在于：\n动态性：每次生成的答案可能不同。\n成本壁垒：观看广告的几十秒时间，对于真人用户是可以接受的“微小付出”，但对于追求效率、大规模作业的广告机器人来说，却构成了极高的时间与模拟交互成本，使其攻击行为变得极不经济。\n正向激励：广告收益可以反哺社群运营或工具开发，形成良性循环。\n2. 管理后台终端化与敏捷化 解决了用户端的问题，管理员端同样需要解放。传统修改群答案的操作非常低效，需要修改代码重新发布或者打开管理后台进行设置。\n现在我们的解决方案是：将答案设置功能集成到同一个微信小程序的管理后台。\n移动管理：管理员只需在手机上打开小程序的管理员页面，即可随时、随地生成并设置新的QQ群验证答案。\n敏捷响应：一旦发现异常或出于定期更新的安全考虑，管理员能在1分钟内完成答案的全局更新，让所有旧答案立即失效，实现对安全威胁的分钟级响应。\n操作闭环：整个“风险感知 -\u0026gt; 决策 -\u0026gt; 执行”的安全管理闭环，在移动端瞬间完成，极大地提升了社群的主动防御能力。\n三、技术实现思路简述 小程序端：作为核心交互界面，负责展示信息、调用广告组件、并向服务器发起获取动态码的请求。\n服务器端：维护验证逻辑。当收到小程序端“广告播放完成”的回调后，结合时间戳、用户临时标识等生成当前有效的QQ群答案。\n管理后台：在小程序中为管理员提供专属界面，调用API通知服务器更新QQ群验证答案，保持信息一致性。\nQQ群设置：答案更新后，管理员仍需在QQ群设置中填入新答案，想过自动化设置QQ群答案方案（有封号风险，放弃）。但由于管理后台在手机端，这一步操作变得非常便捷。\n四、总结：从“静态防守”到“智能动态防御” 这套方案的本质，是将社群准入机制从一道固定的、被动的门，升级为一个智能的、互动的过滤器。\n对真实用户：流程清晰，付出微小时间成本，获得一个无广告的纯净交流环境。\n对广告机器人：构建了难以自动化跨越的“时间成本”与“动态密码”双重高墙。\n对管理员：实现了安全策略的敏捷部署与极致便捷的移动化运维。\n通过“动态验证码”提高攻击成本，再通过“小程序管理后台”降低防御成本，这一升一降之间，我们不仅有效地屏蔽了广告骚扰，更构建了一个具备持续进化能力的社群安全运营体系。技术的价值，在于优雅地解决真实世界的问题，这便是这一实践最好的注脚。\n","permalink":"https://blog.91demo.top/wiki/qqgrptech.html","summary":"\u003cp\u003e在运营技术社群时，一个核心矛盾始终存在：既要开放接纳真正的同行者，又要坚决拦截泛滥的广告与营销机器人。传统的QQ群“固定答案”验证方式，在初期简单有效，但随着时间推移，其静态特性成为了最大的安全漏洞——答案一旦被爬取或泄露，广告机器人便可大规模、自动化地入侵，使群管理陷入疲于应付的被动局面。\u003c/p\u003e\n\u003cp\u003e本文介绍一套我们实践并升级的解决方案，其核心思想是：将静态的进群壁垒，转变为一次性的、由用户主动完成的有广告成本的动态验证流程，并将管理后台智能化、移动化。\u003c/p\u003e\n\u003ch2 id=\"一旧模式的风险固定答案的失效时钟\"\u003e一、旧模式的风险：固定答案的“失效时钟”\u003c/h2\u003e\n\u003cp\u003e过去，许多社群采用在群资料页设置一个固定问题（如：“我们的工具叫什么？”），并配上一个不变的答案。\u003c/p\u003e\n\u003cp\u003e这种方式存在固有缺陷：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e可被遍历破解：如果放在公开页面（如引导页 91demo.top），机器人脚本可以爬取答案。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e无用户成本：如果设置场景答案（如：“我们的工具叫什么？”），人工获取答案可以零成本，批量QQ号码申请加群。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e管理滞后：管理员在广告账号进群后发现漏洞，然后手动踢人，更改答案，过程繁琐且效果不好，用户进群没有成本，并且永远慢攻击一步。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"二新模式的核心思想验证流程动态化与管理终端化\"\u003e二、新模式的核心思想：验证流程动态化与管理终端化\u003c/h2\u003e\n\u003cp\u003e我们的新体系围绕两个核心理念重构：\u003c/p\u003e\n\u003ch3 id=\"1-验证流程动态化与成本化\"\u003e1. 验证流程动态化与成本化\u003c/h3\u003e\n\u003cp\u003e我们不再让答案“躺”在某个公开页面。流程变为：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e引导：用户从 91demo.top或其它入口获知，需通过微信小程序获取一次性的进群答案。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e交互：用户扫描小程序码进入小程序。小程序首先展示社群介绍、规则，建立初步认知与信任。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e验证与获取：用户点击“支持作者”等按钮，触发观看一则激励式视频广告。广告播放完毕后，小程序后端会为该次会话生成一个动态验证码，并显示给用户。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e入群：用户在QQ加群申请中，手动输入或粘贴此动态码完成验证。\u003c/p\u003e\n\u003cp\u003e这一流程的精妙之处在于：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e动态性：每次生成的答案可能不同。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e成本壁垒：观看广告的几十秒时间，对于真人用户是可以接受的“微小付出”，但对于追求效率、大规模作业的广告机器人来说，却构成了极高的时间与模拟交互成本，使其攻击行为变得极不经济。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e正向激励：广告收益可以反哺社群运营或工具开发，形成良性循环。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-管理后台终端化与敏捷化\"\u003e2. 管理后台终端化与敏捷化\u003c/h3\u003e\n\u003cp\u003e解决了用户端的问题，管理员端同样需要解放。传统修改群答案的操作非常低效，需要修改代码重新发布或者打开管理后台进行设置。\u003c/p\u003e\n\u003cp\u003e现在我们的解决方案是：将答案设置功能集成到同一个微信小程序的管理后台。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e移动管理：管理员只需在手机上打开小程序的管理员页面，即可随时、随地生成并设置新的QQ群验证答案。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e敏捷响应：一旦发现异常或出于定期更新的安全考虑，管理员能在1分钟内完成答案的全局更新，让所有旧答案立即失效，实现对安全威胁的分钟级响应。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e操作闭环：整个“风险感知 -\u0026gt; 决策 -\u0026gt; 执行”的安全管理闭环，在移动端瞬间完成，极大地提升了社群的主动防御能力。\u003c/p\u003e\n\u003ch2 id=\"三技术实现思路简述\"\u003e三、技术实现思路简述\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e小程序端：作为核心交互界面，负责展示信息、调用广告组件、并向服务器发起获取动态码的请求。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e服务器端：维护验证逻辑。当收到小程序端“广告播放完成”的回调后，结合时间戳、用户临时标识等生成当前有效的QQ群答案。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e管理后台：在小程序中为管理员提供专属界面，调用API通知服务器更新QQ群验证答案，保持信息一致性。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eQQ群设置：答案更新后，管理员仍需在QQ群设置中填入新答案，想过自动化设置QQ群答案方案（有封号风险，放弃）。但由于管理后台在手机端，这一步操作变得非常便捷。\u003c/p\u003e\n\u003ch2 id=\"四总结从静态防守到智能动态防御\"\u003e四、总结：从“静态防守”到“智能动态防御”\u003c/h2\u003e\n\u003cp\u003e这套方案的本质，是将社群准入机制从一道固定的、被动的门，升级为一个智能的、互动的过滤器。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e对真实用户：流程清晰，付出微小时间成本，获得一个无广告的纯净交流环境。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e对广告机器人：构建了难以自动化跨越的“时间成本”与“动态密码”双重高墙。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e对管理员：实现了安全策略的敏捷部署与极致便捷的移动化运维。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e通过“动态验证码”提高攻击成本，再通过“小程序管理后台”降低防御成本，这一升一降之间，我们不仅有效地屏蔽了广告骚扰，更构建了一个具备持续进化能力的社群安全运营体系。技术的价值，在于优雅地解决真实世界的问题，这便是这一实践最好的注脚。\u003c/p\u003e","title":"以动态验证码构建QQ社群防护网：从固定答案到小程序智能管理的安全升级"},{"content":"🔐 欢迎加入我们的交流群，让我们一起走得更远\n你好，新朋友！\n非常高兴你想加入我们的社群。这里聚集了一群对技术、开发、工具效率有着共同热爱的伙伴。为了确保这里能成为一个高质量、无骚扰、专注交流的空间，我们设置了一个简单、透明的进群流程。\n扫码获取进群答案：\n【技术源泉-豆子工具交流群】群号：309409711 【Minio技术学习交流群】群号：1006992606 为什么需要这一步？ 你可能注意到了，在获取进群“门票”前，需要观看一段简短的激励广告。这并非为了设置障碍，而是我们为保护这个社群环境所建立的一道最基础、也最有效的防火墙。\n它的唯一目的，是拦截大量的广告机器人、营销号和垃圾信息发布者。我们相信，一个愿意为进入一个干净社群而付出一点点时间的你，正是我们希望遇到的、真诚的交流者。你的这几秒钟，将直接帮助所有人获得一个更清爽、更有价值的交流环境。\n如何加入？只需三步\n扫描微信小程序码。\n完成验证步骤：系统会引导你观看一段激励广告（通常约15-30秒）。广告播放完毕后，你会自动获得唯一的进群答案（一串数字验证码）。\n申请加群：回到QQ群搜索界面，找到我们的群（群号/群名通常会与本站相关），在申请验证答案栏中，输入你刚刚获得的那串答案，并提交申请。\n（选填）管理员会在核实答案后，尽快通过你的申请。通常这个过程是自动或半自动的，速度很快。\n我们的承诺 无隐私收集：此流程不会要求你提供任何个人敏感信息。\n纯粹的环境：我们致力维护一个无商业广告、无骚扰、专注于主题讨论的社群。\n你的支持有意义：广告产生的微量收益，将直接用于支持相关工具/网站的服务器与持续开发。\n感谢你的理解、支持与这短短的等待。我们期待在门后与你相遇，一起分享知识，解决问题，共同成长。\n","permalink":"https://blog.91demo.top/wiki/qqgrpanswer.html","summary":"\u003cp\u003e🔐 欢迎加入我们的交流群，让我们一起走得更远\u003c/p\u003e\n\u003cp\u003e你好，新朋友！\u003c/p\u003e\n\u003cp\u003e非常高兴你想加入我们的社群。这里聚集了一群对技术、开发、工具效率有着共同热爱的伙伴。为了确保这里能成为一个高质量、无骚扰、专注交流的空间，我们设置了一个简单、透明的进群流程。\u003c/p\u003e\n\u003cp\u003e扫码获取进群答案：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e【技术源泉-豆子工具交流群】群号：309409711\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"技术源泉-豆子工具交流群\" loading=\"lazy\" src=\"/images/qqtechtool.webp\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e【Minio技术学习交流群】群号：1006992606\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Minio技术学习交流群\" loading=\"lazy\" src=\"/images/qqminio.webp\"\u003e\u003c/p\u003e\n\u003ch2 id=\"为什么需要这一步\"\u003e为什么需要这一步？\u003c/h2\u003e\n\u003cp\u003e你可能注意到了，在获取进群“门票”前，需要观看一段简短的激励广告。这并非为了设置障碍，而是我们为保护这个社群环境所建立的一道最基础、也最有效的防火墙。\u003c/p\u003e\n\u003cp\u003e它的唯一目的，是拦截大量的广告机器人、营销号和垃圾信息发布者。我们相信，一个愿意为进入一个干净社群而付出一点点时间的你，正是我们希望遇到的、真诚的交流者。你的这几秒钟，将直接帮助所有人获得一个更清爽、更有价值的交流环境。\u003c/p\u003e\n\u003cp\u003e如何加入？只需三步\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e扫描微信小程序码。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e完成验证步骤：系统会引导你观看一段激励广告（通常约15-30秒）。广告播放完毕后，你会自动获得唯一的进群答案（一串数字验证码）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e申请加群：回到QQ群搜索界面，找到我们的群（群号/群名通常会与本站相关），在申请验证答案栏中，输入你刚刚获得的那串答案，并提交申请。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e（选填）管理员会在核实答案后，尽快通过你的申请。通常这个过程是自动或半自动的，速度很快。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"我们的承诺\"\u003e我们的承诺\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e无隐私收集：此流程不会要求你提供任何个人敏感信息。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e纯粹的环境：我们致力维护一个无商业广告、无骚扰、专注于主题讨论的社群。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e你的支持有意义：广告产生的微量收益，将直接用于支持相关工具/网站的服务器与持续开发。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e感谢你的理解、支持与这短短的等待。我们期待在门后与你相遇，一起分享知识，解决问题，共同成长。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"获取QQ进群验证答案"},{"content":"在PC客户端开发中，开机自启动是提升用户体验的重要功能之一。豆子域名管家作为一款Windows平台下的域名管理工具，近期添加了随系统启动功能。本文将详细介绍从技术选型到最终实现的全过程，重点阐述在跨平台库适配失败后，如何针对Windows系统特性实现简洁可靠的自启动方案。\n一、技术选型与挑战 1.1 初始方案：跨平台库的尝试 项目初期采用了go-autostart这一流行的Go语言跨平台自启动库。该库设计优雅，理论上支持Windows、macOS、Linux三大主流操作系统。然而在实际集成到wails3项目中时，遇到了编译问题：\nsslchecker .\\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) 经过排查，发现虽然能定位到源码，但由于系统配置或依赖管理问题，无法正确调用库方法。这种问题在Go模块化开发中并不少见，特别是涉及CGO或系统特定依赖时。\n1.2 平台限制的现实考量 进一步分析发现，即使解决编译问题，跨平台方案仍面临以下限制：\nmacOS的签名要求：自macOS Catalina以来，苹果加强了应用安全策略。无签名的应用在开机自启动时会被Gatekeeper拦截，除非用户手动进入系统设置\u0026gt;安全性与隐私\u0026gt;通用中点击\u0026quot;仍要打开\u0026quot;。\nLinux的碎片化：不同桌面环境（GNOME、KDE、XFCE等）的自启动机制存在差异，需要适配多种配置方式。\n维护成本：跨平台库在提供便利的同时，也引入了额外的依赖和潜在的兼容性问题。\n考虑到豆子域名管家主要用户群体为Windows用户，且无macOS开发者证书，决定采用专注Windows的轻量化方案。\n二、Windows自启动实现方案 2.1 技术原理 Windows开机自启动主要通过注册表实现。当前用户的自启动项位于：\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run 系统级的自启动项（需要管理员权限）位于：\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run 对于大多数桌面应用，使用用户级注册表即可满足需求，且无需提权操作。\n2.2 核心实现代码 // auto_start_windows.go // +build windows package main import ( \u0026#34;errors\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;golang.org/x/sys/windows/registry\u0026#34; ) // AutoStartManager Windows自启动管理器 type AutoStartManager struct { appName string exePath string regPath string } // NewAutoStartManager 创建自启动管理器实例 func NewAutoStartManager(appName string) (*AutoStartManager, error) { exePath, err := os.Executable() if err != nil { return nil, errors.New(\u0026#34;获取可执行文件路径失败\u0026#34;) } // 转换为绝对路径并处理空格 absPath, _ := filepath.Abs(exePath) if strings.Contains(absPath, \u0026#34; \u0026#34;) { absPath = `\u0026#34;` + absPath + `\u0026#34;` } return \u0026amp;AutoStartManager{ appName: appName, exePath: absPath, regPath: `Software\\Microsoft\\Windows\\CurrentVersion\\Run`, }, nil } // IsAutoStartEnabled 检查是否已启用开机自启动 func (m *AutoStartManager) IsAutoStartEnabled() (bool, error) { key, err := registry.OpenKey(registry.CURRENT_USER, m.regPath, registry.QUERY_VALUE) if err != nil { return false, err } defer key.Close() value, _, err := key.GetStringValue(m.appName) if err != nil { if err == registry.ErrNotExist { return false, nil } return false, err } // 对比路径，处理可能的引号差异 current := strings.Trim(m.exePath, `\u0026#34;`) stored := strings.Trim(value, `\u0026#34;`) return strings.EqualFold(filepath.Clean(current), filepath.Clean(stored)), nil } // SetAutoStart 设置或取消开机自启动 func (m *AutoStartManager) SetAutoStart(enable bool) error { key, err := registry.OpenKey(registry.CURRENT_USER, m.regPath, registry.QUERY_VALUE|registry.SET_VALUE) if err != nil { return err } defer key.Close() if enable { return key.SetStringValue(m.appName, m.exePath) } else { err = key.DeleteValue(m.appName) if err == registry.ErrNotExist { return nil // 键不存在不算错误 } return err } } 2.3 关键实现细节 路径处理：使用os.Executable()获取可执行文件绝对路径，确保在不同工作目录下都能正确运行。\n空格处理：Windows路径包含空格时必须用引号包裹，否则注册表项无法正确解析。\n错误处理：区分\u0026quot;键不存在\u0026quot;和\u0026quot;读取失败\u0026quot;两种情况，前者表示未设置自启动，后者表示真正的错误。\n构建标签：通过//go:build windows 确保代码仅在Windows平台编译，避免在其他平台编译错误。\n三、前端界面集成 3.1 设计原则 前端界面设计遵循以下原则：\n状态同步：开关状态实时反映系统实际设置\n操作反馈：用户操作后提供明确的成功/失败提示\n错误恢复：操作失败时自动恢复原状态\n3.2 前端实现方案 // settings.js - 前端设置界面 class AutoStartManager { constructor() { this.switchElement = document.getElementById(\u0026#39;autoStartSwitch\u0026#39;); this.saveButton = document.getElementById(\u0026#39;saveSettingsBtn\u0026#39;); this.init(); } async init() { // 初始化时读取当前状态 await this.loadCurrentState(); // 绑定开关事件 this.switchElement.addEventListener(\u0026#39;change\u0026#39;, (e) =\u0026gt; { this.onSwitchChange(e.target.checked); }); // 绑定保存按钮事件 this.saveButton.addEventListener(\u0026#39;click\u0026#39;, () =\u0026gt; { this.saveSettings(); }); } async loadCurrentState() { try { const isEnabled = await window.go.main.App.IsAutoStartEnabled(); this.switchElement.checked = isEnabled; this.updateStatusText(isEnabled); } catch (error) { console.error(\u0026#39;读取自启动状态失败:\u0026#39;, error); this.showError(\u0026#39;无法读取当前设置，请重试\u0026#39;); } } async onSwitchChange(enabled) { const oldState = !enabled; try { const success = await window.go.main.App.SetAutoStart(enabled); if (success) { this.updateStatusText(enabled); this.showSuccess(enabled ? \u0026#39;已开启开机自启动\u0026#39; : \u0026#39;已关闭开机自启动\u0026#39;); } else { // 操作失败，恢复原状态 this.switchElement.checked = oldState; this.showError(\u0026#39;设置失败，请检查权限或重试\u0026#39;); } } catch (error) { console.error(\u0026#39;设置自启动失败:\u0026#39;, error); this.switchElement.checked = oldState; this.showError(\u0026#39;操作失败: \u0026#39; + error.message); } } updateStatusText(enabled) { const statusElement = document.getElementById(\u0026#39;autoStartStatus\u0026#39;); if (statusElement) { statusElement.textContent = enabled ? \u0026#39;已开启\u0026#39; : \u0026#39;已关闭\u0026#39;; statusElement.className = enabled ? \u0026#39;status-enabled\u0026#39; : \u0026#39;status-disabled\u0026#39;; } } showSuccess(message) { // 显示成功提示 this.showNotification(message, \u0026#39;success\u0026#39;); } showError(message) { // 显示错误提示 this.showNotification(message, \u0026#39;error\u0026#39;); } showNotification(message, type) { // 实现通知显示逻辑 const notification = document.createElement(\u0026#39;div\u0026#39;); notification.className = `notification notification-${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() =\u0026gt; { notification.remove(); }, 3000); } async saveSettings() { // 可在此处添加批量保存逻辑 this.showSuccess(\u0026#39;设置已保存\u0026#39;); } } 3.3 后端Go接口 // backend.go - 后端接口 package main import ( \u0026#34;context\u0026#34; ) type App struct { ctx context.Context autoStartManager *AutoStartManager } func NewApp() *App { manager, err := NewAutoStartManager(\u0026#34;BeanDomainManager\u0026#34;) if err != nil { // 记录日志，但不阻止应用启动 log.Printf(\u0026#34;初始化自启动管理器失败: %v\u0026#34;, err) } return \u0026amp;App{ autoStartManager: manager, } } func (a *App) IsAutoStartEnabled() (bool, error) { if a.autoStartManager == nil { return false, errors.New(\u0026#34;自启动功能不可用\u0026#34;) } return a.autoStartManager.IsAutoStartEnabled() } func (a *App) SetAutoStart(enable bool) (bool, error) { if a.autoStartManager == nil { return false, errors.New(\u0026#34;自启动功能不可用\u0026#34;) } err := a.autoStartManager.SetAutoStart(enable) if err != nil { return false, err } return true, nil } 3.4 编译 wails3 build 使用它可以重新编译，生成新的可执行文件。\n3.5 验证 验证有多种方法，我这里直接使用脚本进行查看\nGet-ItemProperty -Path \u0026#34;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\u0026#34; | Select-Object -Property \u0026#34;sslchecker\u0026#34; 3.6 更换桌面图标 这次还更换了系统托盘图标，网上介绍的方法很多，经过实践，我选择了最简单的一个。那就是把你制作好的桌面图标文件，将其命名为appicon.png，然后替换build文件夹下的同名文件，最后重新编译即可。\n通过本次实践，豆子域名管家成功实现了稳定可靠的开机自启动功能，在技术选型上做到了务实与前瞻性的平衡。方案虽简单，但充分考虑了实际使用场景、用户权限和未来扩展性，为类似功能的实现提供了可借鉴的范例。\n","permalink":"https://blog.91demo.top/go/addautostart.html","summary":"\u003cp\u003e在PC客户端开发中，开机自启动是提升用户体验的重要功能之一。豆子域名管家作为一款Windows平台下的域名管理工具，近期添加了随系统启动功能。本文将详细介绍从技术选型到最终实现的全过程，重点阐述在跨平台库适配失败后，如何针对Windows系统特性实现简洁可靠的自启动方案。\u003c/p\u003e\n\u003ch2 id=\"一技术选型与挑战\"\u003e一、技术选型与挑战\u003c/h2\u003e\n\u003ch3 id=\"11-初始方案跨平台库的尝试\"\u003e1.1 初始方案：跨平台库的尝试\u003c/h3\u003e\n\u003cp\u003e项目初期采用了go-autostart这一流行的Go语言跨平台自启动库。该库设计优雅，理论上支持Windows、macOS、Linux三大主流操作系统。然而在实际集成到wails3项目中时，遇到了编译问题：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esslchecker\n.\\domainservice.go:1453:11: app.Enable undefined (type *autostart.App has no field or method Enable)\n.\\domainservice.go:1455:11: app.Disable undefined (type *autostart.App has no field or method Disable)\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e经过排查，发现虽然能定位到源码，但由于系统配置或依赖管理问题，无法正确调用库方法。这种问题在Go模块化开发中并不少见，特别是涉及CGO或系统特定依赖时。\u003c/p\u003e\n\u003ch3 id=\"12-平台限制的现实考量\"\u003e1.2 平台限制的现实考量\u003c/h3\u003e\n\u003cp\u003e进一步分析发现，即使解决编译问题，跨平台方案仍面临以下限制：\u003c/p\u003e\n\u003cp\u003emacOS的签名要求：自macOS Catalina以来，苹果加强了应用安全策略。无签名的应用在开机自启动时会被Gatekeeper拦截，除非用户手动进入系统设置\u0026gt;安全性与隐私\u0026gt;通用中点击\u0026quot;仍要打开\u0026quot;。\u003c/p\u003e\n\u003cp\u003eLinux的碎片化：不同桌面环境（GNOME、KDE、XFCE等）的自启动机制存在差异，需要适配多种配置方式。\u003c/p\u003e\n\u003cp\u003e维护成本：跨平台库在提供便利的同时，也引入了额外的依赖和潜在的兼容性问题。\u003c/p\u003e\n\u003cp\u003e考虑到豆子域名管家主要用户群体为Windows用户，且无macOS开发者证书，决定采用专注Windows的轻量化方案。\u003c/p\u003e\n\u003ch2 id=\"二windows自启动实现方案\"\u003e二、Windows自启动实现方案\u003c/h2\u003e\n\u003ch3 id=\"21-技术原理\"\u003e2.1 技术原理\u003c/h3\u003e\n\u003cp\u003eWindows开机自启动主要通过注册表实现。当前用户的自启动项位于：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e系统级的自启动项（需要管理员权限）位于：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eHKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e对于大多数桌面应用，使用用户级注册表即可满足需求，且无需提权操作。\u003c/p\u003e\n\u003ch3 id=\"22-核心实现代码\"\u003e2.2 核心实现代码\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// auto_start_windows.go\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// +build windows\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003epackage\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e (\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;errors\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;os\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;path/filepath\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;strings\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;golang.org/x/sys/windows/registry\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// AutoStartManager Windows自启动管理器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003etype\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eAutoStartManager\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eexePath\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eregPath\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// NewAutoStartManager 创建自启动管理器实例\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eNewAutoStartManager\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e) (\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eAutoStartManager\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003eerror\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eexePath\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eos\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eExecutable\u003c/span\u003e()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerrors\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eNew\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;获取可执行文件路径失败\u0026#34;\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 转换为绝对路径并处理空格\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eabsPath\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003e_\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efilepath\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eAbs\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eexePath\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrings\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eContains\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eabsPath\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34; \u0026#34;\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eabsPath\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e`\u0026#34;`\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eabsPath\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`\u0026#34;`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eAutoStartManager\u003c/span\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e: \u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eexePath\u003c/span\u003e: \u003cspan style=\"color:#a6e22e\"\u003eabsPath\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eregPath\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e`Software\\Microsoft\\Windows\\CurrentVersion\\Run`\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }, \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// IsAutoStartEnabled 检查是否已启用开机自启动\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eAutoStartManager\u003c/span\u003e) \u003cspan style=\"color:#a6e22e\"\u003eIsAutoStartEnabled\u003c/span\u003e() (\u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003eerror\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eOpenKey\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eCURRENT_USER\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eregPath\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eQUERY_VALUE\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edefer\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eClose\u003c/span\u003e()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003evalue\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003e_\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eGetStringValue\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eErrNotExist\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 对比路径，处理可能的引号差异\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ecurrent\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrings\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eTrim\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eexePath\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e`\u0026#34;`\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003estored\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrings\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eTrim\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003evalue\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e`\u0026#34;`\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estrings\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eEqualFold\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003efilepath\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eClean\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecurrent\u003c/span\u003e), \u003cspan style=\"color:#a6e22e\"\u003efilepath\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eClean\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003estored\u003c/span\u003e)), \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// SetAutoStart 设置或取消开机自启动\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eAutoStartManager\u003c/span\u003e) \u003cspan style=\"color:#a6e22e\"\u003eSetAutoStart\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eenable\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e) \u003cspan style=\"color:#66d9ef\"\u003eerror\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eOpenKey\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eCURRENT_USER\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eregPath\u003c/span\u003e, \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eQUERY_VALUE\u003c/span\u003e|\u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eSET_VALUE\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edefer\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eClose\u003c/span\u003e()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eenable\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eSetStringValue\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eexePath\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    } \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e = \u003cspan style=\"color:#a6e22e\"\u003ekey\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eDeleteValue\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003em\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eappName\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eregistry\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eErrNotExist\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e// 键不存在不算错误\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"23-关键实现细节\"\u003e2.3 关键实现细节\u003c/h3\u003e\n\u003cp\u003e路径处理：使用os.Executable()获取可执行文件绝对路径，确保在不同工作目录下都能正确运行。\u003c/p\u003e","title":"为豆子域名管家实现Windows开机自启动功能的技术实践"},{"content":"一、 那件披在身上的大衣 老家人养猪，冬天的半夜，都要披上厚重的大衣，去猪舍看产床温度。猪舍的墙上挂了一个水银温度计。温度低了猪仔会冻死，温度高了会脱水。氨气重了会生病，感觉味道重了，需要手动打开抽风机。这种“靠人巡、靠鼻闻、靠经验”的原始模式，不仅累，而且风险极高。\n供暖烧煤炉，煤炉需要半夜起来加煤，人困得不行；母猪还得使用电热板，电热板是那种电阻丝加热的，没有温控计，只能隔几个小时去关掉，等温度降下去再打开。说到底，还是得靠人去“看着”。\n我想，能不能用技术，把家人从这种重复、枯燥且充满风险的体力劳动中解放出来一点点？不用花太多钱，因为他们会心疼。\n二、 攻坚方案：黑盒的温控逻辑 可以看到面临的主要是“环境失控风险”。\n温度控制的极端性：依靠烧煤和简易电热板，温度分布不均。半夜人工起夜不仅辛苦，且这种“大跨度”的温差变化会导致猪仔腹泻甚至死亡。 空气质量的隐形威胁：氨气超标是诱发呼吸道疾病的主因。仅靠“鼻闻”时，空气质量往往已经恶化到危及健康的程度。 利润被风险蚕食：养猪大头是料钱和药钱，但成活率才是利润的底线。一次深夜的失误，可能让数月的辛苦付诸东流。 能马上想到的临时解决方案：\n恒温自动化（解决“半夜加煤”与“手动插拔”）\n温控传感器方案：购买带有探头的温控开关，将电热板连接到温控插座上。\n效果：设定好区间（如32-35度），温度低了自动通电，够了自动断电。不仅省去了人工看管，还能通过减少无效耗电抵消设备成本。\n环境预警系统（解决“鼻闻”与“风险”）\n智能监控终端：安装一个集成了温湿度监控与氨气检测的智能传感器，通过 Wi-Fi 或 4G 信号连接手机。\n效果：当温度异常或氨气浓度过高时，手机会自动发出警报铃声提醒，避免了老人家整晚不敢闭眼的焦虑。\n联动抽风（解决“呼吸道疾病”）\n自动排风系统：将原有的抽风机改装为智能联动。\n效果：当氨气传感器感应到超标，自动开启抽风机，达标后关闭。这能精准减少热量流失，同时保证猪舍空气清新。\n核心硬件选择：务实与成本的平衡 因为猪舍现场环境非常恶劣，湿度，氨气，粉尘等因素，温控开关坏的频率非常高，如果接到料房，又采集不了温度。购买氨气专业设备成本又非常高。现场可以根据温度和通风进行解决，但在冬季，保暖是另一个问题。实际考量后，还是自己动手做比较划算。零部件能够直接买并满足现场需求的，直接购买，满足不了，自己动手制作，需要防腐蚀处理：焊接完成后，用酒精清洗焊渣，然后必须喷涂“三防漆”。外壳选用 IP65 防水接线盒，进线口使用电缆防水密封接头 (PG7)，否则氨气会从缝隙钻进去。线路外层加PVC管，防鼠咬，腐蚀，机械损伤。\n传感器：水银温度计不行，无法采集。工业级的 RS485 温湿度传感器太贵（动辄上百元），购买不锈钢封装好的民用级的DS18B20数字温度传感器，焊接后要密封好，挂在猪舍合适的位置。它通过一根单总线连接，协议简单，成本极低。\n主控：具备 NB-IoT 通信功能的物联网核心板或者采用分布式架构，先接一个STM32控制黑盒，然后再连接物联网黑盒，它集成了主控芯片和 NB-IoT 模块，可以直接连接运营商网络。它负责读取传感器、执行逻辑、控制输出，并具备联网能力。\n通信：猪舍没有Wi-Fi，即使有WiFi，也推荐使用RS485线连接，考虑如下：稳定性以及长期收益。布线不方便的地方，可以使用4G或者NBIot模块，用一张物联网卡，可以直接走运营商的网络上传数据，无需在猪舍和住房之间拉网线，年流量费仅需十几元。这是实现“无线化”的关键，需要注意的是，如果在猪舍内加装铁盒，需要接外接天线，防止信号屏蔽。\n报警：前期控制投入，不用手机流量卡和云平台。购买一个433MHz无线门铃，使用黑盒驱动，黑盒使用IP65 防水塑料接线盒，把发射模块接在主控板上。当需要报警时，主控板模拟按下门铃按钮，屋里就会“叮咚”响。这是最直接、最可靠的本地告警。\n第一阶段：从“定时巡”到“听铃响” 目标：解决“夜间需要频繁固定间隔的去猪舍”的核心痛点，将人工巡检间隔时间拉长到“只在有异常时响应”。\n系统架构与原理： 感知：主控板 不断读取 DS18B20 的温度值。\n决策：我在主控板的固件里写死一段简单的判断逻辑（例如：if (温度 \u0026lt; 18度 或 温度 \u0026gt; 28度)）。\n执行：一旦条件满足，主控板 立即驱动 GPIO 引脚，向 433MHz 发射模块发送一个高电平脉冲信号。\n告警：433MHz 信号被屋里的门铃接收器捕获，触发响铃。屋里的人听到铃声，就知道猪舍温度异常，需要去查看。\n可执行性与落地关键： 硬件连接：仅需将 DS18B20 的数据脚、电源、地线接到主控板对应引脚；将 433MHz 发射模块的信号脚接到另一个 GPIO 引脚。接线简单，无需复杂电路。\n供电：猪舍有市电。使用一个手机充电头（5V）和降压模块（给STM32提供3.3V）即可，成本几元钱。\n核心代码：逻辑极其简单，几十行 Arduino 代码即可实现。核心是 DS18B20库读取温度，和 RCSwitch库驱动 433MHz 模块。\n防护：将所有电子元件（主控板、电源模块）装入一个防水塑料盒（如户外接线盒），传感器探头和433MHz 发射模块用热缩管和704胶做好防水，从盒子引出。确保在潮湿的猪舍环境中能长期工作。\n价值：实现了从“人找事”到“事找人”的转变。家人可以安心睡觉，只有门铃响起时才需要起身。这个黑盒用不到50元的成本，解决了最耗费人力的“频繁夜间巡检”问题。\n第二阶段：从“告警”到“初步控制” 在第一阶段稳定运行后，可以低成本增加“自动干预”能力。\n控制电热板：购买一个固态继电器（SSR）​ 模块（约20元）。将电热板的火线剪断，串入 SSR 的输出端。SSR 的控制端接主控板。在温度判断逻辑中增加：if (温度 \u0026lt; 20度) { 开启继电器; } else if (温度 \u0026gt; 25度) { 关闭继电器; }。这样，电热板就能在安全范围内自动启停，防止过热或浪费电。\n需要修改固件，增加控制逻辑，和显示温度逻辑。这里可以购买LCD显示屏或者通过指示灯显示，但是这在猪舍这种环境不太合适。第三阶段给出了解决方案。\n第三阶段：从“单机控制”到“智能换气” 需要购买温湿度传感器，需要另一个黑盒（比如ESP32）,然后通过RS485线从宿舍连到猪舍的黑盒中。\n这个黑盒的作用：\nSTM32黑盒已经实现了猪仔产房的温控逻辑，需要再加装RS485模块和ESP32总控黑盒互联，方便实现下发不同的温控设置温度到每个猪舍，采集每个猪舍温度，用来上传到云端，便于后期分享每个温度对猪仔的成活和生长影响。\n再使用STM32黑盒加装温湿度传感器，控制抽风机，然后通过ESP32总控黑盒设置启动逻辑，比如当温度和湿度达到某个阈值时，启动抽风机，启动时长。保证猪舍内的空气新鲜度。\n当条件允许时，可以增加氨气传感器，实现更科学的联动。\n也非常方便后期扩展，只需加装STM32黑盒就能部署新的猪舍。\n也方便后期根据采集的温度进行统计和分析指定更合理的温度，提供猪仔成活率。\n三、 为什么“便宜”是这里的核心竞争力？ 大型现代化养殖场有造价几十万的环境控制系统，它们确实好，这点无可质疑。但对于存栏几十头、上百头的家庭猪舍，这投入太大，有点不切实际。使用这种 “主控板 + 传感器 + 继电器”​ 黑盒方案，1个总控黑盒+10个分体控制黑盒总硬件成本可以控制在 150元-1500以内（包含线材不包含人工费用）。如果用上云平台和手机报警，每年增加约20元流量费。实际情况需要根据部署方案和购买的零件线材价格来确定。\n它的核心竞争力就是“极致性价比”和“可分解实施”。用户可以从最简单的“温度门铃告警”（\u0026lt;100元）开始，亲眼看到效果，再决定是否增加“自动控温”或“通风联动”功能。每一步的投入都看得见、摸得着，风险极低。\n这样实施之后的好处是：\n提高成活率：只要一套几百元的设备能多救下一头猪仔，设备成本就瞬间收回。 节省药钱：环境稳定，猪仔少生病，省下的药费就是纯利润。 保护人力：身体是最大的本钱，晚上的高质量睡眠能让他们白天更有精力打理其他农活。 四、 证明：黑盒是“活”的架构 这个方案的核心并非某个固化的产品，而是一个可灵活配置的软硬件框架。\n硬件可扩展：主控板 有丰富的 GPIO 和通信接口（I2C, SPI, UART）。今天接温度传感器，明天可以轻松接入土壤湿度传感器（做种植）、电能计量模块（看电耗）。\n逻辑可配置： 黑盒采用执行逻辑，大脑在PC配置工具，可以下发猪舍采集温度，也可以下发抽风机控制，方便后端开发，例如使用APP，网页，甚至小程序，通过云端的数据，开发一个简单的小程序，直接在小程序上设置温度阈值、联动设备等，无需再改代码刷固件。\n通信可升级：当前用433MHz门铃，未来随时可以启用主控板的 NB-IoT 功能，使用MQTT将数据送到物联网平台，也可以使用有线网卡或者光纤通过HTTP、WebSocket、TCP将数据上传到云平台。\n后端开发便捷：配合云平台的前置脚本，后台开发人员拿到数据是直接可读的JSON内容，而不是二进制码，后台下发的控制命令直接可以使用JSON内容下发，而不用使用二进制码。\n思考 技术不应是实验室里的炫技，也不应只是大公司的专利。它更应该像一件趁手的工具，能够以极低的门槛，解决普通人生活中那些具体而微的麻烦。\n从“披上大衣半夜巡栏”到“听见门铃再去查看”，这看似一小步，却是将父辈从重复体力劳动中解放出来的实实在在的一步。用不到一百块钱的成本，证明了“智慧养殖”的种子，同样可以在最朴素的土壤里生根发芽。\n这条路，我将从自家的猪舍开始走起。\n后记与邀请 如果你正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电https://afdian.com/a/modujson​ 持续同步与更新。\n","permalink":"https://blog.91demo.top/embedded/pigsty.html","summary":"\u003ch2 id=\"一-那件披在身上的大衣\"\u003e一、 那件披在身上的大衣\u003c/h2\u003e\n\u003cp\u003e老家人养猪，冬天的半夜，都要披上厚重的大衣，去猪舍看产床温度。猪舍的墙上挂了一个水银温度计。温度低了猪仔会冻死，温度高了会脱水。氨气重了会生病，感觉味道重了，需要手动打开抽风机。这种“靠人巡、靠鼻闻、靠经验”的原始模式，不仅累，而且风险极高。\u003c/p\u003e\n\u003cp\u003e供暖烧煤炉，煤炉需要半夜起来加煤，人困得不行；母猪还得使用电热板，电热板是那种电阻丝加热的，没有温控计，只能隔几个小时去关掉，等温度降下去再打开。说到底，还是得靠人去“看着”。\u003c/p\u003e\n\u003cp\u003e我想，能不能用技术，把家人从这种重复、枯燥且充满风险的体力劳动中解放出来一点点？不用花太多钱，因为他们会心疼。\u003c/p\u003e\n\u003ch2 id=\"二-攻坚方案黑盒的温控逻辑\"\u003e二、 攻坚方案：黑盒的温控逻辑\u003c/h2\u003e\n\u003cp\u003e可以看到面临的主要是“环境失控风险”。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e温度控制的极端性：依靠烧煤和简易电热板，温度分布不均。半夜人工起夜不仅辛苦，且这种“大跨度”的温差变化会导致猪仔腹泻甚至死亡。\u003c/li\u003e\n\u003cli\u003e空气质量的隐形威胁：氨气超标是诱发呼吸道疾病的主因。仅靠“鼻闻”时，空气质量往往已经恶化到危及健康的程度。\u003c/li\u003e\n\u003cli\u003e利润被风险蚕食：养猪大头是料钱和药钱，但成活率才是利润的底线。一次深夜的失误，可能让数月的辛苦付诸东流。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e能马上想到的临时解决方案：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e恒温自动化（解决“半夜加煤”与“手动插拔”）\u003cbr\u003e\n温控传感器方案：购买带有探头的温控开关，将电热板连接到温控插座上。\u003cbr\u003e\n效果：设定好区间（如32-35度），温度低了自动通电，够了自动断电。不仅省去了人工看管，还能通过减少无效耗电抵消设备成本。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e环境预警系统（解决“鼻闻”与“风险”）\u003cbr\u003e\n智能监控终端：安装一个集成了温湿度监控与氨气检测的智能传感器，通过 Wi-Fi 或 4G 信号连接手机。\u003cbr\u003e\n效果：当温度异常或氨气浓度过高时，手机会自动发出警报铃声提醒，避免了老人家整晚不敢闭眼的焦虑。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e联动抽风（解决“呼吸道疾病”）\u003cbr\u003e\n自动排风系统：将原有的抽风机改装为智能联动。\u003cbr\u003e\n效果：当氨气传感器感应到超标，自动开启抽风机，达标后关闭。这能精准减少热量流失，同时保证猪舍空气清新。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"核心硬件选择务实与成本的平衡\"\u003e核心硬件选择：务实与成本的平衡\u003c/h3\u003e\n\u003cp\u003e因为猪舍现场环境非常恶劣，湿度，氨气，粉尘等因素，温控开关坏的频率非常高，如果接到料房，又采集不了温度。购买氨气专业设备成本又非常高。现场可以根据温度和通风进行解决，但在冬季，保暖是另一个问题。实际考量后，还是自己动手做比较划算。零部件能够直接买并满足现场需求的，直接购买，满足不了，自己动手制作，需要防腐蚀处理：焊接完成后，用酒精清洗焊渣，然后必须喷涂“三防漆”。外壳选用 IP65 防水接线盒，进线口使用电缆防水密封接头 (PG7)，否则氨气会从缝隙钻进去。线路外层加PVC管，防鼠咬，腐蚀，机械损伤。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e传感器：水银温度计不行，无法采集。工业级的 RS485 温湿度传感器太贵（动辄上百元），购买不锈钢封装好的民用级的DS18B20数字温度传感器，焊接后要密封好，挂在猪舍合适的位置。它通过一根单总线连接，协议简单，成本极低。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e主控：具备 NB-IoT 通信功能的物联网核心板或者采用分布式架构，先接一个STM32控制黑盒，然后再连接物联网黑盒，它集成了主控芯片和 NB-IoT 模块，可以直接连接运营商网络。它负责读取传感器、执行逻辑、控制输出，并具备联网能力。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e通信：猪舍没有Wi-Fi，即使有WiFi，也推荐使用RS485线连接，考虑如下：稳定性以及长期收益。布线不方便的地方，可以使用4G或者NBIot模块，用一张物联网卡，可以直接走运营商的网络上传数据，无需在猪舍和住房之间拉网线，年流量费仅需十几元。这是实现“无线化”的关键，需要注意的是，如果在猪舍内加装铁盒，需要接外接天线，防止信号屏蔽。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e报警：前期控制投入，不用手机流量卡和云平台。购买一个433MHz无线门铃，使用黑盒驱动，黑盒使用IP65 防水塑料接线盒，把发射模块接在主控板上。当需要报警时，主控板模拟按下门铃按钮，屋里就会“叮咚”响。这是最直接、最可靠的本地告警。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"第一阶段从定时巡到听铃响\"\u003e第一阶段：从“定时巡”到“听铃响”\u003c/h3\u003e\n\u003cp\u003e目标：解决“夜间需要频繁固定间隔的去猪舍”的核心痛点，将人工巡检间隔时间拉长到“只在有异常时响应”。\u003c/p\u003e\n\u003ch4 id=\"系统架构与原理\"\u003e系统架构与原理：\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e感知：主控板 不断读取 DS18B20 的温度值。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e决策：我在主控板的固件里写死一段简单的判断逻辑（例如：if (温度 \u0026lt; 18度 或 温度 \u0026gt; 28度)）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e执行：一旦条件满足，主控板 立即驱动 GPIO 引脚，向 433MHz 发射模块发送一个高电平脉冲信号。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e告警：433MHz 信号被屋里的门铃接收器捕获，触发响铃。屋里的人听到铃声，就知道猪舍温度异常，需要去查看。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"可执行性与落地关键\"\u003e可执行性与落地关键：\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e硬件连接：仅需将 DS18B20 的数据脚、电源、地线接到主控板对应引脚；将 433MHz 发射模块的信号脚接到另一个 GPIO 引脚。接线简单，无需复杂电路。\u003c/p\u003e","title":"从“半夜巡栏”到“智能换气”：我把黑盒搬进了猪舍"},{"content":"一、 场景：从“插卡洗澡”到“手机充值”的断层 “洗澡洗一半，卡里没钱了，得湿着身子跑下楼去圈存机充钱。”——这是十年前大学宿舍的常态。\n我参与过那个“刷卡时代”的项目。彼时，每个宿舍楼都有一个弱电井，里面几十个水表通过RS-485总线串联。学生用M1卡洗澡，钱存在卡里，水表是“单机版”。宿管中心不知道哪个宿舍快没水了，只能被动等待报修。\n核心痛点：用户需要手机充值的便捷，但海量的老式水表（及电表、热表）是“数字孤岛”，只有RS-485接口，不具备任何联网能力。整体更换为物联网表成本极高，且施工影响巨大。\n二、 方案：一个“黑盒”，唤醒沉默的数据 我们的方案是添加一个 “黑盒”——一个协议转换网关。它的逻辑很简单：不换表，不改线，只做“翻译官”。\n物理部署：在弱电井的485总线汇接处，并联接入“黑盒”。原有的刷卡系统完全保留，作为保底。\n核心工作： 采集：黑盒内置高性能Modbus协议栈，主动轮询总线上所有水表，读取余额、用量。\n转换：将不同品牌、不同协议的水表数据，统一“翻译”成标准的JSON格式。\n上传：通过4G或网线，将数据通过MQTT协议上传至云平台。\n手机充值闭环： 学生在小程序支付。\n云端将“为XX房号充值XX元”的指令下发给对应黑盒。\n关键一步：黑盒将指令“反向翻译”成目标水表能识别的、符合其私有协议的485报文，完成“写卡”操作。\n系统确认后，充值到账。\n三、 定位：在巨头缝隙中，做“数字化的梯子” 我深知，国家级、城市级的智慧水务/能源项目，是头部玩家的“铁桶阵”，他们依靠专用网络、5G和强大的工程能力构建壁垒。\n“黑盒”的目标，是那片被忽略的“长尾市场”：\n还未改造的高校宿舍、企业公寓、公租房保留的水表。\n城中村的个体房东，管理几栋到十几栋楼房。\n中小工厂的宿舍楼。\n对他们而言，动辄数十万的整体改造方案是不可承受之重。而一个单价数百元、即插即用、半天可部署、不动原有设施的“黑盒”，是他们迈入数字化管理的唯一可行阶梯。\n四、 挑战与壁垒：并非“即插即用”的童话 这个方案在逻辑上自洽，但真实的商业落地远非易事，存在多重壁垒：\n工程实施壁垒：“不换表”不等于“零施工”。将黑盒接入弱电井，需要开井、找线、破接、取电、固定，这本身就有一定的技术门槛和安全风险，并非普通用户能独立完成。\n协议适配壁垒：水、电、热表协议各异，且大量是厂家私有加密协议。适配、测试每个新协议，都意味着高昂的研发和现场调试成本。这绝非一个“通用字典”就能轻松解决。\n性能与稳定性壁垒：RS-485总线是半双工，挂载几十块表后，轮询周期会拉长，数据实时性下降。网络波动、设备干扰可能导致指令执行失败，需要设计复杂的重试、容错和事务一致性机制。\n渠道与商务壁垒：如何触达并说服高校后勤、房东这些分散的客户？如何提供及时可靠的现场支持？这比技术开发更难。\n五、 思考：技术应俯身解决真问题 从弱电井里满是灰尘的RS-485总线，到云端的MQTT消息；从易丢失的实体卡，到手机里的数字账户。\n“黑盒”的价值，不在于它用了多炫的技术，而在于它用极低的成本，为一个真实、广泛但被忽视的需求，提供了一个可行的解决方案。\n它像一根“数字化的梯子”，让那些无力承担“电梯”费用的用户，也能攀上智能管理的台阶。\n这背后，是对现场通信“脾气”（干扰、雷击、协议冲突）的深刻理解，更是对用户“简单、可靠、别添乱”这一终极诉求的敬畏。\n当巨头们仰望星空，构建未来时，总需要一些人，愿意为角落里那些“老旧笨重”的设备，插上一双通往现代的翅膀。\n这条路充满挑战，但正因如此，每一步才都踏在真实的需求之上。\n后记与邀请 如果你正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电https://afdian.com/a/modujson​ 持续同步与更新。\n","permalink":"https://blog.91demo.top/embedded/watermeter.html","summary":"\u003ch2 id=\"一-场景从插卡洗澡到手机充值的断层\"\u003e一、 场景：从“插卡洗澡”到“手机充值”的断层\u003c/h2\u003e\n\u003cp\u003e“洗澡洗一半，卡里没钱了，得湿着身子跑下楼去圈存机充钱。”——这是十年前大学宿舍的常态。\u003c/p\u003e\n\u003cp\u003e我参与过那个“刷卡时代”的项目。彼时，每个宿舍楼都有一个弱电井，里面几十个水表通过RS-485总线串联。学生用M1卡洗澡，钱存在卡里，水表是“单机版”。宿管中心不知道哪个宿舍快没水了，只能被动等待报修。\u003c/p\u003e\n\u003cp\u003e核心痛点：用户需要手机充值的便捷，但海量的老式水表（及电表、热表）是“数字孤岛”，只有RS-485接口，不具备任何联网能力。整体更换为物联网表成本极高，且施工影响巨大。\u003c/p\u003e\n\u003ch2 id=\"二-方案一个黑盒唤醒沉默的数据\"\u003e二、 方案：一个“黑盒”，唤醒沉默的数据\u003c/h2\u003e\n\u003cp\u003e我们的方案是添加一个 “黑盒”——一个协议转换网关。它的逻辑很简单：不换表，不改线，只做“翻译官”。\u003c/p\u003e\n\u003cp\u003e物理部署：在弱电井的485总线汇接处，并联接入“黑盒”。原有的刷卡系统完全保留，作为保底。\u003c/p\u003e\n\u003ch3 id=\"核心工作\"\u003e核心工作：\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e采集：黑盒内置高性能Modbus协议栈，主动轮询总线上所有水表，读取余额、用量。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e转换：将不同品牌、不同协议的水表数据，统一“翻译”成标准的JSON格式。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e上传：通过4G或网线，将数据通过MQTT协议上传至云平台。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"手机充值闭环\"\u003e手机充值闭环：\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e学生在小程序支付。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e云端将“为XX房号充值XX元”的指令下发给对应黑盒。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e关键一步：黑盒将指令“反向翻译”成目标水表能识别的、符合其私有协议的485报文，完成“写卡”操作。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e系统确认后，充值到账。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三-定位在巨头缝隙中做数字化的梯子\"\u003e三、 定位：在巨头缝隙中，做“数字化的梯子”\u003c/h2\u003e\n\u003cp\u003e我深知，国家级、城市级的智慧水务/能源项目，是头部玩家的“铁桶阵”，他们依靠专用网络、5G和强大的工程能力构建壁垒。\u003c/p\u003e\n\u003cp\u003e“黑盒”的目标，是那片被忽略的“长尾市场”：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e还未改造的高校宿舍、企业公寓、公租房保留的水表。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e城中村的个体房东，管理几栋到十几栋楼房。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e中小工厂的宿舍楼。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e对他们而言，动辄数十万的整体改造方案是不可承受之重。而一个单价数百元、即插即用、半天可部署、不动原有设施的“黑盒”，是他们迈入数字化管理的唯一可行阶梯。\u003c/p\u003e\n\u003ch2 id=\"四-挑战与壁垒并非即插即用的童话\"\u003e四、 挑战与壁垒：并非“即插即用”的童话\u003c/h2\u003e\n\u003cp\u003e这个方案在逻辑上自洽，但真实的商业落地远非易事，存在多重壁垒：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e工程实施壁垒：“不换表”不等于“零施工”。将黑盒接入弱电井，需要开井、找线、破接、取电、固定，这本身就有一定的技术门槛和安全风险，并非普通用户能独立完成。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e协议适配壁垒：水、电、热表协议各异，且大量是厂家私有加密协议。适配、测试每个新协议，都意味着高昂的研发和现场调试成本。这绝非一个“通用字典”就能轻松解决。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e性能与稳定性壁垒：RS-485总线是半双工，挂载几十块表后，轮询周期会拉长，数据实时性下降。网络波动、设备干扰可能导致指令执行失败，需要设计复杂的重试、容错和事务一致性机制。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e渠道与商务壁垒：如何触达并说服高校后勤、房东这些分散的客户？如何提供及时可靠的现场支持？这比技术开发更难。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"五-思考技术应俯身解决真问题\"\u003e五、 思考：技术应俯身解决真问题\u003c/h2\u003e\n\u003cp\u003e从弱电井里满是灰尘的RS-485总线，到云端的MQTT消息；从易丢失的实体卡，到手机里的数字账户。\u003c/p\u003e\n\u003cp\u003e“黑盒”的价值，不在于它用了多炫的技术，而在于它用极低的成本，为一个真实、广泛但被忽视的需求，提供了一个可行的解决方案。\u003c/p\u003e\n\u003cp\u003e它像一根“数字化的梯子”，让那些无力承担“电梯”费用的用户，也能攀上智能管理的台阶。\u003c/p\u003e\n\u003cp\u003e这背后，是对现场通信“脾气”（干扰、雷击、协议冲突）的深刻理解，更是对用户“简单、可靠、别添乱”这一终极诉求的敬畏。\u003c/p\u003e\n\u003cp\u003e当巨头们仰望星空，构建未来时，总需要一些人，愿意为角落里那些“老旧笨重”的设备，插上一双通往现代的翅膀。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这条路充满挑战，但正因如此，每一步才都踏在真实的需求之上。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"后记与邀请\"\u003e后记与邀请\u003c/h2\u003e\n\u003cp\u003e如果你正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电\u003ca href=\"https://afdian.com/a/modujson\"\u003ehttps://afdian.com/a/modujson\u003c/a\u003e​ 持续同步与更新。\u003c/p\u003e","title":"水表、电表、热表：一个“黑盒”如何撬动千亿级存量市场中的利旧改造细分蓝海"},{"content":" “以前看粮仓，那是体力活，现在看粮仓，是技术活。”——一位在粮库工作三十年的老保管员\n一、那些年，我在粮库“插钎子” “我年轻时，最怕夏天查粮仓。”老保管员回忆道。三伏天，他得扛着十几斤重、6米长的铁杆，踩着晃晃悠悠的木板，深一脚浅一脚地爬上几米高的粮堆。杆子前端是温度传感器，杆子末端则是一个巴掌大的小型LED显示屏。每到一个检测点，他就得用力把这“铁签子”插进压得结实的粮堆底部，然后低头查看杆子末端的屏幕读数，再抄录到本子上。全部查完、记录完，要爬一整天。粉尘呛人、闷热缺氧，是家常便饭。\n“那时靠的是腿，是眼，是经验。”老保管员说得对。他练就了“一看、二闻、三摸”的本事，但这套经验在几千吨的粮堆面前，总有盲区。一旦某个角落的粮食开始发热霉变，等人工发现时，往往损失已不可避免。\n这不是故事，这是我的朋友——一位粮库测温系统的实施工程师——告诉我的：\n他入行时，老保管员的手艺活已是过去式。他面对的是更“现代”的麻烦：部署一套有线测温系统。\n这套系统的核心，是把带有固定传感器的“测温电缆”像神经一样埋入粮堆，取代人工巡检。而他的工作，就是把这套“神经”铺进去，再把“信号”引出来。\n这活儿，一点不比爬粮堆轻松。\n夏天，粮仓内像个蒸笼，地面温度超过40度。他要在入粮前，或趁着粮堆还不高时，拖着几十米长、拇指粗的测温电缆在仓内布线。电缆里包裹着钢丝，死沉。他得把它沿着预定路径铺好、固定，确保每个传感器都在设计位置。\n这仅仅是开始。真正的考验是“拉线”。\n粮堆深处的测温电缆，其线头最终要汇集到挂在仓库墙壁或柱子上的“测温分机”（采集器）。他需要扛着梯子，在满是灰尘的仓房里，将几十根从粮堆里引出的线，一根根对号入座，拧到分机箱内的接线端子上。接错一根，整个回路的数据都可能乱掉。\n这还没完。仓库里几十台这样的分机，还需要用更粗的“通信总线”（RS-485线）手拉手串联起来，最后拉到机房，接入一台专用电脑。\n这，才是他口中“从杆子末端拉出数据线，连接到远处的分线器上”的真实图景：​ 那不是在粮堆上现插现拉的潇洒，而是在高温、高粉尘环境下，重复千百次的枯燥、负重且必须精准的体力与细心活。线要拉得横平竖直，接头要拧得牢靠，还要防鼠咬、防磨损。\n刚开始，他觉得这工作充满了技术仪式感。干久了，日复一日地扛电缆、爬高、接线、测通断，只剩下一个字的感受：累。一种被庞杂的物理线缆和复杂现场环境反复折磨的、沉甸甸的疲惫。\n然而，正是这种疲惫，让他对“可靠”二字有了刻骨的理解。他见识过雷雨天后烧毁一片的电路板，处理过因一个接头松动导致整仓数据瘫痪的故障，也深知那台机房里孤零零的电脑，就是整个系统最脆弱的“大脑”。\n二、技术跃迁：从“有线”到“无线” 我朋友后来参与实施的，正是那套用来取代人工的有线测温系统。它的核心是用预埋测温电缆取代了人工铁钎——技术人员在粮堆中预埋专用电缆，每根电缆上集成数十个温度传感器，像“神经末梢”一样分布。数据通过RS-485总线手拉手串联，即设备像糖葫芦一样串在一条总线上，一个接一个。最终汇总到仓外的监控主机。\n除了传统的平房仓，现代化的粮库越来越多地采用立筒仓（圆仓）。这种仓体高大，粮食像瀑布一样从顶部灌入，依靠底部的锥形漏斗和出粮机实现自动化出粮。\n在这里，测温电缆不再是“蛇形”铺在底部，而是像垂直的琴弦一样，从仓顶的采集器一直垂到仓底。采集器不再藏在灰尘满布的机房，而是直接安装在仓顶的防雨箱里，通过无线模块将数据发送到云端。\n这种结构，让粮仓从“体力活”彻底变成了“自动化工厂”。\n再回到我朋友的测温系统，这解决了人工爬仓的问题，但带来了新的、更复杂的麻烦。\n当时觉得这套系统很先进，但现在回想，它太“重”了：\n怕雷击：几百米纵横交错的485总线，成了感应雷的“引雷针”，雷雨季后，分线器经常烧坏一片。\n维护难：专用的监控电脑不能死机，SQL数据库一旦损坏，几年的历史温度数据可能全丢。\n数据孤岛：粮库主任想看一眼温度，必须跑到那个满是灰尘的专用监控室。\n工程浩大：布线、穿管、调试，一个仓的改造就需要数周，成本高昂。\n真正的变革，发生在物联网和无线通信普及之后。\n新一代的“黑盒”采集器出现了。它只有巴掌大小，却集成了主控、4G模块和无线传输功能。它直接读取测温电缆的数据，通过MQTT协议将数据打包成JSON格式，一跳直达云平台。\n粮库管理员在办公室，甚至用手机，就能看到整个粮堆的立体温度云图。系统自动分析、自动报警。\n免布线：每个仓门口挂一个4G黑盒，直接对接原有的分线器。省掉了跨仓位、跨建筑的数千米复杂布线工程。\n云端大脑：不再需要机房那台脆弱的专用电脑，数据直接上云，永不丢失，随时随地可查。\n三、黑盒的定位：避开红海，寻找蓝海 在深入了解行业后，我发现粮储智能化其实是一个典型的“双轨制市场”。\n大公司的“铁桶阵”：资质与工程的护城河 国家级粮储库、大型央企的智能化改造，是标准的“重工程”市场。它被几家头部企业垄断，依靠的是强大的工程化资质（如机电安装、系统集成）和专用设备（如光纤测温、工业级5G网关）。这些方案性能极高，但成本也极其昂贵（单仓改造费用动辄数十万），且施工周期长，需要专业工程队进场。\n我的“黑盒”逻辑：低成本与快速响应 我开发的这个黑盒项目（基于ESP32等开源硬件），从一开始就没打算去冲击那个“铁桶阵”。我清醒地认识到，在需要军工级可靠性的战略储备库，我的方案无法与那些天价的专用设备竞争。\n黑盒的核心价值在于“降维打击”与“普惠”：\n目标市场：中小型粮库、地方储备点、合作社、个体储粮户。这些场景对成本极度敏感，无法承担大型工程的费用，也无需那么严苛的可靠性。\n核心优势：极致低成本（硬件成本可能只有大公司方案的百分之一）和极速部署（即插即用，一个人半小时就能装好一个仓）。\n价值主张：用“够用”的可靠性，解决“有没有”的问题。对于绝大多数只需要基本温湿度监控、异常报警功能的用户，黑盒是让他们迈入数字化门槛的唯一可行选择。\n结语：让技术回归解决问题的本质 技术不应只是大公司的专利和豪华包装下的奢侈品。我的黑盒项目，源于“插钎子”的汗水，目标是证明一件事：在那些大公司不愿弯腰、传统方案贵得用不起的“边缘地带”，廉价的芯片和开源的智慧，同样能默默守护好每一粒粮食。\n这不仅是技术实现，更是一种让技术回归普惠、解决真问题的尝试。\n四、我的“黑盒”：从痛点里长出来的解决方案 “虽然已离开粮储行业多年，但那些亲自插下的钎子，让我深刻理解了RS485总线的‘脾气’、雷击的刁钻、还有现场对‘简单可靠’的极致渴望。”——这种浸入式的场景理解，是我开发这款“黑盒”固件最硬的底气。\n它不是一个实验室里的玩具，而是为了解决 “老旧工业设备低成本上云”​ 这个具体痛点而生的 “数字化插件”​ 。它目前是一个稳定的Modbus/RS485转MQTT网关固件，未来可以扩展为支持多种协议、具备边缘计算能力的核心。\n未来黑盒可以集成LoRa，LoRa 最大的优势在于极强的穿透能力和超长的传输距离，非常适合“密闭、大跨度”的粮仓环境。在粮仓这种密闭、充满金属设备和粮堆的环境下，LoRa 依然表现出色。相比于 4G/NB-IoT 在密闭环境信号较弱的特点，LoRa 的信号能够穿透厚厚的墙体，深入到粮堆内部。配合无线测温探杆，使用电池以及太阳能电池板，彻底免布线。\n关于合作： 我个人专注于核心固件的研发、算法优化与开源社区建设。硬件生产与制造，我与专业的工业设计伙伴合作，以确保设备的出厂品质。我相信，专业的人做专业的事，才能把产品做到最好。\n最终结语 从“汗滴禾下土”的插钎巡检，到“数据云中走”的智能值守，变的不仅仅是工具，更是守护粮食安全的思维与模式。\n我朋友的故事，可能只是大时代里一个很小的注脚。但如果这个源于亲身痛苦、成于开源技术的“黑盒”项目，能帮助到某个正在为粮仓温控发愁的中小业主，或者给某个在工业物联网领域摸索的开发者一点启发，那么，那些年在粮堆上流过的汗，就算有了超越它本身的价值。\n技术，理应如此踏实而有温度。\n后记与邀请 如果你也有过类似的工业现场“血泪史”，或正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电https://afdian.com/a/modujson​ 持续同步与更新。\n","permalink":"https://blog.91demo.top/embedded/granary.html","summary":"\u003cblockquote\u003e\n\u003cp\u003e“以前看粮仓，那是体力活，现在看粮仓，是技术活。”——一位在粮库工作三十年的老保管员\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一那些年我在粮库插钎子\"\u003e一、那些年，我在粮库“插钎子”\u003c/h2\u003e\n\u003cp\u003e“我年轻时，最怕夏天查粮仓。”老保管员回忆道。三伏天，他得扛着十几斤重、6米长的铁杆，踩着晃晃悠悠的木板，深一脚浅一脚地爬上几米高的粮堆。杆子前端是温度传感器，杆子末端则是一个巴掌大的小型LED显示屏。每到一个检测点，他就得用力把这“铁签子”插进压得结实的粮堆底部，然后低头查看杆子末端的屏幕读数，再抄录到本子上。全部查完、记录完，要爬一整天。粉尘呛人、闷热缺氧，是家常便饭。\u003c/p\u003e\n\u003cp\u003e“那时靠的是腿，是眼，是经验。”老保管员说得对。他练就了“一看、二闻、三摸”的本事，但这套经验在几千吨的粮堆面前，总有盲区。一旦某个角落的粮食开始发热霉变，等人工发现时，往往损失已不可避免。\u003c/p\u003e\n\u003cp\u003e这不是故事，这是我的朋友——一位粮库测温系统的实施工程师——告诉我的：\u003c/p\u003e\n\u003cp\u003e他入行时，老保管员的手艺活已是过去式。他面对的是更“现代”的麻烦：部署一套有线测温系统。\u003c/p\u003e\n\u003cp\u003e这套系统的核心，是把带有固定传感器的“测温电缆”像神经一样埋入粮堆，取代人工巡检。而他的工作，就是把这套“神经”铺进去，再把“信号”引出来。\u003c/p\u003e\n\u003cp\u003e这活儿，一点不比爬粮堆轻松。\u003c/p\u003e\n\u003cp\u003e夏天，粮仓内像个蒸笼，地面温度超过40度。他要在入粮前，或趁着粮堆还不高时，拖着几十米长、拇指粗的测温电缆在仓内布线。电缆里包裹着钢丝，死沉。他得把它沿着预定路径铺好、固定，确保每个传感器都在设计位置。\u003c/p\u003e\n\u003cp\u003e这仅仅是开始。真正的考验是“拉线”。\u003c/p\u003e\n\u003cp\u003e粮堆深处的测温电缆，其线头最终要汇集到挂在仓库墙壁或柱子上的“测温分机”（采集器）。他需要扛着梯子，在满是灰尘的仓房里，将几十根从粮堆里引出的线，一根根对号入座，拧到分机箱内的接线端子上。接错一根，整个回路的数据都可能乱掉。\u003c/p\u003e\n\u003cp\u003e这还没完。仓库里几十台这样的分机，还需要用更粗的“通信总线”（RS-485线）手拉手串联起来，最后拉到机房，接入一台专用电脑。\u003c/p\u003e\n\u003cp\u003e这，才是他口中“从杆子末端拉出数据线，连接到远处的分线器上”的真实图景：​ 那不是在粮堆上现插现拉的潇洒，而是在高温、高粉尘环境下，重复千百次的枯燥、负重且必须精准的体力与细心活。线要拉得横平竖直，接头要拧得牢靠，还要防鼠咬、防磨损。\u003c/p\u003e\n\u003cp\u003e刚开始，他觉得这工作充满了技术仪式感。干久了，日复一日地扛电缆、爬高、接线、测通断，只剩下一个字的感受：累。一种被庞杂的物理线缆和复杂现场环境反复折磨的、沉甸甸的疲惫。\u003c/p\u003e\n\u003cp\u003e然而，正是这种疲惫，让他对“可靠”二字有了刻骨的理解。他见识过雷雨天后烧毁一片的电路板，处理过因一个接头松动导致整仓数据瘫痪的故障，也深知那台机房里孤零零的电脑，就是整个系统最脆弱的“大脑”。\u003c/p\u003e\n\u003ch2 id=\"二技术跃迁从有线到无线\"\u003e二、技术跃迁：从“有线”到“无线”\u003c/h2\u003e\n\u003cp\u003e我朋友后来参与实施的，正是那套用来取代人工的有线测温系统。它的核心是用预埋测温电缆取代了人工铁钎——技术人员在粮堆中预埋专用电缆，每根电缆上集成数十个温度传感器，像“神经末梢”一样分布。数据通过RS-485总线手拉手串联，即设备像糖葫芦一样串在一条总线上，一个接一个。最终汇总到仓外的监控主机。\u003c/p\u003e\n\u003cp\u003e除了传统的平房仓，现代化的粮库越来越多地采用立筒仓（圆仓）。这种仓体高大，粮食像瀑布一样从顶部灌入，依靠底部的锥形漏斗和出粮机实现自动化出粮。\u003c/p\u003e\n\u003cp\u003e在这里，测温电缆不再是“蛇形”铺在底部，而是像垂直的琴弦一样，从仓顶的采集器一直垂到仓底。采集器不再藏在灰尘满布的机房，而是直接安装在仓顶的防雨箱里，通过无线模块将数据发送到云端。\u003c/p\u003e\n\u003cp\u003e这种结构，让粮仓从“体力活”彻底变成了“自动化工厂”。\u003c/p\u003e\n\u003cp\u003e再回到我朋友的测温系统，这解决了人工爬仓的问题，但带来了新的、更复杂的麻烦。\u003c/p\u003e\n\u003cp\u003e当时觉得这套系统很先进，但现在回想，它太“重”了：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e怕雷击：几百米纵横交错的485总线，成了感应雷的“引雷针”，雷雨季后，分线器经常烧坏一片。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e维护难：专用的监控电脑不能死机，SQL数据库一旦损坏，几年的历史温度数据可能全丢。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e数据孤岛：粮库主任想看一眼温度，必须跑到那个满是灰尘的专用监控室。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e工程浩大：布线、穿管、调试，一个仓的改造就需要数周，成本高昂。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e真正的变革，发生在物联网和无线通信普及之后。\u003c/p\u003e\n\u003cp\u003e新一代的“黑盒”采集器出现了。它只有巴掌大小，却集成了主控、4G模块和无线传输功能。它直接读取测温电缆的数据，通过MQTT协议将数据打包成JSON格式，一跳直达云平台。\u003c/p\u003e\n\u003cp\u003e粮库管理员在办公室，甚至用手机，就能看到整个粮堆的立体温度云图。系统自动分析、自动报警。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e免布线：每个仓门口挂一个4G黑盒，直接对接原有的分线器。省掉了跨仓位、跨建筑的数千米复杂布线工程。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e云端大脑：不再需要机房那台脆弱的专用电脑，数据直接上云，永不丢失，随时随地可查。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三黑盒的定位避开红海寻找蓝海\"\u003e三、黑盒的定位：避开红海，寻找蓝海\u003c/h2\u003e\n\u003cp\u003e在深入了解行业后，我发现粮储智能化其实是一个典型的“双轨制市场”。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e大公司的“铁桶阵”：资质与工程的护城河\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e国家级粮储库、大型央企的智能化改造，是标准的“重工程”市场。它被几家头部企业垄断，依靠的是强大的工程化资质（如机电安装、系统集成）和专用设备（如光纤测温、工业级5G网关）。这些方案性能极高，但成本也极其昂贵（单仓改造费用动辄数十万），且施工周期长，需要专业工程队进场。\u003c/p\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e我的“黑盒”逻辑：低成本与快速响应\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我开发的这个黑盒项目（基于ESP32等开源硬件），从一开始就没打算去冲击那个“铁桶阵”。我清醒地认识到，在需要军工级可靠性的战略储备库，我的方案无法与那些天价的专用设备竞争。\u003c/p\u003e\n\u003cp\u003e黑盒的核心价值在于“降维打击”与“普惠”：\u003c/p\u003e\n\u003cp\u003e目标市场：中小型粮库、地方储备点、合作社、个体储粮户。这些场景对成本极度敏感，无法承担大型工程的费用，也无需那么严苛的可靠性。\u003c/p\u003e\n\u003cp\u003e核心优势：极致低成本（硬件成本可能只有大公司方案的百分之一）和极速部署（即插即用，一个人半小时就能装好一个仓）。\u003c/p\u003e\n\u003cp\u003e价值主张：用“够用”的可靠性，解决“有没有”的问题。对于绝大多数只需要基本温湿度监控、异常报警功能的用户，黑盒是让他们迈入数字化门槛的唯一可行选择。\u003c/p\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e结语：让技术回归解决问题的本质\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e技术不应只是大公司的专利和豪华包装下的奢侈品。我的黑盒项目，源于“插钎子”的汗水，目标是证明一件事：在那些大公司不愿弯腰、传统方案贵得用不起的“边缘地带”，廉价的芯片和开源的智慧，同样能默默守护好每一粒粮食。\u003c/p\u003e\n\u003cp\u003e这不仅是技术实现，更是一种让技术回归普惠、解决真问题的尝试。\u003c/p\u003e\n\u003ch2 id=\"四我的黑盒从痛点里长出来的解决方案\"\u003e四、我的“黑盒”：从痛点里长出来的解决方案\u003c/h2\u003e\n\u003cp\u003e“虽然已离开粮储行业多年，但那些亲自插下的钎子，让我深刻理解了RS485总线的‘脾气’、雷击的刁钻、还有现场对‘简单可靠’的极致渴望。”——这种浸入式的场景理解，是我开发这款“黑盒”固件最硬的底气。\u003c/p\u003e\n\u003cp\u003e它不是一个实验室里的玩具，而是为了解决 “老旧工业设备低成本上云”​ 这个具体痛点而生的 “数字化插件”​ 。它目前是一个稳定的Modbus/RS485转MQTT网关固件，未来可以扩展为支持多种协议、具备边缘计算能力的核心。\u003c/p\u003e\n\u003cp\u003e未来黑盒可以集成LoRa，LoRa 最大的优势在于极强的穿透能力和超长的传输距离，非常适合“密闭、大跨度”的粮仓环境。在粮仓这种密闭、充满金属设备和粮堆的环境下，LoRa 依然表现出色。相比于 4G/NB-IoT 在密闭环境信号较弱的特点，LoRa 的信号能够穿透厚厚的墙体，深入到粮堆内部。配合无线测温探杆，使用电池以及太阳能电池板，彻底免布线。\u003c/p\u003e\n\u003ch2 id=\"关于合作\"\u003e关于合作：\u003c/h2\u003e\n\u003cp\u003e我个人专注于核心固件的研发、算法优化与开源社区建设。硬件生产与制造，我与专业的工业设计伙伴合作，以确保设备的出厂品质。我相信，专业的人做专业的事，才能把产品做到最好。\u003c/p\u003e\n\u003ch2 id=\"最终结语\"\u003e最终结语\u003c/h2\u003e\n\u003cp\u003e从“汗滴禾下土”的插钎巡检，到“数据云中走”的智能值守，变的不仅仅是工具，更是守护粮食安全的思维与模式。\u003c/p\u003e\n\u003cp\u003e我朋友的故事，可能只是大时代里一个很小的注脚。但如果这个源于亲身痛苦、成于开源技术的“黑盒”项目，能帮助到某个正在为粮仓温控发愁的中小业主，或者给某个在工业物联网领域摸索的开发者一点启发，那么，那些年在粮堆上流过的汗，就算有了超越它本身的价值。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e技术，理应如此踏实而有温度。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"后记与邀请\"\u003e后记与邀请\u003c/h2\u003e\n\u003cp\u003e如果你也有过类似的工业现场“血泪史”，或正在寻找低成本的老旧设备数字化方案，欢迎交流。关于“黑盒”项目的固件核心进展、协议文档与配置字典，我将在爱发电\u003ca href=\"https://afdian.com/a/modujson\"\u003ehttps://afdian.com/a/modujson\u003c/a\u003e​ 持续同步与更新。\u003c/p\u003e","title":"从粮仓 RS485 总线到云端 JSON：一个前实施工程师的数字化反思"},{"content":"一、 项目愿景：让设备上云，像接线一样简单 新的一年，新的计划，我们致力于为工程师、创客及小团队，打造一套开箱即用、稳定可靠的工业物联网数据采集方案。你无需深究复杂的协议栈，只需聚焦业务，即可让传统设备轻松上云。\n本项目的核心价值在于：\n协议翻译官：打造一个高性能的“中间件”，将底层的Modbus协议（支持RS485/TTL）无缝转换为上层的MQTT协议，打通设备与云端的“最后一公里”。 软硬解耦：不绑定特定硬件。无论是WiFi环境（ESP32）还是4G环境（Cat.1模组），只要具备基本通信能力，软件就能赋予其“智能”，提供极大的硬件选型自由度。 解决技术断层：解决有业务需求但没有研发实力的小团队（如硬件工程师、创客、小厂），本项目提供了一种低成本解决方案。它只需通用开发板，就能实现数据采集和上云，花小钱办大事。 二、 技术架构：三位一体的全栈实现 为确保方案的极致稳定与高度可扩展，我们采用“固件 + 配置工具 + 演示端”的三位一体架构，确保方案的完整性与专业性。\n核心固件（“翻译官” - 逻辑版） 固件架构：采用分层架构设计，硬件抽象层隔离具体芯片与通信接口（如RS485/TTL），实现核心逻辑与硬件的解耦；协议栈层实现完整的Modbus协议栈（主/从站，RTU/TCP），并进行高效、健壮的JSON封装；服务层内置MQTT客户端，负责可靠的数据上传、断线重连与本地缓存；配置管理层，通过Rust编写的PC工具下发设备配置，实现软件定义功能。\n技术栈：采用C语言开发，优先实现裸机（No OS）逻辑版本，确保在资源受限的MCU上也能稳定运行，降低用户使用门槛。\n功能：负责实时采集Modbus寄存器数据（如电压、电流），并根据配置的倍率进行数据转换，最后通过MQTT协议将数据打包上传。\nPC配置工具（“指挥官” - Rust版） 技术栈：使用Rust语言开发，利用其内存安全特性，确保配置过程绝对可靠，工具长期运行不崩溃。\n功能：提供图形化界面，用户可轻松配置通信模式、寄存器地址、倍率等参数。支持通过串口（USB转TTL） 进行固件刷新与参数下发，解决现场实施难题，无需复杂的网络配置。\n数据演示端（“仪表盘”） 技术栈：微信小程序。\n功能：扫码即可查看实时数据曲线与数值面板，直观验证数据上云效果，方便现场调试与演示。\n三、 当前状态：诚邀你，成为“共创者” 项目正处于核心研发阶段，当前全力攻克裸机版本，实现通信方式支持WiFi和4G的Modbus RTU（串口）协议的稳定性与性能。它适合简单应用，保证运行稳定。我们相信，最好的产品源于真实场景的千锤百炼。\n未来会研发通信方式支持LoRa、以太网的Modbus TCP，以及FreeRTOS多任务版，用于解决需要同时处理多路数据采集，复杂业务逻辑的高端场景。\n因此，我们发起此次共创计划：\n你的支持，将直接转化为测试硬件（ESP32、各类Modbus传感器），用于极限环境下的兼容性验证。 你的设备，可以寄来成为我们的“适配样本”，共同完善设备库。 你的反馈，将直接塑造产品的下一个版本。 四、未来承诺 当固件通过大量真实场景验证，达到工业级稳定标准后，我们将正式申请软著，并启动授权售卖。所有共创阶段的支持者，都将依据历史贡献，获得丰厚的升级折扣与永久优先支持权。\n我入驻了爱发电，更多详情内容请查看：https://afdian.com/a/modujson\n","permalink":"https://blog.91demo.top/embedded/modujson.html","summary":"\u003ch2 id=\"一-项目愿景让设备上云像接线一样简单\"\u003e一、 项目愿景：让设备上云，像接线一样简单\u003c/h2\u003e\n\u003cp\u003e新的一年，新的计划，我们致力于为工程师、创客及小团队，打造一套开箱即用、稳定可靠的工业物联网数据采集方案。你无需深究复杂的协议栈，只需聚焦业务，即可让传统设备轻松上云。\u003c/p\u003e\n\u003cp\u003e本项目的核心价值在于：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e协议翻译官：打造一个高性能的“中间件”，将底层的Modbus协议（支持RS485/TTL）无缝转换为上层的MQTT协议，打通设备与云端的“最后一公里”。\u003c/li\u003e\n\u003cli\u003e软硬解耦：不绑定特定硬件。无论是WiFi环境（ESP32）还是4G环境（Cat.1模组），只要具备基本通信能力，软件就能赋予其“智能”，提供极大的硬件选型自由度。\u003c/li\u003e\n\u003cli\u003e解决技术断层：解决有业务需求但没有研发实力的小团队（如硬件工程师、创客、小厂），本项目提供了一种低成本解决方案。它只需通用开发板，就能实现数据采集和上云，花小钱办大事。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"二-技术架构三位一体的全栈实现\"\u003e二、 技术架构：三位一体的全栈实现\u003c/h2\u003e\n\u003cp\u003e为确保方案的极致稳定与高度可扩展，我们采用“固件 + 配置工具 + 演示端”的三位一体架构，确保方案的完整性与专业性。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e核心固件（“翻译官” - 逻辑版）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e固件架构：采用分层架构设计，硬件抽象层隔离具体芯片与通信接口（如RS485/TTL），实现核心逻辑与硬件的解耦；协议栈层实现完整的Modbus协议栈（主/从站，RTU/TCP），并进行高效、健壮的JSON封装；服务层内置MQTT客户端，负责可靠的数据上传、断线重连与本地缓存；配置管理层，通过Rust编写的PC工具下发设备配置，实现软件定义功能。\u003c/p\u003e\n\u003cp\u003e技术栈：采用C语言开发，优先实现裸机（No OS）逻辑版本，确保在资源受限的MCU上也能稳定运行，降低用户使用门槛。\u003c/p\u003e\n\u003cp\u003e功能：负责实时采集Modbus寄存器数据（如电压、电流），并根据配置的倍率进行数据转换，最后通过MQTT协议将数据打包上传。\u003c/p\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003ePC配置工具（“指挥官” - Rust版）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e技术栈：使用Rust语言开发，利用其内存安全特性，确保配置过程绝对可靠，工具长期运行不崩溃。\u003c/p\u003e\n\u003cp\u003e功能：提供图形化界面，用户可轻松配置通信模式、寄存器地址、倍率等参数。支持通过串口（USB转TTL） 进行固件刷新与参数下发，解决现场实施难题，无需复杂的网络配置。\u003c/p\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e数据演示端（“仪表盘”）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e技术栈：微信小程序。\u003c/p\u003e\n\u003cp\u003e功能：扫码即可查看实时数据曲线与数值面板，直观验证数据上云效果，方便现场调试与演示。\u003c/p\u003e\n\u003ch2 id=\"三-当前状态诚邀你成为共创者\"\u003e三、 当前状态：诚邀你，成为“共创者”\u003c/h2\u003e\n\u003cp\u003e项目正处于核心研发阶段，当前全力攻克裸机版本，实现通信方式支持WiFi和4G的Modbus RTU（串口）协议的稳定性与性能。它适合简单应用，保证运行稳定。我们相信，最好的产品源于真实场景的千锤百炼。\u003c/p\u003e\n\u003cp\u003e未来会研发通信方式支持LoRa、以太网的Modbus TCP，以及FreeRTOS多任务版，用于解决需要同时处理多路数据采集，复杂业务逻辑的高端场景。\u003c/p\u003e\n\u003cp\u003e因此，我们发起此次共创计划：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e你的支持，将直接转化为测试硬件（ESP32、各类Modbus传感器），用于极限环境下的兼容性验证。\u003c/li\u003e\n\u003cli\u003e你的设备，可以寄来成为我们的“适配样本”，共同完善设备库。\u003c/li\u003e\n\u003cli\u003e你的反馈，将直接塑造产品的下一个版本。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"四未来承诺\"\u003e四、未来承诺\u003c/h2\u003e\n\u003cp\u003e当固件通过大量真实场景验证，达到工业级稳定标准后，我们将正式申请软著，并启动授权售卖。所有共创阶段的支持者，都将依据历史贡献，获得丰厚的升级折扣与永久优先支持权。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我入驻了爱发电，更多详情内容请查看：\u003ca href=\"https://afdian.com/a/modujson\"\u003ehttps://afdian.com/a/modujson\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Modbus转MQTT网关固件研发与共创计划"},{"content":"前言：消失的“30秒”与开发者的尊严 作为开发者，我每天开机后的第一个动作就是：打开 VirtualBox UI -\u0026gt; 选中虚拟机 -\u0026gt; 点击启动 -\u0026gt; 等待窗口弹出 -\u0026gt; 最小化窗口。\n这套动作耗时约 30 秒，虽然微不足道，但这种重复的机械劳动是消磨创造力的元凶。今天，我决定拔掉这颗“硌脚的沙子”，用最优雅的方式让开发环境随系统静默启动。\n技术核心：什么是 Headless 模式？ 通常我们启动虚拟机都会弹出一个窗口，但在服务器环境下，我们只需要它的后台服务（如 SSH、Web Server）。VirtualBox 提供的 headless 模式可以实现：\n无窗口运行：不占用任务栏，像原生系统服务一样。 低资源占用：省去了图形界面的显存开销。 第一步：编写自动化脚本 (.bat) 为了避免开机瞬间磁盘 IO 占用过高导致启动失败，我们在脚本中加入了 10 秒延迟。AutoStartDev.bat的脚本如下:\n@echo off title Dev-Server Delayed Starter echo [SYSTEM] System initialized... :: Wait for system stability echo [WAIT] Waiting for 10 seconds to ensure system stability... timeout /t 10 /nobreak echo [SYSTEM] Starting \u0026#34;dev-server\u0026#34; in headless mode... :: Run VirtualBox command :: Ensure VBoxManage is in your System PATH VBoxManage startvm \u0026#34;dev-server\u0026#34; --type headless if %errorlevel% equ 0 ( echo. echo ======================================== echo [SUCCESS] VM \u0026#34;dev-server\u0026#34; is now RUNNING. echo ======================================== timeout /t 5 ) else ( echo. echo [ERROR] Failed to start VM. echo [TIP] Check your VM name or VirtualBox installation. pause ) dev-server是虚拟机的名称，请修改为你自己的虚拟机名称。\n如果脚本无法正常运行，请检查你的 VBoxManage 是否已加入系统环境变量（在 CMD 输入 VBoxManage -v 验证）。\n注：如果提示找不到命令，请先将 VirtualBox 的安装路径（通常是 C:\\Program Files\\Oracle\\VirtualBox）添加到系统的环境变量 Path 中。\n第二步：配置 Windows 任务计划程序 如果你希望更专业，不满足于把脚本丢进 startup 文件夹，那么 Windows Task Scheduler 是最佳选择：\n创建任务：打开“任务计划程序”，点击“创建任务”。 触发器 (Trigger)：选择“登录时 (At log on)”。 延迟设置：在触发器高级设置中，勾选“延迟任务时间”，手动输入 10 seconds。 操作 (Action)：选择“启动程序”，指向刚才创建的 .bat 文件。 电源选项：在“条件”选项卡中，取消勾选“只有在交流电源下启动”，确保笔记本模式也能正常运行。 结语：拒绝“伪需求”，解决“真痛点” 之前我尝试过很多宏大的项目，总因为找不到反馈而放弃。但这次小小的自动化改进，让我每天开机都能感受到 “环境已就绪” 的丝滑感。\n感悟： 真正的内在动力往往不是改变世界，而是改善自己当下的生活。\n","permalink":"https://blog.91demo.top/wiki/autostartvm.html","summary":"\u003ch2 id=\"前言消失的30秒与开发者的尊严\"\u003e前言：消失的“30秒”与开发者的尊严\u003c/h2\u003e\n\u003cp\u003e作为开发者，我每天开机后的第一个动作就是：打开 VirtualBox UI -\u0026gt; 选中虚拟机 -\u0026gt; 点击启动 -\u0026gt; 等待窗口弹出 -\u0026gt; 最小化窗口。\u003c/p\u003e\n\u003cp\u003e这套动作耗时约 30 秒，虽然微不足道，但这种重复的机械劳动是消磨创造力的元凶。今天，我决定拔掉这颗“硌脚的沙子”，用最优雅的方式让开发环境随系统静默启动。\u003c/p\u003e\n\u003ch2 id=\"技术核心什么是-headless-模式\"\u003e技术核心：什么是 Headless 模式？\u003c/h2\u003e\n\u003cp\u003e通常我们启动虚拟机都会弹出一个窗口，但在服务器环境下，我们只需要它的后台服务（如 SSH、Web Server）。VirtualBox 提供的 headless 模式可以实现：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e无窗口运行：不占用任务栏，像原生系统服务一样。\u003c/li\u003e\n\u003cli\u003e低资源占用：省去了图形界面的显存开销。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"第一步编写自动化脚本-bat\"\u003e第一步：编写自动化脚本 (.bat)\u003c/h3\u003e\n\u003cp\u003e为了避免开机瞬间磁盘 IO 占用过高导致启动失败，我们在脚本中加入了 10 秒延迟。AutoStartDev.bat的脚本如下:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e@echo off\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etitle Dev-Server Delayed Starter\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003eSYSTEM\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e System initialized...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e:: Wait \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e system stability\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003eWAIT\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e Waiting \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e seconds to ensure system stability...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etimeout /t \u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e /nobreak\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003eSYSTEM\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e Starting \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;dev-server\u0026#34;\u003c/span\u003e in headless mode...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e:: Run VirtualBox command\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e:: Ensure VBoxManage is in your System PATH\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eVBoxManage startvm \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;dev-server\u0026#34;\u003c/span\u003e --type headless\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e %errorlevel% equ \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo.\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo \u003cspan style=\"color:#f92672\"\u003e========================================\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003eSUCCESS\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e VM \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;dev-server\u0026#34;\u003c/span\u003e is now RUNNING.\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo \u003cspan style=\"color:#f92672\"\u003e========================================\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    timeout /t \u003cspan style=\"color:#ae81ff\"\u003e5\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo.\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003eERROR\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e Failed to start VM. \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    echo \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003eTIP\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e Check your VM name or VirtualBox installation.\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    pause\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003edev-server是虚拟机的名称，请修改为你自己的虚拟机名称。\u003c/p\u003e","title":"3分钟搞定：用 Headless 模式优雅自动开启 VirtualBox 开发环境"},{"content":"一、 项目设计核心思想 本项目的核心定位是内网穿透的一键化管理。参考 ngrok 的服务模式，通过自建 frp 服务器 提供稳定中转，将复杂的配置封装在 Wails 客户端中。\n商业闭环：通过微信小程序激励视频获取连接权限（2小时有效期）。 用户体验：一键连接、自动分配二级域名、配置持久化。 二、 前端架构：原生 JS 的三维交互 为了保持轻量，前端放弃了重量级框架，采用原生 JS 与 Wails 运行时通信。\n控制面板 (Dashboard)：状态驱动 UI。涉及扫码弹窗逻辑、广告验证状态机。 配置页面 (Settings)：表单处理。重点在于 Local Port 的保存与通过 Wails Bind 将数据下发给 Go 后端。 运行日志 (Logs)：虚拟黑屏终端。难点在于实时流式展示后端 frpc 吐出的日志。 三、 后端核心技术要点 将按照应用生命周期逻辑，对以下模块进行深度归纳：\na. 应用原生窗口定义\n结构化管理：AppManager 模式 // AppManager 统一管理应用和窗口 type AppManager struct { App *application.App MainWindow application.Window } var manager = \u0026amp;AppManager{} 采用了 AppManager 结构体来统一持有 App 实例和 MainWindow 实例。\n知识点：在 Wails 3 中，不再像 v2 那样通过上下文（ctx）传递，而是鼓励通过对象持有的方式管理窗口引用。这方便了后续在任何 Service 中通过 manager.MainWindow 直接操控窗口（如置顶、隐藏、发送事件）。\nService 注入机制 ms := NewMoleService() Services: []application.Service{ application.NewService(ms), }, 知识点：这是 Wails 3 的重大改进。通过 application.NewService 将自定义的 MoleService（核心逻辑类）注册进去。\n作用：Service 会在应用启动时自动初始化，并且其公开方法可以被前端 JS 直接调用，实现了前后端的“无感通讯”。\n跨平台窗口与生命周期策略 manager.App = application.New(application.Options{ Name: \u0026#34;FRP管理客户端\u0026#34;, Description: \u0026#34;一个实现自动内网穿透的管理工具\u0026#34;, LogLevel: slog.LevelDebug, Services: []application.Service{ application.NewService(ms), }, Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: false, }, Windows: application.WindowsOptions{ DisableQuitOnLastWindowClosed: true, }, OnShutdown: func() { // 清理资源 ms.Cleanup() }, }) 针对 Windows 和 Mac 做了不同的退出策略，这是为了实现托盘运行的关键一步：\nWindows 策略：DisableQuitOnLastWindowClosed: true。即使点击了关闭按钮，后端进程依然运行，配合托盘图标可以实现“后台挂机”。 Mac 策略：ApplicationShouldTerminateAfterLastWindowClosed: false（为了保持穿透稳定，设为 false）。 资源清理：OnShutdown 回调中执行 ms.Cleanup()。这确保了用户退出客户端时，后台运行的 frpc 进程会被强制杀死，杜绝进程残留。 精细化原生窗口配置 manager.MainWindow = manager.App.Window.NewWithOptions(application.WebviewWindowOptions{ Name: \u0026#34;main\u0026#34;, Title: \u0026#34;FRP 控制面板\u0026#34;, Width: 675, // 设置宽度 Height: 575, // 设置高度 DisableResize: true, MaxWidth: 675, MaxHeight: 575, Mac: application.MacWindow{ InvisibleTitleBarHeight: 50, Backdrop: application.MacBackdropTranslucent, TitleBar: application.MacTitleBarHiddenInset, // macOS 禁用缩放也会自动禁用全屏按钮 }, BackgroundColour: application.NewRGB(27, 38, 54), URL: \u0026#34;/\u0026#34;, }) UI 规格控制：通过 DisableResize、MaxWidth/Height 严格限制了窗口尺寸（675x575），这对于简单的工具软件能保证 UI 布局不走样。 Mac 特色适配： TitleBarHiddenInset：隐藏标题栏但保留红绿灯按钮，这是现代 macOS 应用（如 Raycast, Warp）的主流设计。 MacBackdropTranslucent：开启原生窗口的毛玻璃半透明效果，提升质感。 静态资源托管：AssetFileServerFS(assets) 利用 Go 的 embed 功能，将所有 HTML/JS/CSS 打包进二进制文件。 b. 设备 ID (Machine ID) 的唯一性生成\n知识点：如何生成机器指纹，确保用户在小程序看广告后，服务器能准确推送到对应的客户端。\nfunc (s *MoleService) getUniqueDeviceID() string { appID := \u0026#34;91demo.top\u0026#34; // 1. 获取平台信息 (如: windows, darwin, linux) osName := runtime.GOOS // 2. 获取架构信息 (如: amd64, arm64) archName := runtime.GOARCH // 3. 获取当前毫秒级时间戳 timestamp := time.Now().UnixNano() / int64(time.Millisecond) // 4. 组合原始字符串 rawString := fmt.Sprintf(\u0026#34;%s-%s-%s-%d\u0026#34;, appID, osName, archName, timestamp) // 5. 计算 SHA256 哈希 hash := sha256.Sum256([]byte(rawString)) // 6. 转为十六进制字符串并截取前 16 位 result := \u0026#34;mc\u0026#34; + fmt.Sprintf(\u0026#34;%x\u0026#34;, hash)[:16] return result } 指纹合成（Fingerprinting）使用 appID (\u0026ldquo;91demo.top\u0026rdquo;) 确保 ID 具备命名空间属性，防止与其他应用冲突，引入 runtime.GOOS 和 runtime.GOARCH，使用 UnixNano 毫秒级时间戳，意味着每次生成的 ID 都是不同的。现在的业务场景是“扫码后绑定连接”，这种动态 ID 保证了安全性（旧链接自动失效）。当用户将配置文件拷贝到另一台电脑并不影响，当需要绑定设备时，就需要获取设备序列号，网卡，硬盘等信息。\nc. 小程序码交互与扫码逻辑\n这个知识点很硬核，牵涉到了小程序码生成，扫码，以及信息交换逻辑。\nfunc (s *MoleService) getSubDomain(devID string) string { apiURL := \u0026#34;https://91demo.top/subdomain\u0026#34; ts := Now() content := fmt.Sprintf(\u0026#34;%s%d\u0026#34;, devID, ts) sign := HmacSign(content, secretKey) postData := map[string]any{ \u0026#34;device_id\u0026#34;: devID, \u0026#34;timestamp\u0026#34;: ts, \u0026#34;signature\u0026#34;: sign, } res, err := requestBackend(\u0026#34;POST\u0026#34;, apiURL, postData) if err != nil { log.Println(\u0026#34;获取子域名错误1，\u0026#34;, err) return \u0026#34;\u0026#34; } if !res.Success { log.Println(\u0026#34;获取子域名错误2，\u0026#34;, res.Error) return \u0026#34;\u0026#34; } return res.Subdomain } func (s *MoleService) getMpCode(devID string, subDomain string) string { apiURL := \u0026#34;https://91demo.top/mpcode\u0026#34; // 你的后台地址 ts := Now() content := fmt.Sprintf(\u0026#34;%s%d\u0026#34;, devID, ts) sign := HmacSign(content, secretKey) postData := map[string]any{ \u0026#34;device_id\u0026#34;: devID, \u0026#34;sub_domain\u0026#34;: subDomain, \u0026#34;timestamp\u0026#34;: ts, \u0026#34;signature\u0026#34;: sign, } res, err := requestBackend(\u0026#34;POST\u0026#34;, apiURL, postData) if err != nil { log.Println(\u0026#34;获取小程序码错误1，\u0026#34;, err) return \u0026#34;\u0026#34; } if !res.Success { log.Println(\u0026#34;获取小程序码错误2，\u0026#34;, res.Error) return \u0026#34;\u0026#34; } return res.MpCode } 当系统第一次启动的时候，会生成设备ID，然后根据设备ID获取子域名，也就是说，当第一次启动的时候，就确定了设备ID和子域名。当这些都获取成功后，会再次请求，获取专属小程序码，这个小程序码包含了设备ID和子域名。当用户扫码时会跳转到观看广告页面。这里不会绑定身份，不管谁看都可以。\n已经准备好了材料，当客户端计算上次看广告的时间距离当前时间已经超过了两个小时，那么就会弹出小程序码弹窗。\nfunc (s *MoleService) HandleStartFrp() Response { // 1. 检查广告状态 if !s.checkAdStatus() { // 启动一个协程监控广告状态 go func() { for { if s.isFinished.Load() { s.startFrp() // 启动 FRP // 通知主界面更新 UI s.emitAdStatus(\u0026#34;done\u0026#34;, \u0026#34;验证成功\u0026#34;) break } time.Sleep(2 * time.Second) } }() // 4. 后端建立 WebSocket (使用 gorilla/websocket) s.StartWS() return Response{ Code: 2, Content: s.config.MpCode, } } // 3. 如果已看广告，直接启动 FRP s.startFrp() url := s.GetDomainURL() return Response{ Code: 1, Content: url, } } 这一段 HandleStartFrp 函数是整个应用业务逻辑的中枢调度器。它完美展示了如何通过 Go 的并发特性处理异步业务（广告验证）与长连接管理。\n当 checkAdStatus 失败（用户未看广告）时，程序并未阻塞，而是启动了一个新的 Goroutine (协程) 运行匿名函数。使用 s.isFinished.Load() 来表示用户是否观看广告，这使用了 sync/atomic 包来处理跨协程的布尔标志位，这在多线程环境下是保证线程安全的标准做法，避免了数据竞争。每 2 秒检查一次状态，一旦验证通过，立即触发 startFrp()。\n调用 s.StartWS() 建立WebSocket，用来接听用户是否完成观看广告。 由于“看广告”是在移动端（微信小程序）完成的，服务端需要通过 WebSocket 主动向桌面客户端“推送”完成信号。客户端不再死等 API 返回，而是通过长连接静默等待，极大提升了 UI 的响应速度。\ns.emitAdStatus(\u0026ldquo;done\u0026rdquo;, \u0026ldquo;验证成功\u0026rdquo;) 使用事件驱动的 UI 更新 (Emit Event)，这是 Wails 的核心优势之一。后端逻辑层可以主动触发前端的 JS 回调。当协程发现广告看完后，直接发送事件，前端收到后自动关闭扫码弹窗并切换到连接成功界面，实现了数据驱动视图。\n使用了一种典型的状态机驱动。根据当前业务状态（是否看广告），返回不同的数据结构，让前端逻辑保持极简。其中：\nCode 2：代表“需要看广告”。返回 MpCode（小程序码），告诉前端：“去展示扫码界面”。 Code 1：代表“已看过广告”。直接返回 DomainURL，告诉前端：“显示你的内网穿透地址”。 当完整观看广告后，后端会启动frpc。\nd. 内嵌 frpc 资源的打包与管理\n在启动frpc之前，我们需要先找到frpc。\n// 仅打包 Windows 平台的两个架构 //go:embed resources/bin/frpc_windows_amd64.exe resources/bin/frpc_windows_arm64.exe var frpcBin embed.FS // 定义各架构对应的文件名 var frpcMap = map[string]string{ \u0026#34;amd64\u0026#34;: \u0026#34;resources/bin/frpc_windows_amd64.exe\u0026#34;, \u0026#34;arm64\u0026#34;: \u0026#34;resources/bin/frpc_windows_arm64.exe\u0026#34;, } const frpcTargetName = \u0026#34;frpc.exe\u0026#34; 这里使用 //go:embed 将 frpc 二进制文件打包进程序，方便分发，可以避免分发时缺少文件。上面是Windows的平台，还有Linux和Mac平台。\nfunc (s *MoleService) PrepareFrpEnv() (string, string, error) { binDir := s.getFrpBinDir() // 1. 确定 frpc 路径 frpcPath := filepath.Join(binDir, frpcTargetName) // frpcTargetName 是你在条件编译文件里定义的名称 // 如果文件不存在则从 embed 释放 if _, err := os.Stat(frpcPath); os.IsNotExist(err) { arch := runtime.GOARCH data, err := frpcBin.ReadFile(frpcMap[arch]) if err != nil { return \u0026#34;\u0026#34;, \u0026#34;\u0026#34;, err } if err := os.WriteFile(frpcPath, data, 0755); err != nil { return \u0026#34;\u0026#34;, \u0026#34;\u0026#34;, err } } // 2. 确定 toml 路径并写入 tomlPath := filepath.Join(binDir, \u0026#34;frpc.toml\u0026#34;) if _, err := os.Stat(tomlPath); os.IsNotExist(err) { tomlContent := s.genFrpConfig() if err := os.WriteFile(tomlPath, []byte(tomlContent), 0600); err != nil { return \u0026#34;\u0026#34;, \u0026#34;\u0026#34;, err } } return frpcPath, tomlPath, nil } 这是客户端环境初始化逻辑，它在运行前，会把frpc二进制解压到bin目录，然后生成Frpc配置文件。这里动态生成配置文件有两个好处，一是免除了用户配置繁琐性，二是可以动态生成一些广告相关参数。\n模块通过 “内存内嵌 -\u0026gt; 架构匹配 -\u0026gt; 自动释放 -\u0026gt; 动态写库” 的流水线，将一个复杂的内网穿透工具简化成了对用户完全透明的本地环境。它是客户端能够“一键连接”的技术基石。\ne. 动态启动 frpc 进程\nfunc (s *MoleService) startFrp() { // 1. 创建命令 s.frpCmd = exec.Command(frpcPath, \u0026#34;-c\u0026#34;, tomlPath) // 2. 针对 Windows 隐藏黑窗口 if runtime.GOOS == \u0026#34;windows\u0026#34; { // 使用 windows 专用的属性隐藏窗口 s.frpCmd.SysProcAttr = \u0026amp;syscall.SysProcAttr{HideWindow: true} } // 创建管道获取输出 stdout, _ := s.frpCmd.StdoutPipe() stderr, _ := s.frpCmd.StderrPipe() // 启动一个定时器，每 250ms 检查一次缓存并发送 ticker := time.NewTicker(250 * time.Millisecond) go func() { for { select { case \u0026lt;-ticker.C: s.flushLogs() case \u0026lt;-s.ctx.Done(): ticker.Stop() return } } }() // 2. 合并读取日志的函数 readLog := func(reader io.ReadCloser) { // 关键点：函数结束时关闭 reader，确保系统资源释放 defer reader.Close() scanner := bufio.NewScanner(reader) // 当进程退出，管道关闭时，Scan() 会自动返回 false，循环结束 for scanner.Scan() { line := scanner.Text() s.logMu.Lock() s.logBuffer = append(s.logBuffer, line) // 将日志存入切片 s.logMu.Unlock() } } // 启动进程 if err := s.frpCmd.Start(); err != nil { s.mu.Unlock() s.emitLog(\u0026#34;frpc 进程启动失败，\u0026#34;, err.Error()) log.Printf(\u0026#34;启动 frpc 失败: %v\u0026#34;, err) return } s.mu.Unlock() go readLog(stdout) go readLog(stderr) go func() { // Wait 会阻塞直到进程结束 _ = s.frpCmd.Wait() s.mu.Lock() defer s.mu.Unlock() // 清理句柄并重置运行状态 s.frpCmd = nil // 区分退出的原因 if s.isManualStop.Load() { s.emitLog(\u0026#34;frpc 进程已退出\u0026#34;) } else { s.emitLog(\u0026#34;警告：frpc 进程异常退出，请检查配置或网络\u0026#34;) // 这里可以触发 Wails 事件通知前端 UI 变更为“停止”状态 s.emitFrpStatus(\u0026#34;stop\u0026#34;) } }() // 发送自定义事件，通知前端关闭弹窗 log.Printf(\u0026#34;frpc 已启动，PID: %d，配置文件: %s\u0026#34;, s.frpCmd.Process.Pid, tomlPath) } 这是整个客户端的核心，它是动力引擎，不仅负责外部二进制程序的调用，还精细的处理了进程生命周期，日志流转，以及跨平台细节优化。\n使用s.mu.Lock()可以确保在多线程环境下（比如用户连点按钮）不会启动多个frpc进程，保证了状态的唯一性。\n通过syscall.SysProcAttr{HideWindow: true} 解决了 Windows 环境下弹出 CMD 黑窗口的痛点，使应用表现得像一个原生 GUI 程序。\n由于frpc会产生大量日志，每一行日志通过Wails Event立即发送给JS，会造成前端渲染压力过载，导致UI掉帧或者卡顿。这里使用了日志切片，后端协程readLog负责将扫描到的日志先存入切片，使用logMu保证并发写入安全。然后启动一个250ms的定时器，通过flushLogs批量将缓冲区内的日志一次性推送到前端。这样可以极大地降低前后端通信频率，保证UI流程。\n最后就是资源清理，当发生异常，或者关闭时，清理s.frpCmd = nil，为下一次“一键连接”扫清障碍。\n通过“OS 进程控制 + 线程安全缓冲区 + 定时批处理日志”，实现了一个工业级的子进程管理器。它不仅能稳定驱动 frpc，还通过优雅的日志处理方案，兼顾了后端运行的高效性与前端显示的流畅度。\nf. 系统托盘 (System Tray) 与生命周期\nmanager.MainWindow.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { // Hide the window manager.MainWindow.Hide() // Cancel the event so it doesn\u0026#39;t get destroyed e.Cancel() }) systemTray := manager.App.SystemTray.New() // Use the template icon on macOS so the clock respects light/dark modes. if runtime.GOOS == \u0026#34;darwin\u0026#34; { systemTray.SetTemplateIcon(icons.SystrayMacTemplate) } // 2. 定义左键点击逻辑：显示并聚焦窗口 systemTray.OnClick(func() { if manager.MainWindow != nil { manager.MainWindow.Show() manager.MainWindow.Focus() } }) // 3. 定义右键菜单 menu := manager.App.NewMenu() menu.Add(\u0026#34;显示窗口\u0026#34;).OnClick(func(ctx *application.Context) { manager.MainWindow.Show() manager.MainWindow.Focus() }) menu.AddSeparator() // 分割线 menu.Add(\u0026#34;退出\u0026#34;).OnClick(func(ctx *application.Context) { manager.App.Quit() }) systemTray.SetMenu(menu) 这是系统托盘的代码，为客户端注入了“灵魂”，它将一个普通的窗口程序转化为了一个常驻后台的系统服务工具。在 Wails 3 中，系统托盘（System Tray）与生命周期钩子（Hooks）的配合是提升用户体验的关键。\n通过 RegisterHook 监听 events.Common.WindowClosing，调用 manager.MainWindow.Hide() 而非销毁。e.Cancel()，这告诉操作系统：“别真的关掉这个进程，我只是把它藏起来了”。这保证了内网穿透服务的持续性。用户点击“叉号”只是关闭 UI，后台的 frpc 依然稳定运行。\n对于系统托盘 (System Tray) ，我们需要多平台适配，例如macOS 模板图标：SetTemplateIcon 确保图标在 macOS 的黑夜/白天模式下自动反色，保持原生质感。\n系统托盘菜单的交互逻辑：\nOnClick (左键)：实现“一键唤回”。通过 Show() 和 Focus() 组合，确保窗口从后台弹出并直接置于用户视觉中心。 右键菜单 (Context Menu)：通过 manager.App.NewMenu() 构建功能矩阵。区分了“显示窗口”与“退出程序”，给用户明确的心理预期。 在菜单中调用 manager.App.Quit()。这会触发前面定义的 OnShutdown 钩子，从而执行 ms.Cleanup()（关闭 frpc 进程），完成整个应用资源的闭环清理。\n四、 总结与反思 通过使用 Wails 3 与 Go 的结合，我成功将原本复杂的 frp 配置过程简化为了“扫码-连接”的直觉化操作。回顾整个开发过程，这不仅是一次对跨平台桌面开发的尝试，更是一次关于用户体验与商业逻辑结合的实践。\n技术层面的核心收获 在本项目中，我完成了以下几个关键的技术验证：\n资源极简化：通过 go:embed 实现了 frpc 二进制文件的内嵌与按需释放，达成了“单文件绿色运行”的目标。 通信异步化：利用 Goroutine 配合 WebSocket 解决了跨端（小程序到桌面端）的状态同步难题。 UI 流畅性：通过日志批处理（Ticker + Buffer）机制，在高频日志输出与前端渲染性能之间找到了完美的平衡点。 原生集成：深入应用了 Wails 3 的窗口钩子（Hooks）与系统托盘 API，使程序具备了成熟工具软件应有的常驻后台能力。 下一阶段的演进方向 虽然核心逻辑已经跑通，但要成为一个真正面向大众的“产品”，后续还有一段路要走：\n安全加固：目前的签名机制虽已成型，但后续还需在 代码混淆 和 二进制加壳 上下功夫，防止核心逻辑被破解。 合规化合集：针对 macOS 的 App Notarization（公证） 和 Windows 的 代码签名证书 是提升用户信任感、避免系统误报毒的必经之路。 分发与增长：技术实现只是起点，如何编写更具吸引力的宣传文案、如何建立稳定的用户反馈渠道，将决定这个工具能走多远。 最后的感悟 开发这个 frp 管理客户端的过程中，我深刻体会到：好的技术不应该增加用户的认知负担，而应该通过底层的复杂来换取表层的简单。\n我深知一个产品从‘能用’到‘商业化’还有巨大的鸿沟，包括上架、品牌化、合规化等繁琐流程。作为一个独立开发者，我或许没有足够的精力去完成每一个商业环节。\n但当我看到‘内网穿透’这一专业的概念，最终被简化为一个绿色的‘连接成功’状态时，那种创造的成就感已然足够。这篇文章记录下的每一个坑位和解决方案，才是我未来在技术路上最真实、最宝贵的资产。\n后续 在完成核心功能的开发后，我去简单了解一下这方面的知识。我面临了一个所有穿透类工具开发者都会遇到的终极问题：“技术中立”能否作为逃避监管风险的挡箭牌？\n作为个人开发者，将这样一款“一键式”工具公开发布，虽然能获得流量和成就感，但随之而来的合规风险是不容忽视的：\n域名内容监管：由于客户端自动分配的是我主域名下的二级域名，根据“谁接入谁负责”的原则，一旦用户利用该工具发布违规内容（如钓鱼、色情或非法信息），域名备案主体将面临直接的法律追责。 内容审计的缺位：个人开发者往往缺乏足够的力量去构建一套像大厂那样严密的协议层审计系统。在无法确保 100% 识别非法流量的情况下，盲目开放公共服务是极其危险的。 技术与责任的平衡：我始终认为，技术的进步是为了降低生产力的成本，而不是为了降低违规的成本。 因此，我做出了一个决定： 本项目仅作为个人技术研究与归纳，并不对外提供公网商业化服务。在文章中，我详细记录了所有技术坑位，旨在分享 Wails 3 与 Go 处理多进程管理的经验。\n对于同样想开发此类工具的同学，我建议在发布前务必考虑以下防护策略：\n接入实名认证：通过小程序或手机号，将流量行为追溯到具体自然人。 限制协议类型：仅开放非 Web 类的 TCP/UDP 转发，减少敏感内容暴露风险。 自建服务器模式：鼓励用户自备 frp 服务器，开发者仅提供易用的客户端界面，实现“工具”与“服务”的解耦。 这种“知止”的态度，是个人开发者在技术探索路上保护自己的最好方式。\n经过深思熟虑，我决定移除原有的“小程序码验证”与“动态子域名分配”等强绑定业务逻辑，回归工具本质，将其重塑为一个纯粹、通用的 frp 自动化管理客户端。\n现在的它支持连接用户自建的服务器，彻底实现了“工具与服务脱钩”，不仅消除了合规层面的后顾之忧，也赋予了用户更高的自由度。目前该项目已正式在 GitHub 开源，欢迎感兴趣的开发者参考或共建。\n🔗 项目地址：littletow/mole-go\n“代码因分享而有价值，工具因纯粹而更长久。”\n","permalink":"https://blog.91demo.top/go/mole-summary.html","summary":"\u003ch2 id=\"一-项目设计核心思想\"\u003e一、 项目设计核心思想\u003c/h2\u003e\n\u003cp\u003e本项目的核心定位是内网穿透的一键化管理。参考 ngrok 的服务模式，通过自建 frp 服务器 提供稳定中转，将复杂的配置封装在 Wails 客户端中。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e商业闭环：通过微信小程序激励视频获取连接权限（2小时有效期）。\u003c/li\u003e\n\u003cli\u003e用户体验：一键连接、自动分配二级域名、配置持久化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"二-前端架构原生-js-的三维交互\"\u003e二、 前端架构：原生 JS 的三维交互\u003c/h2\u003e\n\u003cp\u003e为了保持轻量，前端放弃了重量级框架，采用原生 JS 与 Wails 运行时通信。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e控制面板 (Dashboard)：状态驱动 UI。涉及扫码弹窗逻辑、广告验证状态机。\u003c/li\u003e\n\u003cli\u003e配置页面 (Settings)：表单处理。重点在于 Local Port 的保存与通过 Wails Bind 将数据下发给 Go 后端。\u003c/li\u003e\n\u003cli\u003e运行日志 (Logs)：虚拟黑屏终端。难点在于实时流式展示后端 frpc 吐出的日志。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三-后端核心技术要点\"\u003e三、 后端核心技术要点\u003c/h2\u003e\n\u003cp\u003e将按照应用生命周期逻辑，对以下模块进行深度归纳：\u003cbr\u003e\na. 应用原生窗口定义\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e结构化管理：AppManager 模式\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// AppManager 统一管理应用和窗口\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003etype\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eAppManager\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003eApp\u003c/span\u003e        \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eapplication\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eApp\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a6e22e\"\u003eMainWindow\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eapplication\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eWindow\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emanager\u003c/span\u003e = \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eAppManager\u003c/span\u003e{}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e采用了 AppManager 结构体来统一持有 App 实例和 MainWindow 实例。\u003cbr\u003e\n知识点：在 Wails 3 中，不再像 v2 那样通过上下文（ctx）传递，而是鼓励通过对象持有的方式管理窗口引用。这方便了后续在任何 Service 中通过 manager.MainWindow 直接操控窗口（如置顶、隐藏、发送事件）。\u003c/p\u003e","title":"Wails 3 进阶实战：基于 Go 语言实现 frp 自动化管理客户端的代码深度解析"},{"content":"1. 为什么是 QUIC？从 TCP 的瓶颈说起 手机在 Wi-Fi 和 4G/5G 之间切换（即“切网”）导致 TCP 连接断开，是移动互联网开发中的经典痛点。\nTCP 连接是基于源 IP、源端口、目的 IP、目的端口这“四元组”来标识的。当你从 Wi-Fi 切换到 5G 时，手机的 IP 地址发生了变化，旧的四元组立即失效，TCP 必须重新进行三次握手建立新连接，正在传输的数据（如视频缓冲、下载）就会中断。\nQUIC 引入了 Connection ID 的概念。它不依赖于底层 IP 地址。只要 CID 不变，即便你的 IP 从 A 变成了 B，服务端依然能通过 CID 认出：“噢，你还是刚才那个客户端！”。这样对于业务层完全无感知，数据传输无缝继续。这就是所谓的 “连接迁移 (Connection Migration)”。\n除了切网，QUIC 在弱网（丢包率高、延迟高）下更强的原因在于：\n改进的拥塞控制： QUIC 在应用层实现，可以更激进地进行丢包恢复。 无队头阻塞： 在 TCP 中，丢一个包全家等死；在 QUIC 中，你刷朋友圈的图丢了一个包，不会影响你接收聊天消息的流。 2. go 库quic-go 简介 这是Go的库，在 Go 语言世界里，quic-go 是事实上的标准实现。它不仅完整实现了 IETF QUIC 协议，还提供了类标准库 net 的简洁接口。\nConnection (连接)： 代表两个端点之间的 UDP 隧道。 Stream (流)： 连接内部的逻辑通道，双向且独立。 3. 证书准备：使用 OpenSSL 生成 Ed25519 证书 为了极致的性能与安全，弃用了传统的 RSA，选择 Ed25519 算法。它的签名速度更快，密钥更短。\n执行以下命令生成自签名证书：\n# 生成私钥 openssl genpkey -algorithm ed25519 -out server.key # 生成自签名证书 # /C=国家(CN) /ST=省份 /L=城市 /O=公司名 /OU=部门名 /CN=你的域名 openssl req -new -x509 -key server.key -out server.crt -days 365 \\ -subj \u0026#34;/C=CN/ST=Shanghai/L=Shanghai/O=MyTechCo/OU=IT/CN=yourdomain.com\u0026#34; # 获取证书哈希 openssl x509 -in server.crt -noout -sha256 -fingerprint # 获取公钥哈希 openssl x509 -in server.crt -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -hex 注：这里的 /CN=yourdomain.com 很关键，后续客户端校验域名时会用到。\n4. 服务端实战：构建 Echo Server 这里使用一个回声服务作为示例。服务端的核心逻辑是：监听端口 -\u0026gt; 接受连接 -\u0026gt; 接受流 -\u0026gt; 读写数据。\n// 核心逻辑演示 listener, _ := quic.ListenAddr(\u0026#34;127.0.0.1:4242\u0026#34;, tlsConfig, nil) for { conn, _ := listener.Accept(ctx) go func(c quic.Conn) { for { stream, _ := c.AcceptStream(ctx) // 经典的 Echo：将读取到的数据原样写回 go io.Copy(stream, stream) } }(conn) } 关键点： AcceptStream 是阻塞的，一个 Connection 可以开启无数个 Stream。\n5. 客户端实战：自定义证书校验与公钥哈希 在实际开发或测试中，我们常使用自签名证书。直接设置 InsecureSkipVerify: true 会导致中间人攻击，更优雅的做法是校验证书公钥的哈希值 (Pinning)。\n我们需要跳过默认校验： 设置 InsecureSkipVerify: true。然后进行深度检查： 利用 tls.Config 的 VerifyConnection 回调。\ntlsConfig := \u0026amp;tls.Config{ InsecureSkipVerify: true, // 必须跳过内置校验，否则自签名证书会报错 VerifyConnection: func(cs tls.ConnectionState) error { // 1. 拿到对端发来的第一个证书 cert := cs.PeerCertificates[0] // 2. 计算 SHA256 hash := sha256.Sum256(cert.Raw) actualHash := hex.EncodeToString(hash[:]) // 3. 与你预埋的期望哈希比对 expectHash := \u0026#34;服务端给出的哈希串\u0026#34; if actualHash != expectHash { return fmt.Errorf(\u0026#34;警报！证书指纹不匹配，可能存在中间人攻击\u0026#34;) } return nil }, } 虽然 InsecureSkipVerify 设置为 true 听起来很危险，但通过手动 VerifyConnection 校验哈希，安全性反而比信任系统根证书更高，因为这只允许你指定的特定证书通过。\n另外 quic.DialAddr 时传入的 serverName 必须和 openssl 命令中的 /CN= 填入的域名保持一致。\n6. 思考：QUIC 的生命周期 通过这个 Echo 服务，我们可以清晰地梳理出 QUIC 的工作流：\n握手阶段： 客户端发起 Dial，在 1 个 RTT 内完成 UDP 联通与 TLS 1.3 密钥交换。 多路复用： 双方可以在同一个 Connection 上 OpenStream。每个 Stream 都有自己的偏移量控制，互不干扰。 可靠性保障： 虽然底层是 UDP，但 quic-go 在应用层实现了丢包重传和流量控制。 结语： QUIC 不仅仅是加速版的 HTTPS，它为实时音视频、游戏底层协议提供了一个高效、安全且可控的基石。\n","permalink":"https://blog.91demo.top/go/quicgo.html","summary":"\u003ch2 id=\"1-为什么是-quic从-tcp-的瓶颈说起\"\u003e1. 为什么是 QUIC？从 TCP 的瓶颈说起\u003c/h2\u003e\n\u003cp\u003e手机在 Wi-Fi 和 4G/5G 之间切换（即“切网”）导致 TCP 连接断开，是移动互联网开发中的经典痛点。\u003c/p\u003e\n\u003cp\u003eTCP 连接是基于源 IP、源端口、目的 IP、目的端口这“四元组”来标识的。当你从 Wi-Fi 切换到 5G 时，手机的 IP 地址发生了变化，旧的四元组立即失效，TCP 必须重新进行三次握手建立新连接，正在传输的数据（如视频缓冲、下载）就会中断。\u003c/p\u003e\n\u003cp\u003eQUIC 引入了 Connection ID 的概念。它不依赖于底层 IP 地址。只要 CID 不变，即便你的 IP 从 A 变成了 B，服务端依然能通过 CID 认出：“噢，你还是刚才那个客户端！”。这样对于业务层完全无感知，数据传输无缝继续。这就是所谓的 “连接迁移 (Connection Migration)”。\u003c/p\u003e\n\u003cp\u003e除了切网，QUIC 在弱网（丢包率高、延迟高）下更强的原因在于：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e改进的拥塞控制： QUIC 在应用层实现，可以更激进地进行丢包恢复。\u003c/li\u003e\n\u003cli\u003e无队头阻塞： 在 TCP 中，丢一个包全家等死；在 QUIC 中，你刷朋友圈的图丢了一个包，不会影响你接收聊天消息的流。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"2-go-库quic-go-简介\"\u003e2. go 库\u003ca href=\"https://github.com/quic-go/quic-go\"\u003equic-go\u003c/a\u003e 简介\u003c/h2\u003e\n\u003cp\u003e这是Go的库，在 Go 语言世界里，quic-go 是事实上的标准实现。它不仅完整实现了 IETF QUIC 协议，还提供了类标准库 net 的简洁接口。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eConnection (连接)： 代表两个端点之间的 UDP 隧道。\u003c/li\u003e\n\u003cli\u003eStream (流)： 连接内部的逻辑通道，双向且独立。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"3-证书准备使用-openssl-生成-ed25519-证书\"\u003e3. 证书准备：使用 OpenSSL 生成 Ed25519 证书\u003c/h2\u003e\n\u003cp\u003e为了极致的性能与安全，弃用了传统的 RSA，选择 Ed25519 算法。它的签名速度更快，密钥更短。\u003c/p\u003e","title":"网络协议新纪元：基于 Go 语言实现支持 Ed25519 加密的 QUIC 高性能通信实战"},{"content":"在数字内容创作的旅程中，工具的选择往往折射出创作者在不同阶段的诉求。从最初的小程序“豆子碎片”，到尝试使用 Rust 生态的 mdBook，再到最终回归并定于 Hugo，这不仅是技术栈的迁移，更是我对内容分发、用户体验以及商业化潜力深度思考后的结果。\n1. 痛定思痛：告别移动端封闭生态的束缚 最初，我将大量的笔记和技术心得打造成了一个名为“豆子碎片”的小程序。初衷是利用移动端的便捷性，但随着内容的积累，弊端逐渐显现。\n最直观的痛点在于性能与体验。小程序在渲染长篇幅的技术文章，尤其是包含大量代码片段的内容时，卡顿感非常明显。更重要的是，作为一名开发者，我发现小程序在内容搜索与社交分享上存在天然的屏障。技术内容应当是开放的，它需要被搜索引擎检索，也需要方便地在网页端被阅读和引用。\n为了打破这种孤岛状态，我决定回归网站博客。\n2. 抉择：为什么不是 mdBook？ 在回归 Web 的第一站，我首先关注到了 mdBook。作为一个偏爱简洁风格的开发者，mdBook 这种类似文档流的展示方式起初非常吸引我。然而，在深入使用并尝试进行定制化开发时，我遇到了一些瓶颈。\n扩展性的局限\nmdBook 虽然在文档编写上非常纯粹，但在作为通用博客平台的扩展性上显得略为乏力。由于它主要为 Rust 文档设计，当我试图通过编写插件来处理一些特殊逻辑时（例如将我在小程序中定义的私有格式 type|url|params 自动还原为标准 URL 链接），开发过程并不如预期般顺遂。\n技术栈的契合度\n作为一名后端开发者，我对 Go 语言 拥有更高的熟悉度。在折腾 mdBook 插件遇到阻碍后，我意识到：用熟不用生不仅是开发经验，更是提升生产力的核心。\n3. 回归 Hugo：灵活性与商业化的平衡 最终选择 Hugo，是我在权衡了扩展性、性能与长期维护成本后的决定。\n强大的扩展能力与 Go 生态\nHugo 作为基于 Go 语言构建的静态网站生成器，其渲染速度堪称业界天花板。更重要的是，它的模板系统极其灵活。对于我之前在小程序中定义的自定义链接格式，我可以通过 Hugo 的 Shortcodes 或正则表达式替换轻松实现自动化转换。这种“随心所欲”的控制感，是后端开发者最看重的。\n布局与精力的分配\n在经历了几年的技术折腾后，我悟出了一个道理：开发者的时间应该花在内容创作上，而不是无休止地调整 CSS 布局。\n我选择了 PaperMod 主题，因为它精准地命中了我所有的痛点：\n极致简洁：没有冗余的装饰，让读者聚焦于文字。 响应式设计：完美适配手机端阅读，填补了告别小程序后的移动端体验。 易于 SEO：内置了完善的 SEO 结构，为后续的流量增长打下基础。 4. 商业化的考量：为了 Google AdSense 之所以选择 Hugo 而非继续留在 mdBook，还有一个非常现实的原因——流量主申请与广告布局。\nGoogle AdSense 对网站的结构、内容丰富度以及布局的规范性有一定要求。Hugo 丰富的社区生态提供了大量成熟的方案，让我能够快速地在侧边栏、正文间隙等位置灵活嵌入广告代码，同时不破坏整体的阅读体验。这种灵活性，是 mdBook 那种相对封闭的文档结构难以比拟的。\n从“豆子碎片”小程序到 Hugo 博客，这是一次从“封闭”走向“开放”的历程。我不再纠结于界面样式的细微调整，而是利用 Hugo 的灵活性，将复杂的数据转换自动化，从而把精力真正回归到后端技术研究与内容产出上。\n对于我来说，Hugo 不仅仅是一个静态网站生成器，它是我在数字世界中一个既高效又可靠的“内容实验室”。\n","permalink":"https://blog.91demo.top/wiki/mdbook2hugo.html","summary":"\u003cp\u003e在数字内容创作的旅程中，工具的选择往往折射出创作者在不同阶段的诉求。从最初的小程序“豆子碎片”，到尝试使用 \u003ca href=\"https://rust-lang.org/zh-CN/\"\u003eRust\u003c/a\u003e 生态的 \u003ca href=\"https://rust-lang.github.io/mdBook/\"\u003emdBook\u003c/a\u003e，再到最终回归并定于 \u003ca href=\"https://gohugo.io/\"\u003eHugo\u003c/a\u003e，这不仅是技术栈的迁移，更是我对内容分发、用户体验以及商业化潜力深度思考后的结果。\u003c/p\u003e\n\u003ch2 id=\"1-痛定思痛告别移动端封闭生态的束缚\"\u003e1. 痛定思痛：告别移动端封闭生态的束缚\u003c/h2\u003e\n\u003cp\u003e最初，我将大量的笔记和技术心得打造成了一个名为“豆子碎片”的小程序。初衷是利用移动端的便捷性，但随着内容的积累，弊端逐渐显现。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"豆子碎片小程序\" loading=\"lazy\" src=\"/images/notes/mdlist.webp\"\u003e\u003cbr\u003e\n最直观的痛点在于\u003cstrong\u003e性能与体验\u003c/strong\u003e。小程序在渲染长篇幅的技术文章，尤其是包含大量代码片段的内容时，卡顿感非常明显。更重要的是，作为一名开发者，我发现小程序在内容搜索与社交分享上存在天然的屏障。技术内容应当是开放的，它需要被搜索引擎检索，也需要方便地在网页端被阅读和引用。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"豆子碎片小程序\" loading=\"lazy\" src=\"/images/notes/mdshow.webp\"\u003e\u003cbr\u003e\n为了打破这种孤岛状态，我决定回归网站博客。\u003c/p\u003e\n\u003ch2 id=\"2-抉择为什么不是-mdbook\"\u003e2. 抉择：为什么不是 mdBook？\u003c/h2\u003e\n\u003cp\u003e在回归 Web 的第一站，我首先关注到了 mdBook。作为一个偏爱简洁风格的开发者，mdBook 这种类似文档流的展示方式起初非常吸引我。然而，在深入使用并尝试进行定制化开发时，我遇到了一些瓶颈。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e扩展性的局限\u003cbr\u003e\nmdBook 虽然在文档编写上非常纯粹，但在作为通用博客平台的扩展性上显得略为乏力。由于它主要为 Rust 文档设计，当我试图通过编写插件来处理一些特殊逻辑时（例如将我在小程序中定义的私有格式 \u003ccode\u003etype|url|params\u003c/code\u003e 自动还原为标准 URL 链接），开发过程并不如预期般顺遂。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e技术栈的契合度\u003cbr\u003e\n作为一名后端开发者，我对 Go 语言 拥有更高的熟悉度。在折腾 mdBook 插件遇到阻碍后，我意识到：用熟不用生不仅是开发经验，更是提升生产力的核心。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"3-回归-hugo灵活性与商业化的平衡\"\u003e3. 回归 Hugo：灵活性与商业化的平衡\u003c/h2\u003e\n\u003cp\u003e最终选择 Hugo，是我在权衡了扩展性、性能与长期维护成本后的决定。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e强大的扩展能力与 Go 生态\u003cbr\u003e\nHugo 作为基于 Go 语言构建的静态网站生成器，其渲染速度堪称业界天花板。更重要的是，它的模板系统极其灵活。对于我之前在小程序中定义的自定义链接格式，我可以通过 Hugo 的 Shortcodes 或正则表达式替换轻松实现自动化转换。这种“随心所欲”的控制感，是后端开发者最看重的。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e布局与精力的分配\u003cbr\u003e\n在经历了几年的技术折腾后，我悟出了一个道理：开发者的时间应该花在内容创作上，而不是无休止地调整 CSS 布局。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我选择了 PaperMod 主题，因为它精准地命中了我所有的痛点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e极致简洁：没有冗余的装饰，让读者聚焦于文字。\u003c/li\u003e\n\u003cli\u003e响应式设计：完美适配手机端阅读，填补了告别小程序后的移动端体验。\u003c/li\u003e\n\u003cli\u003e易于 SEO：内置了完善的 SEO 结构，为后续的流量增长打下基础。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"4-商业化的考量为了-google-adsense\"\u003e4. 商业化的考量：为了 Google AdSense\u003c/h2\u003e\n\u003cp\u003e之所以选择 Hugo 而非继续留在 mdBook，还有一个非常现实的原因——流量主申请与广告布局。\u003c/p\u003e","title":"从 mdBook 到 Hugo：一位后端开发者的博客架构演进与思考"},{"content":"我日常涉及 Hugo 博客发布、客户端打包、Nginx 运维等多种重复性脚本。每次都要 SSH 连服务器并执行命令，操作链过长，也不方便，特别是在身边没有电脑的情况。所以我就想构建一个通用的执行引擎，通过小程序远程触发，且具备零前端修改的扩展能力。\n系统架构设计 为了实现“明天增加脚本，小程序不发版”的目标，采用了“配置驱动” 模式。即“配置在云端，指令在指尖”。通过将业务逻辑（脚本路径与名称）完全从前端小程序中解耦，实现一套代码支持无限扩展的运维能力。\n核心流程 后端 (Go)：维护一个脚本配置列表（数据库或配置文件）。 前端 (小程序)：启动时请求后端接口，拉取可用脚本列表。 触发：使用时选择脚本名称，点击执行。 鉴权：后端校验小程序 OpenID，仅允许本人指令生效。 技术实现 系统分为三层，确保安全性与扩展性的统一： 配置层 (Go Config)：在服务器端定义脚本的 ID、名称和实际路径。 鉴权层 (WeChat Auth)：利用微信小程序 OpenID 建立强一致性的身份白名单。 展示层 (Mini Program UI)：动态拉取后端配置，仅负责“展示列表”与“触发指令”。 技术实现方案 A. 后端：动态脚本引擎 (Go)\n后端不再硬编码脚本路径，而是定义一个结构体：\n// 脚本任务定义 type ScriptTask struct { ID string `json:\u0026#34;id\u0026#34;` // 前端传递的任务标识 Name string `json:\u0026#34;name\u0026#34;` // 小程序界面显示的文字 Command string `json:\u0026#34;-\u0026#34;` // 实际执行的脚本路径 (对前端保密) } // 示例配置（可存放在 JSON 文件或数据库中） var tasks = []ScriptTask{ {ID: \u0026#34;hugo-post\u0026#34;, Name: \u0026#34;发布 Hugo 文章\u0026#34;, Command: \u0026#34;/scripts/deploy_hugo.sh\u0026#34;}, {ID: \u0026#34;build-client\u0026#34;, Name: \u0026#34;构建客户端\u0026#34;, Command: \u0026#34;/scripts/build_mole_go.sh\u0026#34;}, {ID: \u0026#34;nginx-restart\u0026#34;, Name: \u0026#34;重启 Nginx 服务\u0026#34;, Command: \u0026#34;systemctl restart nginx\u0026#34;}, } 对外接口定义：\nAPI 1 (/list): 返回 []ScriptTask（过滤掉 Path 字段）。 API 2 (/execute): 接收 id 和 code (小程序 OpenID/Token)。 其中在调用/execute执行接口时，通过微信登录流程获取 code 换取 openid，后端建立白名单。实现OpenID 物理隔离，前端只传递 ID（如 nginx-restart），不传递任何实际的 shell 指令，防止命令注入攻击。实现指令脱敏。非常危险的脚本，如执行删除操作，不对外提供接口，手动在服务器操作。\n这是执行鉴权示例代码：\nconst AdminOpenID = \u0026#34;YOUR_WECHAT_OPENID\u0026#34; func ExecuteHandler(w http.ResponseWriter, r *http.Request) { userOpenID := getOpenID(r) // 从鉴权 Token 中解析 if userOpenID != AdminOpenID { http.Error(w, \u0026#34;权限不足\u0026#34;, http.StatusForbidden) return } // 执行对应 ID 的脚本 } B. 前端：动态 UI 渲染 (小程序)\n小程序端保持高度抽象，只负责展示，小程序端不包含任何具体的脚本逻辑，界面完全由后端接口驱动。例如：\nData 结构：scripts: [] 交互逻辑：onLoad 时请求 /list，将结果渲染为 picker (选择器) 或列表。点击按钮时，发送当前选中的 id 给后端。 当扩展新的功能时，例如明天增加了重启Nginx的需求：\n在服务器编写脚本：restart_nginx.sh。 更新后端配置：在 tasks 数组中追加一条记录 {ID: \u0026ldquo;nginx-reload\u0026rdquo;, Name: \u0026ldquo;重启 Nginx\u0026rdquo;}。 生效：重新打开小程序，列表已自动出现“重启 Nginx”，点击即可运行。 该方案优势\n极简维护：小程序前端代码一生只需写一次，后续所有改动均在服务器端完成。 绝对安全：前端无法传递自定义 shell 命令，只能选择后端允许的 ID，杜绝注入攻击。基于微信生态，非本人无法通过鉴权。 轻量化：不记录日志、不反馈复杂结果，仅作为“远程开关”，响应极快。 前端示例代码：\n操作界面 \u0026lt;view class=\u0026#34;page\u0026#34; data-weui-theme=\u0026#34;light\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form__text-area\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;weui-form__title\u0026#34;\u0026gt;指令中心\u0026lt;/h2\u0026gt; \u0026lt;view class=\u0026#34;weui-form__desc\u0026#34;\u0026gt;仅限管理员操作，请谨慎触发本地脚本\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-form__control-area\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cells__group weui-cells__group_form\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cells weui-cells_after-title\u0026#34;\u0026gt; \u0026lt;!-- 脚本选择器 --\u0026gt; \u0026lt;picker bindchange=\u0026#34;bindScriptChange\u0026#34; value=\u0026#34;{{index}}\u0026#34; range=\u0026#34;{{scriptList}}\u0026#34; range-key=\u0026#34;name\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cell weui-cell_active weui-cell_select weui-cell_select-after\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__hd\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;weui-label\u0026#34;\u0026gt;待执行脚本\u0026lt;/label\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__bd\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-select\u0026#34;\u0026gt;{{scriptList[index].name || \u0026#39;点击选择脚本\u0026#39;}}\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/picker\u0026gt; \u0026lt;!-- 身份确认展示（只读） --\u0026gt; \u0026lt;view class=\u0026#34;weui-cell\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__hd\u0026#34;\u0026gt;\u0026lt;label class=\u0026#34;weui-label\u0026#34;\u0026gt;当前身份\u0026lt;/label\u0026gt;\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__bd\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;weui-badge\u0026#34; style=\u0026#34;background-color: #07C160;\u0026#34;\u0026gt;管理员 (Authenticated)\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 操作区 --\u0026gt; \u0026lt;view class=\u0026#34;weui-form__tips-area\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form__tips\u0026#34;\u0026gt;所选脚本将在服务器后台静默执行\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-form__opr-area\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;weui-btn weui-btn_primary\u0026#34; bindtap=\u0026#34;executeCommand\u0026#34;\u0026gt;立即执行指令\u0026lt;/button\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 页脚说明 --\u0026gt; \u0026lt;view class=\u0026#34;weui-footer weui-footer_fixed-bottom\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-footer__text\u0026#34;\u0026gt;Copyright © 2026 Eagle 豆子实验室\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 操作JS逻辑 Page({ data: { // 脚本列表：实际开发时通过 onLoad 调用后端接口获取 scriptList: [ { id: \u0026#39;hugo-post\u0026#39;, name: \u0026#39;发布 Hugo 文章\u0026#39; }, { id: \u0026#39;build-client\u0026#39;, name: \u0026#39;构建客户端\u0026#39; }, { id: \u0026#39;nginx-restart\u0026#39;, name: \u0026#39;重启 Nginx 服务\u0026#39; } ], index: 0, // 当前选择的索引 }, onLoad: function() { // TODO: 从后端接口拉取最新的脚本配置列表 // this.fetchScriptConfig(); }, // 选择脚本 bindScriptChange: function(e) { this.setData({ index: e.detail.value }); }, // 执行指令 executeCommand: function() { const selectedTask = this.data.scriptList[this.data.index]; wx.showModal({ title: \u0026#39;确认执行\u0026#39;, content: \u0026#39;确定要运行「${selectedTask.name}」吗？\u0026#39;, confirmText: \u0026#39;执行\u0026#39;, confirmColor: \u0026#39;#FA5151\u0026#39;, success: (res) =\u0026gt; { if (res.confirm) { console.log(\u0026#39;正在调用接口执行 ID:\u0026#39;, selectedTask.id); // TODO: 发送 OpenID 和 TaskID 到后端 wx.showToast({ title: \u0026#39;指令已发出\u0026#39;, icon: \u0026#39;success\u0026#39; }); } } }); } }) 操作界面样式 .page { height: 100%; background-color: #ededed; } .weui-label { width: 105px; word-wrap: break-word; word-break: break-all; } .weui-badge { display: inline-block; padding: 0.15em 0.4em; min-width: 8px; border-radius: 18px; color: #fff; line-height: 1.2; text-align: center; font-size: 10px; vertical-align: middle; } 想想就😄，当我在下班路上碰到服务器内存告警的时候，在手机上轻轻一点我的服务，问题就解决了，回家再慢慢查找原因。\n","permalink":"https://blog.91demo.top/go/bagscript.html","summary":"\u003cp\u003e我日常涉及 Hugo 博客发布、客户端打包、Nginx 运维等多种重复性脚本。每次都要 SSH 连服务器并执行命令，操作链过长，也不方便，特别是在身边没有电脑的情况。所以我就想构建一个通用的执行引擎，通过小程序远程触发，且具备零前端修改的扩展能力。\u003c/p\u003e\n\u003ch2 id=\"系统架构设计\"\u003e系统架构设计\u003c/h2\u003e\n\u003cp\u003e为了实现“明天增加脚本，小程序不发版”的目标，采用了“配置驱动” 模式。即“配置在云端，指令在指尖”。通过将业务逻辑（脚本路径与名称）完全从前端小程序中解耦，实现一套代码支持无限扩展的运维能力。\u003c/p\u003e\n\u003ch3 id=\"核心流程\"\u003e核心流程\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e后端 (Go)：维护一个脚本配置列表（数据库或配置文件）。\u003c/li\u003e\n\u003cli\u003e前端 (小程序)：启动时请求后端接口，拉取可用脚本列表。\u003c/li\u003e\n\u003cli\u003e触发：使用时选择脚本名称，点击执行。\u003c/li\u003e\n\u003cli\u003e鉴权：后端校验小程序 OpenID，仅允许本人指令生效。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"技术实现\"\u003e技术实现\u003c/h2\u003e\n\u003ch3 id=\"系统分为三层确保安全性与扩展性的统一\"\u003e系统分为三层，确保安全性与扩展性的统一：\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e配置层 (Go Config)：在服务器端定义脚本的 ID、名称和实际路径。\u003c/li\u003e\n\u003cli\u003e鉴权层 (WeChat Auth)：利用微信小程序 OpenID 建立强一致性的身份白名单。\u003c/li\u003e\n\u003cli\u003e展示层 (Mini Program UI)：动态拉取后端配置，仅负责“展示列表”与“触发指令”。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"技术实现方案\"\u003e技术实现方案\u003c/h3\u003e\n\u003cp\u003eA. 后端：动态脚本引擎 (Go)\u003c/p\u003e\n\u003cp\u003e后端不再硬编码脚本路径，而是定义一个结构体：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 脚本任务定义\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003etype\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eScriptTask\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eID\u003c/span\u003e      \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`json:\u0026#34;id\u0026#34;`\u003c/span\u003e      \u003cspan style=\"color:#75715e\"\u003e// 前端传递的任务标识\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eName\u003c/span\u003e    \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`json:\u0026#34;name\u0026#34;`\u003c/span\u003e    \u003cspan style=\"color:#75715e\"\u003e// 小程序界面显示的文字\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eCommand\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`json:\u0026#34;-\u0026#34;`\u003c/span\u003e       \u003cspan style=\"color:#75715e\"\u003e// 实际执行的脚本路径 (对前端保密)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 示例配置（可存放在 JSON 文件或数据库中）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etasks\u003c/span\u003e = []\u003cspan style=\"color:#a6e22e\"\u003eScriptTask\u003c/span\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {\u003cspan style=\"color:#a6e22e\"\u003eID\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;hugo-post\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eName\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;发布 Hugo 文章\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eCommand\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/scripts/deploy_hugo.sh\u0026#34;\u003c/span\u003e},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {\u003cspan style=\"color:#a6e22e\"\u003eID\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;build-client\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eName\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;构建客户端\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eCommand\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/scripts/build_mole_go.sh\u0026#34;\u003c/span\u003e},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {\u003cspan style=\"color:#a6e22e\"\u003eID\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;nginx-restart\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eName\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;重启 Nginx 服务\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eCommand\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;systemctl restart nginx\u0026#34;\u003c/span\u003e},\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e对外接口定义：\u003c/p\u003e","title":"全栈实战：基于 Go + 小程序构建“口袋指令中心”，实现远程发布控制"},{"content":" 工具说明：在进行内网穿透（如调试 mole-go）或配置服务器白名单时，频繁查询公网 IP 是刚需。为了告别繁琐的登录和搜索，我开发了这个集成在博客中的实时探测组件。\n🌐 公网 IP 探测 🌐 当前公网 IP： 正在探测... 复制 🛠️ 核心实现逻辑 这个工具基于 Hugo 的 Shortcode 功能实现，采用了异步加载技术，确保不会影响页面的首屏渲染速度。\n1. 结构设计 利用 HTML5 的 fetch API 调用轻量级的 IP 接口，并结合 navigator.clipboard 实现一键复制。\n2. 代码实现 在 layouts/shortcodes/myip.html 中存入以下代码：\n\u0026lt;!-- 样式与布局 --\u0026gt; \u0026lt;div style=\u0026#34;background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 15px; display: flex; align-items: center; justify-content: space-between;\u0026#34;\u0026gt; \u0026lt;div style=\u0026#34;display: flex; align-items: center; gap: 10px;\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org\u0026#34; width=\u0026#34;20\u0026#34; height=\u0026#34;20\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34; fill=\u0026#34;none\u0026#34; stroke=\u0026#34;var(--primary)\u0026#34; stroke-width=\u0026#34;2\u0026#34; stroke-linecap=\u0026#34;round\u0026#34; stroke-linejoin=\u0026#34;round\u0026#34;\u0026gt;\u0026lt;circle cx=\u0026#34;12\u0026#34; cy=\u0026#34;12\u0026#34; r=\u0026#34;10\u0026#34;\u0026gt;\u0026lt;/circle\u0026gt;\u0026lt;line x1=\u0026#34;2\u0026#34; y1=\u0026#34;12\u0026#34; x2=\u0026#34;22\u0026#34; y2=\u0026#34;12\u0026#34;\u0026gt;\u0026lt;/line\u0026gt;\u0026lt;path d=\u0026#34;M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\u0026#34;\u0026gt;\u0026lt;/path\u0026gt;\u0026lt;/svg\u0026gt; \u0026lt;span style=\u0026#34;font-weight: bold; color: var(--primary);\u0026#34;\u0026gt;我的公网 IP：\u0026lt;/span\u0026gt; \u0026lt;code id=\u0026#34;ip-display\u0026#34; style=\u0026#34;font-size: 1.1em; background: none; padding: 0;\u0026#34;\u0026gt;正在探测...\u0026lt;/code\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;button id=\u0026#34;copy-btn\u0026#34; onclick=\u0026#34;copyIP()\u0026#34; style=\u0026#34;background: var(--primary); color: #fff; border: none; border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 12px; display: none;\u0026#34;\u0026gt;复制\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; // 逻辑部分详见项目源码... function fetchIP() { // 使用 ipify API 获取 IP fetch(\u0026#39;https://api.ipify.org\u0026#39;) .then(response =\u0026gt; response.json()) .then(data =\u0026gt; { const ipSpan = document.getElementById(\u0026#39;ip-display\u0026#39;); ipSpan.innerText = data.ip; document.getElementById(\u0026#39;copy-btn\u0026#39;).style.display = \u0026#39;inline-block\u0026#39;; }) .catch(() =\u0026gt; { document.getElementById(\u0026#39;ip-display\u0026#39;).innerText = \u0026#39;获取失败，请检查网络\u0026#39;; }); } function copyIP() { const ip = document.getElementById(\u0026#39;ip-display\u0026#39;).innerText; navigator.clipboard.writeText(ip).then(() =\u0026gt; { const btn = document.getElementById(\u0026#39;copy-btn\u0026#39;); const originalText = btn.innerText; btn.innerText = \u0026#39;已复制！\u0026#39;; setTimeout(() =\u0026gt; { btn.innerText = originalText; }, 2000); }); } // 页面加载完成后立即执行 document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, fetchIP); \u0026lt;/script\u0026gt; 💡 为什么选 Shortcode？ 零依赖：不需要复杂的后端环境，完全前端实现。 高复用：除了 About 页面和专门的文章，未来在任何技术教程中只需一行\n{{\u0026lt; myip \u0026gt;}}即可调用。 SEO 友好：文章不仅自用，还能通过搜索“Hugo IP查询插件”为本站引流。 🚀 这样做的好处： 自留地工具化：把“实验室”的一部分功能静态化了。即使不登录，只要访问这篇文章（甚至你可以把它通过 hugo.toml 的菜单直接链接到“IP查询”），就能立刻用到工具。 置顶方便：通过 weight: 1 或在 Front Matter 中设置 pinned: true（取决于主题） ","permalink":"https://blog.91demo.top/wiki/mypubip.html","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e工具说明\u003c/strong\u003e：在进行内网穿透（如调试 \u003ccode\u003emole-go\u003c/code\u003e）或配置服务器白名单时，频繁查询公网 IP 是刚需。为了告别繁琐的登录和搜索，我开发了这个集成在博客中的实时探测组件。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"-公网-ip-探测\"\u003e🌐 公网 IP 探测\u003c/h2\u003e\n\u003cdiv\n    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;\"\u003e\n    \u003cdiv\u003e\n        \u003cstrong style=\"color: var(--primary);\"\u003e🌐 当前公网 IP：\u003c/strong\u003e\n        \u003ccode id=\"ip-display\" style=\"font-size: 1.1em; background: none; padding: 0;\"\u003e正在探测...\u003c/code\u003e\n    \u003c/div\u003e\n    \u003cbutton id=\"copy-btn\" onclick=\"copyIP()\"\n        style=\"background: var(--primary); color: #fff; border: none; border-radius: 4px; padding: 5px 12px; cursor: pointer; font-size: 12px; display: none;\"\u003e\n        复制\n    \u003c/button\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\n    function fetchIP() {\n        \n        fetch('https://91demo.top/api/myip')\n            .then(response =\u003e response.json())\n            .then(data =\u003e {\n                const ipSpan = document.getElementById('ip-display');\n                ipSpan.innerText = data.ip;\n                document.getElementById('copy-btn').style.display = 'inline-block';\n            })\n            .catch(() =\u003e {\n                document.getElementById('ip-display').innerText = '获取失败，请检查网络';\n            });\n    }\n\n    function copyIP() {\n        const ip = document.getElementById('ip-display').innerText;\n        navigator.clipboard.writeText(ip).then(() =\u003e {\n            const btn = document.getElementById('copy-btn');\n            const originalText = btn.innerText;\n            btn.innerText = '已复制！';\n            setTimeout(() =\u003e { btn.innerText = originalText; }, 2000);\n        });\n    }\n\n    \n    document.addEventListener('DOMContentLoaded', fetchIP);\n\u003c/script\u003e\n\u003chr\u003e\n\u003ch3 id=\"-核心实现逻辑\"\u003e🛠️ 核心实现逻辑\u003c/h3\u003e\n\u003cp\u003e这个工具基于 Hugo 的 \u003cstrong\u003eShortcode\u003c/strong\u003e 功能实现，采用了异步加载技术，确保不会影响页面的首屏渲染速度。\u003c/p\u003e","title":"实战：为 Hugo 博客开发一个公网 IP 探测 Shortcode"},{"content":"我使用go写了一个http转WebSocket服务，这是一个命令行程序，双击可以直接运行，今天，有个新的需求，需要将这个命令行程序转为dll，然后供第三方使用。\n1，改造程序 这个命令行程序很小，核心逻辑就是启动了一个HTTP服务，然后接收连接，然后通过WebSocket将内容转发出去。所以，在调整为dll时，仅仅需要导出两个函数即可。\n1，StartServer，启动http服务，监听端口地址。因为不能阻塞，所以需要在监听服务时使用go方法启动一个携程。\n2，StopServer，关闭http服务，需要提供给第三方，当它关闭时，需要调用它，释放监听地址等资源。\n下面开始改造代码：\npackage main import \u0026#34;C\u0026#34; // 必须导入 C 包 import ( // ... 原有导入 ... ) // 保持原有的全局变量和函数逻辑 (例如transDataGet, connWs 等) // 需要注意下面的//export这中间不能有空格，否则无法导出头文件 //export StartServer func StartServer() { // 将原本 main 函数里的逻辑放在这里 gin.SetMode(gin.ReleaseMode) r := gin.New() // ... 注册路由 ... r.Run(\u0026#34;127.0.0.1:9988\u0026#34;) } // 必须保留一个空的 main 函数 func main() {} // 其它参考StartServer，如果需要导出，一定要添加//export 2，编译命令 我是在windows环境，需要安装cgo环境，我安装了Mingw-w64支持C编译环境，执行以下命令：\ngo build -buildmode=c-shared -o trans.dll main.go 执行后会生成trans.h和trans.dll两个文件。\n3，验证dll 除了一些可以查看dll的工具外，还可以使用第三方语言进行测试。例如我这里使用Python。\nimport ctypes import time lib = ctypes.CDLL(\u0026#34;./trans.dll\u0026#34;) lib.StartServer() print(\u0026#34;服务已在后台启动...\u0026#34;) time.sleep(10) # 模拟第三方程序运行 lib.StopServer() print(\u0026#34;服务已关闭\u0026#34;) 4，再次优化代码 当实际测试的时候，我发现它会阻塞调用者，为了解决这个问题，我们需要再次改造程序。将启动服务改为非阻塞模式。将Gin的启动逻辑放入协程，并利用http.Server提供的Shutdown方法来实现优雅退出。\npackage main import \u0026#34;C\u0026#34; import ( // 原来的导入库 ) var ( srv *http.Server ) //export StartServer func StartServer() { if srv != nil { return // 避免重复启动 } gin.SetMode(gin.ReleaseMode) r := gin.New() // 保持你原有的路由配置 srv = \u0026amp;http.Server{ Addr: \u0026#34;127.0.0.1:9988\u0026#34;, Handler: r, } // 关键：在协程中启动，不会阻塞调用方的 DLL 加载线程 go func() { if err := srv.ListenAndServe(); err != nil \u0026amp;\u0026amp; err != http.ErrServerClosed { // 实际生产中可以考虑将错误日志记录到文件 } }() } //export StopServer func StopServer() { if srv != nil { // 优雅关闭：给 5 秒时间处理未完成的请求 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() srv.Shutdown(ctx) disConnWs() // 关闭你的 WebSocket 连接 srv = nil } } func main() {} // 必须保留空 main 改造完毕后，使用上面的编译命令再次编译。\n这里解释一下：\nStartServer：调用后立即返回，Gin 在后台运行。 StopServer：停止 HTTP 服务并清理 WebSocket。第三方程序在退出前必须调用此函数，否则可能导致进程残留。 继续使用Python验证，满足需求。\n5，减少体积 当功能满足需求时，发现这个dll文件非常大，为了解决体积大的问题，可以在编译时添加这个参数：\ngo build -ldflags=\u0026#34;-s -w\u0026#34; -buildmode=c-shared -o trans.dll main.go 这个可以减少dll文件大小。如果还嫌尺寸大，可以使用upx -9 trans.dll再次减少体积。\n6，如何实现多实例 需求又增加了，需要开启多个服务。那么对于C或者第三方来说，就需要交换数据，实际的需求就是在DLL开发中，将Go对象（如WebSocket连接、HTTP Server等）以句柄（Handle）的形式返回给C/第三方，这是实现多实例管理、长期控制的专业做法。\n其底层核心逻辑是：Go内部维护一个Map，Key为递增的整数（句柄），Value为对象指针。\n1，句柄管理器的实现\n需要建立一个全局的注册表，并配合读写锁（确保多线程安全）。\npackage main import \u0026#34;C\u0026#34; import ( \u0026#34;sync\u0026#34; \u0026#34;net/http\u0026#34; ) var ( // 存储句柄与 Server 的对应关系 instanceMap = make(map[int]*http.Server) instanceMu sync.RWMutex nextHandle = 1 ) // 内部函数：注册实例 func registerInstance(s *http.Server) int { instanceMu.Lock() defer instanceMu.Unlock() h := nextHandle instanceMap[h] = s nextHandle++ return h } // 内部函数：获取实例 func getInstance(h int) *http.Server { instanceMu.RLock() defer instanceMu.RUnlock() return instanceMap[h] } // 内部函数：释放实例 func removeInstance(h int) { instanceMu.Lock() defer instanceMu.Unlock() delete(instanceMap, h) } 2，修改导出函数返回句柄\n将StartServer修改为返回一个int，供第三方保存。\n//export StartServer func StartServer() int { gin.SetMode(gin.ReleaseMode) r := gin.New() // ... 配置你的路由 ... s := \u0026amp;http.Server{ Addr: \u0026#34;127.0.0.1:9988\u0026#34;, Handler: r, } go s.ListenAndServe() // 注册并返回句柄给 C return registerInstance(s) } //export StopServer func StopServer(handle int) int { s := getInstance(handle) if s == nil { return -1 // 句柄无效 } // 执行关闭逻辑 s.Shutdown(context.Background()) removeInstance(handle) return 0 // 成功 } 上面的问题是固定的端口地址会启动失败，需要调整：\npackage main import \u0026#34;C\u0026#34; import ( // 导入库 ) var ( // 使用 sync.Map 存储句柄与 http.Server 的映射，确保并发安全 serverMap sync.Map nextHandle int64 = 1 mu sync.Mutex ) // 使用了C的数据类型 //export StartServer func StartServer(port C.int) C.int { p := int(port) gin.SetMode(gin.ReleaseMode) r := gin.New() // 路由配置（此处复用你之前的逻辑） srv := \u0026amp;http.Server{ Addr: fmt.Sprintf(\u0026#34;127.0.0.1:%d\u0026#34;, p), Handler: r, } // 启动服务 go func() { if err := srv.ListenAndServe(); err != nil \u0026amp;\u0026amp; err != http.ErrServerClosed { fmt.Printf(\u0026#34;Port %d error: %v\\n\u0026#34;, p, err) } }() // 生成并存储句柄 mu.Lock() h := nextHandle nextHandle++ mu.Unlock() serverMap.Store(h, srv) return C.int(h) } //export StopServer func StopServer(handle C.int) C.int { h := int64(handle) val, ok := serverMap.Load(h) if !ok { return -1 // 句柄不存在 } srv := val.(*http.Server) // 优雅关闭 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { return -2 } serverMap.Delete(h) return 0 // 成功 } func main() {} 这里使用C.int是为了类型安全，Go的int长度在64位系统上是8字节，而C语言的int通常固定为4字节。直接使用Go的int可能会导致内存对齐错误或数据截断。C.int会确保Go编译器生成的.h头文件使用标准C的int类型，这样第三方语言才能准确对应。\n3，编译\n确保使用64位编译以匹配Python环境：\nset GOARCH=amd64 set CGO_ENABLED=1 go build -ldflags=\u0026#34;-s -w\u0026#34; -buildmode=c-shared -o myserver.dll main.go 4，第三方（Python）如何使用句柄\n第三方程序现在可以像控制窗口句柄一样控制你的服务：\nimport ctypes import time import requests # 1. 加载 DLL try: # 使用 CDLL 加载，cdecl 约定 lib = ctypes.CDLL(\u0026#34;./myserver.dll\u0026#34;) except Exception as e: print(f\u0026#34;加载失败: {e}\u0026#34;) exit() # 2. 启动两个不同端口的服务 print(\u0026#34;--- 启动实例 1 ---\u0026#34;) handle1 = lib.StartServer(9988) print(f\u0026#34;实例 1 启动，句柄 ID: {handle1}\u0026#34;) print(\u0026#34;\\n--- 启动实例 2 ---\u0026#34;) handle2 = lib.StartServer(9989) print(f\u0026#34;实例 2 启动，句柄 ID: {handle2}\u0026#34;) time.sleep(1) # 等待服务就绪 # 3. 验证服务是否独立运行 for port in [9988, 9989]: try: r = requests.get(f\u0026#34;http://127.0.0.1:{port}/\u0026#34;) print(f\u0026#34;端口 {port} 响应: {r.text}\u0026#34;) except Exception as e: print(f\u0026#34;端口 {port} 访问失败\u0026#34;) # 4. 根据句柄关闭实例 1 print(\u0026#34;\\n--- 停止实例 1 ---\u0026#34;) res = lib.StopServer(handle1) print(f\u0026#34;停止操作返回码: {res} (0表示成功)\u0026#34;) # 5. 最终验证 try: requests.get(\u0026#34;http://127.0.0.1\u0026#34;, timeout=1) except: print(\u0026#34;端口 9988 已成功释放\u0026#34;) try: r = requests.get(\u0026#34;http://127.0.0.1\u0026#34;) print(f\u0026#34;端口 9989 依然存活: {r.text}\u0026#34;) lib.StopServer(handle2) print(\u0026#34;实例 2 已手动清理\u0026#34;) except: pass 使用ctypes加载DLL，并使用句柄控制两个不同端口的服务。\n5，为什么使用映射表而不直接传指针？\n安全性：直接把 Go 的内存地址（指针）给 C 非常危险。一旦 Go 的垃圾回收（GC）移动了对象，指针就失效了。使用 整数 ID 作为句柄是工业界的标准做法。 多实例支持：第三方可以同时启动多个 WS 转发服务（只要端口不同），并通过句柄分别控制。 6，关于 WebSocket 连接的句柄\n同样的逻辑也可以应用在 wsConn 上。你可以提供一个 ConnectWS(url) 返回 connHandle，然后 SendMessage(connHandle, data)。这样你的 DLL 就不再是单例的，而是变成了一个功能强大的类库。\n这样做的好处是你的 DLL 彻底变成了“无状态”的，所有的状态都由第三方通过句柄维护。\n7，扩展内容，字符串处理\n对于端口（数字），使用C.int很简单，但如果之后需要传入IP地址或URL（字符串），\n需要在入参时使用*C.char，并用C.GoString(param)转换。\n由第三方分配的字符串指针，Go只读不改是安全的。\n那么如果Go要返回字符串给第三方呢？\n这是最麻烦的地方，因为Go分配的内存地址会被Go GC回收。\n推荐的方法是，不要让Go返回字符串，而是让第三方传入一个预分配好的缓冲区（Buffer），\n让Go往里写。\n这是一个示例：\npackage main /* #include \u0026lt;string.h\u0026gt; */ import \u0026#34;C\u0026#34; import ( \u0026#34;unsafe\u0026#34; ) //export GetWsMessage // handle: 句柄ID // buffer: 调用方（Python）准备好的空字节数组指针 // bufSize: 缓冲区的大小 func GetWsMessage(handle C.int, buffer *C.char, bufSize C.int) C.int { // 1. 模拟获取数据（实际中你会从你的 map/chan 中读取） msg := \u0026#34;I am ok, now is \u0026#34; + \u0026#34;2026-01-02 15:04:05\u0026#34; // 2. 将 Go 字符串转为字节切片 msgBytes := []byte(msg) msgLen := len(msgBytes) // 3. 检查缓冲区是否够大 (留 1 字节给 C 字符串的终止符 \\0) if int(bufSize) \u0026lt;= msgLen { return C.int(-1) // 告诉调用方：空间不足 } // 4. 将数据拷贝到 C 内存空间 // 使用 unsafe 获取 C 指针对应的切片进行操作 ptr := unsafe.Pointer(buffer) cSlice := (*[1 \u0026lt;\u0026lt; 30]byte)(ptr)[:int(bufSize):int(bufSize)] copy(cSlice, msgBytes) cSlice[msgLen] = 0 // 必须手动加上 C 字符串的结束符 return C.int(msgLen) // 返回实际写入的长度 } func main() {} 需要接受一个缓冲区指针（C.char）和缓冲区的大小（C.int）。\n其中ptr:=unsafe.Pointer(buffer)是将C.char转换为unsafe.Pointer，\n在Go中，unsafe.Ponter是所有指针的中转站，只有转成它，才能进行后续的强制类型转换。\n(*[1\u0026lt;\u0026lt;30]byte)是定义了一个极大的数组类型即1GB。(ptr)是一个指针转换。\n它的意思是：“告诉编译器，把ptr指向的地址看作是一个容量为1GB的数组的开头”。\n这里设置这么大，是因为在编译阶段，Go需要知道这个数组的类型。由于C传过来的缓冲区大小是动态的，\n由bufSize决定，我们在写代码时无法确定具体长度。于是我们声明一个极大的上限，确保后续的切片操作\n不会超过这个定义的边界。注意，这个是类型声明，并不会真的分配1GB内存。\n[:int(bufSize):int(bufSize)]这是Go的三元切片表达式，[low:high:max]。\n分别为从0开始，切片的长度，切片的容量。这步操作就是将那个虚构的1GB数组裁剪成了实际传入的bufSize大小的切片。这样，通过cSlice写入数据时，Go就能提供越界检查，确保安全。\n如果使用Go1.17版本以上，可以使用标准库的unsafe.Slice，它完全替代了上面那行晦涩的代码，更安全也更好懂：\nimport \u0026#34;unsafe\u0026#34; // 替代那行黑魔法 cSlice := unsafe.Slice((*byte)(unsafe.Pointer(buffer)), int(bufSize)) 这就话就是告诉Go，从这个指针开始，帮我创建一个长度为bufSize的切片。\n然后我们就可以使用copy将内容复制进去。\n这里没有使用C.GoBytes是因为它会创建一个新的内存副本，而我们这里的需求是直接使用第三方提供的内存，所以直接操作指针。\n下面是Python验证程序：\nimport ctypes lib = ctypes.CDLL(\u0026#34;./myserver.dll\u0026#34;) # 1. 准备一个 1024 字节的缓冲区 buf_size = 1024 buffer = ctypes.create_string_buffer(buf_size) # 2. 调用 Go 函数 # 假设句柄为 1 actual_len = lib.GetWsMessage(1, buffer, buf_size) if actual_len \u0026gt; 0: # 3. 从缓冲区读取内容 (.value 会根据 \\0 自动截断) message = buffer.value.decode(\u0026#39;utf-8\u0026#39;) print(f\u0026#34;收到消息: {message}, 长度: {actual_len}\u0026#34;) elif actual_len == -1: print(\u0026#34;缓冲区太小了！\u0026#34;) else: print(\u0026#34;没有新消息\u0026#34;) 我们再来一个更有难度的，如果处理二进制数据，那么它和普通字符串的最大区别在于：\n不能依赖\\0（Null Terminator）作为结束符。\n在二进制模式下，必须通过显示返回数据的实际字节长度，让第三方根据长度来截取数据，\n而不是读取到\\0停止。\n这是一个使用unsafe.Slice来操作内容的内容，在代码里，我们构造了一个包含0x00的消息。\npackage main import \u0026#34;C\u0026#34; import ( \u0026#34;unsafe\u0026#34; ) //export GetWsBinaryMessage func GetWsBinaryMessage(handle C.int, buffer *C.char, bufSize C.int) C.int { // 1. 模拟二进制数据：[72, 101, 0, 108, 108, 111] -\u0026gt; \u0026#34;He\\0llo\u0026#34; // 传统的 C 字符串只能读到 \u0026#34;He\u0026#34;，剩下的 \u0026#34;llo\u0026#34; 会丢失 binaryData := []byte{0x48, 0x65, 0x00, 0x6C, 0x6C, 0x6F, 0xFF, 0x00, 0xAA} dataLen := len(binaryData) // 2. 空间检查 if int(bufSize) \u0026lt; dataLen { return -1 // 缓冲区不足 } // 3. 直接操作内存（不拷贝，直接写） // 将 *C.char 转为 []byte 切片 cSlice := unsafe.Slice((*byte)(unsafe.Pointer(buffer)), int(bufSize)) // 4. 拷贝数据 copy(cSlice, binaryData) // 5. 关键：返回实际的字节长度 // 第三方必须依赖这个返回值来读取内存 return C.int(dataLen) } func main() {} 在Python中，不能使用.value，因为.value会在第一个\\0处截断。必须使用切片预防截取指定长度。\nimport ctypes lib = ctypes.CDLL(\u0026#34;./myserver.dll\u0026#34;) # 1. 准备缓冲区 buf_size = 1024 buffer = ctypes.create_string_buffer(buf_size) # 2. 调用并获取实际长度 data_len = lib.GetWsBinaryMessage(1, buffer, buf_size) if data_len \u0026gt; 0: # 3. 关键：使用 raw 属性并根据长度切片 # .raw 返回整个缓冲区的原始字节流，我们只取前 data_len 个 result_bytes = buffer.raw[:data_len] print(f\u0026#34;收到二进制长度: {len(result_bytes)}\u0026#34;) print(f\u0026#34;原始字节 (Hex): {result_bytes.hex().upper()}\u0026#34;) # 你会发现 00 成功传过来了，没有被截断 else: print(\u0026#34;错误或无数据\u0026#34;) 今天就到这里，消化一下。\n","permalink":"https://blog.91demo.top/go/godll.html","summary":"\u003cp\u003e我使用go写了一个http转WebSocket服务，这是一个命令行程序，双击可以直接运行，今天，有个新的需求，需要将这个命令行程序转为dll，然后供第三方使用。\u003c/p\u003e\n\u003ch2 id=\"1改造程序\"\u003e1，改造程序\u003c/h2\u003e\n\u003cp\u003e这个命令行程序很小，核心逻辑就是启动了一个HTTP服务，然后接收连接，然后通过WebSocket将内容转发出去。所以，在调整为dll时，仅仅需要导出两个函数即可。\u003c/p\u003e\n\u003cp\u003e1，StartServer，启动http服务，监听端口地址。因为不能阻塞，所以需要在监听服务时使用go方法启动一个携程。\u003cbr\u003e\n2，StopServer，关闭http服务，需要提供给第三方，当它关闭时，需要调用它，释放监听地址等资源。\u003c/p\u003e\n\u003cp\u003e下面开始改造代码：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epackage main\n\nimport \u0026#34;C\u0026#34; // 必须导入 C 包\nimport (\n    // ... 原有导入 ...\n)\n\n// 保持原有的全局变量和函数逻辑 (例如transDataGet, connWs 等)\n// 需要注意下面的//export这中间不能有空格，否则无法导出头文件\n\n//export StartServer\nfunc StartServer() {\n    // 将原本 main 函数里的逻辑放在这里\n    gin.SetMode(gin.ReleaseMode)\n    r := gin.New()\n    // ... 注册路由 ...\n    r.Run(\u0026#34;127.0.0.1:9988\u0026#34;)\n}\n\n// 必须保留一个空的 main 函数\nfunc main() {}\n\n// 其它参考StartServer，如果需要导出，一定要添加//export\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"2编译命令\"\u003e2，编译命令\u003c/h2\u003e\n\u003cp\u003e我是在windows环境，需要安装cgo环境，我安装了Mingw-w64支持C编译环境，执行以下命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ego build -buildmode=c-shared -o trans.dll main.go \n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e执行后会生成trans.h和trans.dll两个文件。\u003c/p\u003e\n\u003ch2 id=\"3验证dll\"\u003e3，验证dll\u003c/h2\u003e\n\u003cp\u003e除了一些可以查看dll的工具外，还可以使用第三方语言进行测试。例如我这里使用Python。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eimport ctypes\nimport time\n\nlib = ctypes.CDLL(\u0026#34;./trans.dll\u0026#34;)\nlib.StartServer()\nprint(\u0026#34;服务已在后台启动...\u0026#34;)\n\ntime.sleep(10) # 模拟第三方程序运行\n\nlib.StopServer()\nprint(\u0026#34;服务已关闭\u0026#34;)\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"4再次优化代码\"\u003e4，再次优化代码\u003c/h2\u003e\n\u003cp\u003e当实际测试的时候，我发现它会阻塞调用者，为了解决这个问题，我们需要再次改造程序。将启动服务改为非阻塞模式。将Gin的启动逻辑放入协程，并利用http.Server提供的Shutdown方法来实现优雅退出。\u003c/p\u003e","title":"Go 语言进阶实战：编写高性能 Windows DLL 动态链接库并供第三方多语言调用"},{"content":"在移动应用分发、文件共享等场景中，我们常常会遇到这样的困境：云存储平台（如阿里云OSS、腾讯云COS、蓝奏云等）上传的文件地址一旦生成就无法修改，但客户端版本频繁迭代时，每次更新都需要重新生成下载链接和二维码。用户端需要不断更新访问入口，体验极差。我就碰到了此类问题，在上一篇文章，我介绍了自己的客户端，并分享了下载地址。但我发现当我需要重新升级版本时，已经生成的地址将无法变更。在我的文章中，我直接使用了文件下载地址。这就让我急需解决如何实现\u0026quot;一次分发，永久可用\u0026quot;的优雅方案？Nginx反向代理配合301重定向，正是解决这一痛点的利器。\n一、问题根源：云存储的地址锁定机制 大多数云存储服务采用\u0026quot;对象存储\u0026quot;模式，文件上传后生成固定URL，该地址包含存储桶名称、地域、文件路径等不可变信息。这种设计虽然保证了数据一致性，却带来了分发难题：每次版本更新，开发者必须重新上传文件、生成新地址、制作新二维码，用户需要重新扫描或获取新链接。对于频繁迭代的应用，这种重复劳动不仅效率低下，更可能因用户未及时更新而影响使用体验。\n二、解决方案：Nginx重定向的架构设计 核心思路是地址解耦——将\u0026quot;物理存储地址\u0026quot;与\u0026quot;用户访问入口\u0026quot;分离。通过Nginx服务器作为中间层，用户始终访问固定域名，Nginx根据配置将请求重定向到实际的云存储地址。当文件更新时，只需修改Nginx配置中的目标地址，用户端的访问入口保持不变。\n具体架构如下：\n用户 → 固定域名/短链接 → Nginx服务器（301重定向） → 云存储实际地址\n这种设计实现了三个关键目标：\n入口稳定性：用户扫描的二维码或保存的链接永久有效 维护灵活性：后台地址变更只需修改Nginx配置，无需重新分发 成本可控性：Nginx只做重定向，流量消耗极低，普通云服务器即可承载 三、技术实现：从配置到部署 环境准备购买一台云服务器（1核1G配置即可），安装Nginx，将自有域名解析到服务器IP。推荐申请SSL证书启用HTTPS。 Nginx配置示例 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; } } 重定向类型选择 301永久重定向：搜索引擎会更新索引，客户端会缓存重定向结果，适合生产环境 302临时重定向：每次请求都会重定向，适合测试阶段频繁变更的场景 二维码生成 将固定地址（如https://your-domain.com/app/v2.0.apk）生成二维码，用户扫描即可下载。后续版本更新时，只需修改Nginx配置中的目标地址，二维码无需重新制作。\n四、方案优势与注意事项 核心优势\n零用户感知：用户无需关心后台地址变化 维护成本低：修改配置即可，无需重新上传二维码 扩展性强：可支持多版本共存、灰度发布等复杂场景 兼容性好：支持各种客户端（浏览器、APP、微信等） 实施建议\n测试先行：在测试环境验证重定向逻辑，确保目标地址正确 监控告警：配置Nginx日志监控，及时发现重定向失败问题 备份机制：保留旧版本配置一段时间，避免新配置问题影响用户 性能优化：虽然重定向开销小，但高并发时需确保服务器性能 五、总结 Nginx重定向方案将复杂的地址管理问题转化为简单的配置变更，实现了\u0026quot;一次分发，永久可用\u0026quot;的理想状态。这种解耦思维不仅适用于文件分发场景，在API网关、微服务路由等场景中同样具有借鉴意义。对于中小型项目而言，这是成本最低、效果最直接的解决方案，值得每一位开发者掌握。\n最后推荐一下我的豆子域名管家客户端，这个客户端可以解决域名证书到期忘记更换证书的问题。我是为了解决这个问题而想出来的这个方案。这是我的新的下载地址：https://lab.91demo.top/b011\n这样当我修改下载地址时，我不用再变更这个地址，比如我最近刚刚优化了域名扫描逻辑，上传了新的版本，它生成了一个新的下载地址。\n当然这个方案还是有一点瑕疵，它需要每次更新都登录服务器修改nginx配置文件，这对于非技术人员非常不方便。后期我还有再次进行优化，我将使用小程序完成新地址的更换。大致思路如下：开发一个轻量的Go服务，提供接口给小程序提交新的地址，然后保存编号和新地址到数据库，例如上面的b011是编号，它对应新的下载URL地址。然后使用Nginx代理这个服务，当用户访问b011时查询它的URL地址，然后返回301和新的地址。这样就不用每次都登录服务器修改Nginx配置，在手机上也方便操作，提供了极大的便利性，也可以交给非技术人员进行维护。\n","permalink":"https://blog.91demo.top/wiki/nginx301.html","summary":"\u003cp\u003e在移动应用分发、文件共享等场景中，我们常常会遇到这样的困境：云存储平台（如阿里云OSS、腾讯云COS、蓝奏云等）上传的文件地址一旦生成就无法修改，但客户端版本频繁迭代时，每次更新都需要重新生成下载链接和二维码。用户端需要不断更新访问入口，体验极差。我就碰到了此类问题，在上一篇文章，我介绍了自己的客户端，并分享了下载地址。但我发现当我需要重新升级版本时，已经生成的地址将无法变更。在我的文章中，我直接使用了文件下载地址。这就让我急需解决如何实现\u0026quot;一次分发，永久可用\u0026quot;的优雅方案？Nginx反向代理配合301重定向，正是解决这一痛点的利器。\u003c/p\u003e\n\u003ch2 id=\"一问题根源云存储的地址锁定机制\"\u003e一、问题根源：云存储的地址锁定机制\u003c/h2\u003e\n\u003cp\u003e大多数云存储服务采用\u0026quot;对象存储\u0026quot;模式，文件上传后生成固定URL，该地址包含存储桶名称、地域、文件路径等不可变信息。这种设计虽然保证了数据一致性，却带来了分发难题：每次版本更新，开发者必须重新上传文件、生成新地址、制作新二维码，用户需要重新扫描或获取新链接。对于频繁迭代的应用，这种重复劳动不仅效率低下，更可能因用户未及时更新而影响使用体验。\u003c/p\u003e\n\u003ch2 id=\"二解决方案nginx重定向的架构设计\"\u003e二、解决方案：Nginx重定向的架构设计\u003c/h2\u003e\n\u003cp\u003e核心思路是地址解耦——将\u0026quot;物理存储地址\u0026quot;与\u0026quot;用户访问入口\u0026quot;分离。通过Nginx服务器作为中间层，用户始终访问固定域名，Nginx根据配置将请求重定向到实际的云存储地址。当文件更新时，只需修改Nginx配置中的目标地址，用户端的访问入口保持不变。\u003c/p\u003e\n\u003cp\u003e具体架构如下：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e用户 → 固定域名/短链接 → Nginx服务器（301重定向） → 云存储实际地址\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e这种设计实现了三个关键目标：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e入口稳定性：用户扫描的二维码或保存的链接永久有效\u003c/li\u003e\n\u003cli\u003e维护灵活性：后台地址变更只需修改Nginx配置，无需重新分发\u003c/li\u003e\n\u003cli\u003e成本可控性：Nginx只做重定向，流量消耗极低，普通云服务器即可承载\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三技术实现从配置到部署\"\u003e三、技术实现：从配置到部署\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e环境准备购买一台云服务器（1核1G配置即可），安装Nginx，将自有域名解析到服务器IP。推荐申请SSL证书启用HTTPS。\u003c/li\u003e\n\u003cli\u003eNginx配置示例\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eserver {\n    listen 80;\n    server_name your-domain.com;\n    \n    # 方案一：直接重定向\n    location /download/app.apk {\n        return 301 https://bucket.oss-cn-region.aliyuncs.com/v2.0/app.apk;\n    }\n    \n    # 方案二：通配符匹配（更灵活）\n    location ~ ^/app/(.*)$ {\n        rewrite ^/app/(.*)$ https://new-bucket.oss-cn-region.aliyuncs.com/$1 permanent;\n    }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003col start=\"3\"\u003e\n\u003cli\u003e重定向类型选择\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003e301永久重定向：搜索引擎会更新索引，客户端会缓存重定向结果，适合生产环境\u003c/li\u003e\n\u003cli\u003e302临时重定向：每次请求都会重定向，适合测试阶段频繁变更的场景\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"4\"\u003e\n\u003cli\u003e二维码生成\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e将固定地址（如https://your-domain.com/app/v2.0.apk）生成二维码，用户扫描即可下载。后续版本更新时，只需修改Nginx配置中的目标地址，二维码无需重新制作。\u003c/p\u003e\n\u003ch2 id=\"四方案优势与注意事项\"\u003e四、方案优势与注意事项\u003c/h2\u003e\n\u003cp\u003e核心优势\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e零用户感知：用户无需关心后台地址变化\u003c/li\u003e\n\u003cli\u003e维护成本低：修改配置即可，无需重新上传二维码\u003c/li\u003e\n\u003cli\u003e扩展性强：可支持多版本共存、灰度发布等复杂场景\u003c/li\u003e\n\u003cli\u003e兼容性好：支持各种客户端（浏览器、APP、微信等）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e实施建议\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e测试先行：在测试环境验证重定向逻辑，确保目标地址正确\u003c/li\u003e\n\u003cli\u003e监控告警：配置Nginx日志监控，及时发现重定向失败问题\u003c/li\u003e\n\u003cli\u003e备份机制：保留旧版本配置一段时间，避免新配置问题影响用户\u003c/li\u003e\n\u003cli\u003e性能优化：虽然重定向开销小，但高并发时需确保服务器性能\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"五总结\"\u003e五、总结\u003c/h2\u003e\n\u003cp\u003eNginx重定向方案将复杂的地址管理问题转化为简单的配置变更，实现了\u0026quot;一次分发，永久可用\u0026quot;的理想状态。这种解耦思维不仅适用于文件分发场景，在API网关、微服务路由等场景中同样具有借鉴意义。对于中小型项目而言，这是成本最低、效果最直接的解决方案，值得每一位开发者掌握。\u003c/p\u003e\n\u003cp\u003e最后推荐一下我的豆子域名管家客户端，这个客户端可以解决域名证书到期忘记更换证书的问题。我是为了解决这个问题而想出来的这个方案。这是我的新的下载地址：\u003ca href=\"https://lab.91demo.top/b011\"\u003ehttps://lab.91demo.top/b011\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e这样当我修改下载地址时，我不用再变更这个地址，比如我最近刚刚优化了域名扫描逻辑，上传了新的版本，它生成了一个新的下载地址。\u003c/p\u003e\n\u003cp\u003e当然这个方案还是有一点瑕疵，它需要每次更新都登录服务器修改nginx配置文件，这对于非技术人员非常不方便。后期我还有再次进行优化，我将使用小程序完成新地址的更换。大致思路如下：开发一个轻量的Go服务，提供接口给小程序提交新的地址，然后保存编号和新地址到数据库，例如上面的b011是编号，它对应新的下载URL地址。然后使用Nginx代理这个服务，当用户访问b011时查询它的URL地址，然后返回301和新的地址。这样就不用每次都登录服务器修改Nginx配置，在手机上也方便操作，提供了极大的便利性，也可以交给非技术人员进行维护。\u003c/p\u003e","title":"巧用Nginx重定向解决云存储文件地址变更的痛点"},{"content":"域名过期导致业务中断、流量缺失、品牌受损的案例比比皆是。手动记录域名的到期时间？是否会忘记，漏看？\n我以前曾介绍过，我在微信小程序实现了域名证书监控功能。但是担心隐私，功能限制。这次我带来了豆子域名管家，本地化运行。\n经过数月的规划和开发测试，豆子域名管家终于可以使用了。这是一款完全本地运行、支持批量导入管理以及可以企微和钉钉通知的域名证书检测工具。旨在解决域名过期监控难题。\n特性 传统方式 豆子域名管家 运行方式 依赖云端服务 纯本地运行，数据不出本地 批量处理 手动逐个查询 一键导入，批量删除 通知渠道 单一（邮件/短信） 钉钉+企微双通道，支持Markdown格式 自定义提醒 固定时间提醒 可配置通知时间，提前预警天数 系统集成 无 系统托盘常驻，后台静默运行 隐私安全 数据上传第三方 域名数据和配置数据存本地 技术架构亮点：\n工具基于Wails3框架构建，采用Vue3+NaiveUI前端技术栈，确保界面简洁美观，交互流畅，支持暗黑/浅色两种主题。 本地证书检测引擎，直接调用系统网络库进行TLS检测，无需依赖外部API。 多线程并发处理，使用go的特性批量进行域名检测。 跨平台兼容：支持Windows、macOS、Linux系统。 配置持久化，所有配置本地存储，重启后自动恢复。 支持域名证书检测，域名到期时间检测。 软件运行界面预览：\n控制面板\n配置页面\n基本操作指南：\n1，导入域名我们需要把要监控的域名按行录入到txt文档中，可以在软件配置界面下载示例模板，当准备完成后，选择刚才的文件，然后验证。没有问题后，点击确认导入即可。\n2，配置机器人目前工具支持钉钉机器人和企业微信机器人，支持Markdown格式消息，可以查点击推送预览效果按钮查看推送效果。当输入机器人配置后，可以验证测试，当收到消息后说明配置成功，点击保存即可。可以同时配置企微和钉钉。这样会同时推送两份通知。\n3，配置推送通知策略目前工具扫描调度间隔固定24小时，可以配置通知时间，和告警天数间隔。在通知时间，系统会将当天扫描的结果报表推送到机器人。如果用户更新某些域名证书后，可以在监控面板进行手动刷新。想查看效果，可以把通知时间设置为当前时间加几分钟，当到达时间后，将推送报表。确认无误后，可以调整为真正的推送时间。\n4，系统托盘运行当完成域名导入和机器人以及通知策略配置后，可以关闭窗口，工具将自动缩放到系统托盘。如果需要退出，需要右键系统托盘退出。注意，如果退出工具，将不能监控域名，因为这是一个本地工具。所以需要工具长期运行。\n5，监控面板监控面板的仪表板显示你的域名配置项统计信息，表格显示监视的域名列表。域名可以搜索，刷新以及删除。域名按照过期、告警、正常顺序排序。请查看最前面域名并及时处理。\n工具按照自己的真实需求开发，如果你需要尝试，可以通过下方下载链接。目前仅提供了Windows版本。\nhttps://91demo.top/b011\n如果您有任何问题和建议，欢迎反馈和交流。\n","permalink":"https://blog.91demo.top/go/sslchecker.html","summary":"\u003cp\u003e域名过期导致业务中断、流量缺失、品牌受损的案例比比皆是。手动记录域名的到期时间？是否会忘记，漏看？\u003c/p\u003e\n\u003cp\u003e我以前曾介绍过，我在微信小程序实现了域名证书监控功能。但是担心隐私，功能限制。这次我带来了豆子域名管家，本地化运行。\u003c/p\u003e\n\u003cp\u003e经过数月的规划和开发测试，豆子域名管家终于可以使用了。这是一款完全本地运行、支持批量导入管理以及可以企微和钉钉通知的域名证书检测工具。旨在解决域名过期监控难题。\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e特性\u003c/th\u003e\n          \u003cth\u003e传统方式\u003c/th\u003e\n          \u003cth\u003e豆子域名管家\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e运行方式\u003c/td\u003e\n          \u003ctd\u003e依赖云端服务\u003c/td\u003e\n          \u003ctd\u003e纯本地运行，数据不出本地\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e批量处理\u003c/td\u003e\n          \u003ctd\u003e手动逐个查询\u003c/td\u003e\n          \u003ctd\u003e一键导入，批量删除\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e通知渠道\u003c/td\u003e\n          \u003ctd\u003e单一（邮件/短信）\u003c/td\u003e\n          \u003ctd\u003e钉钉+企微双通道，支持Markdown格式\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e自定义提醒\u003c/td\u003e\n          \u003ctd\u003e固定时间提醒\u003c/td\u003e\n          \u003ctd\u003e可配置通知时间，提前预警天数\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e系统集成\u003c/td\u003e\n          \u003ctd\u003e无\u003c/td\u003e\n          \u003ctd\u003e系统托盘常驻，后台静默运行\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e隐私安全\u003c/td\u003e\n          \u003ctd\u003e数据上传第三方\u003c/td\u003e\n          \u003ctd\u003e域名数据和配置数据存本地\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e技术架构亮点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e工具基于Wails3框架构建，采用Vue3+NaiveUI前端技术栈，确保界面简洁美观，交互流畅，支持暗黑/浅色两种主题。\u003c/li\u003e\n\u003cli\u003e本地证书检测引擎，直接调用系统网络库进行TLS检测，无需依赖外部API。\u003c/li\u003e\n\u003cli\u003e多线程并发处理，使用go的特性批量进行域名检测。\u003c/li\u003e\n\u003cli\u003e跨平台兼容：支持Windows、macOS、Linux系统。\u003c/li\u003e\n\u003cli\u003e配置持久化，所有配置本地存储，重启后自动恢复。\u003c/li\u003e\n\u003cli\u003e支持域名证书检测，域名到期时间检测。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e软件运行界面预览：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e控制面板\u003cbr\u003e\n\u003cimg alt=\"控制面板\" loading=\"lazy\" src=\"/images/notes/b01panel.webp\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e配置页面\u003cbr\u003e\n\u003cimg alt=\"配置页面\" loading=\"lazy\" src=\"/images/notes/b01config.webp\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e基本操作指南：\u003cbr\u003e\n1，导入域名我们需要把要监控的域名按行录入到txt文档中，可以在软件配置界面下载示例模板，当准备完成后，选择刚才的文件，然后验证。没有问题后，点击确认导入即可。\u003cbr\u003e\n2，配置机器人目前工具支持钉钉机器人和企业微信机器人，支持Markdown格式消息，可以查点击推送预览效果按钮查看推送效果。当输入机器人配置后，可以验证测试，当收到消息后说明配置成功，点击保存即可。可以同时配置企微和钉钉。这样会同时推送两份通知。\u003cbr\u003e\n3，配置推送通知策略目前工具扫描调度间隔固定24小时，可以配置通知时间，和告警天数间隔。在通知时间，系统会将当天扫描的结果报表推送到机器人。如果用户更新某些域名证书后，可以在监控面板进行手动刷新。想查看效果，可以把通知时间设置为当前时间加几分钟，当到达时间后，将推送报表。确认无误后，可以调整为真正的推送时间。\u003cbr\u003e\n4，系统托盘运行当完成域名导入和机器人以及通知策略配置后，可以关闭窗口，工具将自动缩放到系统托盘。如果需要退出，需要右键系统托盘退出。注意，如果退出工具，将不能监控域名，因为这是一个本地工具。所以需要工具长期运行。\u003cbr\u003e\n5，监控面板监控面板的仪表板显示你的域名配置项统计信息，表格显示监视的域名列表。域名可以搜索，刷新以及删除。域名按照过期、告警、正常顺序排序。请查看最前面域名并及时处理。\u003c/p\u003e\n\u003cp\u003e工具按照自己的真实需求开发，如果你需要尝试，可以通过下方下载链接。目前仅提供了Windows版本。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://91demo.top/b011\"\u003ehttps://91demo.top/b011\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果您有任何问题和建议，欢迎反馈和交流。\u003c/p\u003e","title":"告别域名过期焦虑：基于 Go + Wails 3 开发“豆子域名管家”，实现批量监测与企微钉钉预警"},{"content":"最近开发完 Mole-go，想给它做个网站用来展示和下载。但我这个后端糙汉子，样式真搞不定，求助 AI 调了半天还是差点意思。最头疼的是，手机端调试得一遍遍输 IP，给朋友演示也得发一串 IP 端口，太不专业了！于是我一顿折腾，搞出了这套方案……\n为了解决这些痛点，我摸索出了一套“黄金组合”：Dufs + Mole-go + FRP + Caddy。这套方案打通了从本地到公网域名的全链路，实现了自动 HTTPS、域名访问以及极致的访问体验。\n第一步：构建本地内容基石（Dufs） 一切的起点是本地文件服务。我选择使用 Dufs 作为静态服务器。它极其轻量，支持上传、搜索、打包下载甚至 WebDAV，是我演示 Web 应用或分发安装包的首选。\n通过简单的命令，我在本地 5000 端口启动了服务。虽然此时它还被“困”在局域网内，但它为后续的展示提供了稳固的基础。\n第二步：突破局域网束缚（FRP 与 Mole-go） 为了让公网流量能精准触达内网，我采用了经典的 FRP 方案，但在客户端层面，我使用了自己开发的 Mole-go。\n服务端 (FRP Server)：部署在具备公网 IP 的云服务器上，充当流量中转站。这个服务器配置可以很低，网站服务都在本地电脑，如果本地有数据库，也非常方便调试。\n客户端 (Mole-go)：这是我为 FRP 打造的桌面管理客户端。它封装了 frpc 核心，不仅提供了直观的 UI，还通过系统托盘设计彻底解决了“关闭窗口即断连”的痛点。\n使用 Mole-go，我可以将本地 5000 端口通过加密隧道安全地映射到云端。它出色的资源管理和连接稳定性，确保了演示过程中即便网络波动，链接依然稳固如初。\n第三步：优雅的网关入口（Caddy） 即便流量已到达公网，我也不希望朋友们通过 http://IP:端口 这种生硬的方式访问。我追求的是“域名+HTTPS”的专业感，这不仅是为了美观，更是为了开发环境需求，下次开发公众号等必须 HTTPS 环境时可以拿来就直接使用。\n我选择了 Caddy 担任“守门人”。Caddy 的魅力在于其近乎零配置的 自动 HTTPS 功能。看中了它的简单方便，非常符合我的场景。在 Caddyfile 中，我只需写下：\nexample.com { reverse_proxy localhost:7000 # 指向 FRP 映射出的本地端口 } 仅需这一行配置，Caddy 就会自动搞定 SSL 证书的申请与续签。当访问者输入域名时，映入眼帘的是受信任的绿色小锁头，所有的复杂端口逻辑都被完美隐藏。\n第四步：最终的演示时刻 配置生效后，我在 5G 网络下打开手机，输入域名——秒开！画面清晰地展示了 Dufs 托管的网站内容。通过 Caddy 的高效转发与 Mole-go 稳定的隧道，整个过程丝滑顺畅。我再也不用频繁地将代码打包上传到生产服务器，只需在本地修改，远端朋友就能实时刷新看到效果。\n这种方案不仅节省了部署的时间成本，更赋予了演示过程一种“极致的专业感”。当网站最终打磨完成，朋友点头称赞时，我才会将其正式部署到生产环境并切换为 Nginx 代理。\n思考 Dufs + Mole-go + FRP + Caddy 的组合，将本地开发的灵活性与公网访问的便捷性结合到了极致。对于需要频繁进行跨端调试或远程演示的开发者来说，这不仅是一套技术方案，更是一次效率的革命。\n","permalink":"https://blog.91demo.top/devops/mole-devenv.html","summary":"\u003cp\u003e最近开发完 Mole-go，想给它做个网站用来展示和下载。但我这个后端糙汉子，样式真搞不定，求助 AI 调了半天还是差点意思。最头疼的是，手机端调试得一遍遍输 IP，给朋友演示也得发一串 IP 端口，太不专业了！于是我一顿折腾，搞出了这套方案……\u003c/p\u003e\n\u003cp\u003e为了解决这些痛点，我摸索出了一套“黄金组合”：Dufs + Mole-go + FRP + Caddy。这套方案打通了从本地到公网域名的全链路，实现了自动 HTTPS、域名访问以及极致的访问体验。\u003c/p\u003e\n\u003ch2 id=\"第一步构建本地内容基石dufs\"\u003e第一步：构建本地内容基石（Dufs）\u003c/h2\u003e\n\u003cp\u003e一切的起点是本地文件服务。我选择使用 Dufs 作为静态服务器。它极其轻量，支持上传、搜索、打包下载甚至 WebDAV，是我演示 Web 应用或分发安装包的首选。\u003c/p\u003e\n\u003cp\u003e通过简单的命令，我在本地 5000 端口启动了服务。虽然此时它还被“困”在局域网内，但它为后续的展示提供了稳固的基础。\u003c/p\u003e\n\u003ch2 id=\"第二步突破局域网束缚frp-与-mole-go\"\u003e第二步：突破局域网束缚（FRP 与 Mole-go）\u003c/h2\u003e\n\u003cp\u003e为了让公网流量能精准触达内网，我采用了经典的 FRP 方案，但在客户端层面，我使用了自己开发的 Mole-go。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e服务端 (FRP Server)：部署在具备公网 IP 的云服务器上，充当流量中转站。这个服务器配置可以很低，网站服务都在本地电脑，如果本地有数据库，也非常方便调试。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e客户端 (Mole-go)：这是我为 FRP 打造的桌面管理客户端。它封装了 frpc 核心，不仅提供了直观的 UI，还通过系统托盘设计彻底解决了“关闭窗口即断连”的痛点。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e使用 Mole-go，我可以将本地 5000 端口通过加密隧道安全地映射到云端。它出色的资源管理和连接稳定性，确保了演示过程中即便网络波动，链接依然稳固如初。\u003c/p\u003e\n\u003ch2 id=\"第三步优雅的网关入口caddy\"\u003e第三步：优雅的网关入口（Caddy）\u003c/h2\u003e\n\u003cp\u003e即便流量已到达公网，我也不希望朋友们通过 http://IP:端口 这种生硬的方式访问。我追求的是“域名+HTTPS”的专业感，这不仅是为了美观，更是为了开发环境需求，下次开发公众号等必须 HTTPS 环境时可以拿来就直接使用。\u003c/p\u003e\n\u003cp\u003e我选择了 Caddy 担任“守门人”。Caddy 的魅力在于其近乎零配置的 自动 HTTPS 功能。看中了它的简单方便，非常符合我的场景。在 Caddyfile 中，我只需写下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eexample.com {    \n    reverse_proxy localhost:7000  # 指向 FRP 映射出的本地端口\n    }\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e仅需这一行配置，Caddy 就会自动搞定 SSL 证书的申请与续签。当访问者输入域名时，映入眼帘的是受信任的绿色小锁头，所有的复杂端口逻辑都被完美隐藏。\u003c/p\u003e","title":"拒绝频繁上传：基于 Dufs + Mole-go (FRP) 快速搭建高效的内网穿透演示环境"},{"content":"在实现了使用VOIP客户端拨打8000号码后，播报语音验证码的功能后，我发现了一个最大的缺点，就是这需要用户主动去操作。这对于想使用API集成无法实现。\n在思考之后，我决定使用一个可以调用API就呼叫VOIP客户端，当用户接通后，播报语音验证码的功能。当实现这个功能后，它的好处是显而易见的。比如，可以集成到嵌入式，集成到第三方网站。\n那么该如何实现它呢？\n一、 系统原理 传统的拨号方案（Dialplan）是静态的，而 ARI 允许我们动态控制。整个“API 触发呼叫并播报”的流程如下：\n触发阶段：第三方系统通过 API 向 Go 服务发送呼叫请求（包含目标 ID 和验证码）。 呼叫发起（Originate）：Go 服务调用 Asterisk ARI 的 /channels 接口。此时 Asterisk 会尝试向 PJSIP 终端（或通过中继向手机）发起呼叫。 接通监听（Stasis Start）：一旦用户接起电话，该通道会被移交给一个名为 Stasis 的应用。此时 Go 服务会收到一个“通道已接通”的 WebSocket 事件。 语音合成与播放：Go 服务识别到接通后，调用播报指令（可以播放预录音文件，或对接 TTS 引擎生成的语音流）。 挂断处理：播报完毕后，服务发送挂断指令，释放资源。 二、系统架构 [第三方API] --\u0026gt; [Go 后端服务] --(REST API)--\u0026gt; [Asterisk ARI] | | (WebSocket) (PJSIP/IMS) | | [接通状态回调] \u0026lt;--- [用户终端接听] 三、 核心代码实现 (Golang) 假设你使用了 GitHub 上的 go-ari 库。\n1. 初始化 ARI 客户端 import ( \u0026#34;github.com/v5\u0026#34; \u0026#34;github.com/v5/client/native\u0026#34; ) // 连接到 Asterisk ARI cl, err := native.Connect(\u0026amp;native.Options{ Application: \u0026#34;voice-verify\u0026#34;, // 必须与 asterisk.conf 配置一致 Username: \u0026#34;admin\u0026#34;, Password: \u0026#34;password\u0026#34;, URL: \u0026#34;http://localhost:8088/ari\u0026#34;, }) 2. 实现呼叫并播报逻辑 这是核心逻辑：接收参数 -\u0026gt; 发起呼叫 -\u0026gt; 监听接通 -\u0026gt; 播放语音。\na. 接收参数\n// 呼叫 router.POST(\u0026#34;/call\u0026#34;, func(c *gin.Context) { var req arimanager.CallRequest if err := c.ShouldBindJSON(\u0026amp;req); err != nil { c.JSON(http.StatusBadRequest, gin.H{\u0026#34;error\u0026#34;: err.Error()}) return } callID, err := ariManager.ExecuteCall(req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{\u0026#34;error\u0026#34;: err.Error()}) return } c.JSON(http.StatusAccepted, gin.H{ \u0026#34;callId\u0026#34;: callID, \u0026#34;status\u0026#34;: \u0026#34;queued\u0026#34;, }) }) 当收到参数时，就进行内部呼叫，并返回第三方，目前呼叫已经进入队列。\nb. 发起呼叫\nfunc (m *ARIManager) makeARICall(target string, callType CallType, payload map[string]string) (string, error) { // 构建变量映射 variables := map[string]string{ \u0026#34;CALL_ID\u0026#34;: target, // 自定义变量 \u0026#34;CALL_TYPE\u0026#34;: string(callType), } // 添加 payload 到变量 for key, value := range payload { variables[fmt.Sprintf(\u0026#34;CALL_%s\u0026#34;, strings.ToUpper(key))] = value } // 发起 ARI 呼叫 channel, err := m.client.Channel().Originate(nil, ari.OriginateRequest{ Endpoint: fmt.Sprintf(\u0026#34;PJSIP/%s\u0026#34;, target), App: m.client.ApplicationName(), Extension: \u0026#34;s\u0026#34;, Context: \u0026#34;from-ari\u0026#34;, Timeout: 30, // 超时时间（秒） CallerID: fmt.Sprintf(\u0026#34;\\\u0026#34;Call System\\\u0026#34; \u0026lt;%s\u0026gt;\u0026#34;, m.client.ApplicationName()), Variables: variables, }) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;ARI originate failed: %v\u0026#34;, err) } return channel.ID(), nil } 这里执行真正的呼叫，它会查找Context上下文，并进入到对应拨号计划中，将上面的信息塞入通道。\nc. 监听接通\nfunc (m *ARIManager) handleVerification(ctx context.Context, h *ari.ChannelHandle) { if err := h.Answer(); err != nil { shared.Log.Error(\u0026#34;failed to answer verification call\u0026#34;, \u0026#34;error\u0026#34;, err) return } // 从变量获取验证码 code, _ := h.GetVariable(\u0026#34;CALL_CODE\u0026#34;) // 播放验证码 if err := m.playVerificationCode(ctx, h, code); err != nil { shared.Log.Error(\u0026#34;failed to play verification code\u0026#34;, \u0026#34;error\u0026#34;, err) return } shared.Log.Info(\u0026#34;verification playback completed\u0026#34;, \u0026#34;code\u0026#34;, code) } 当在应用中收到事件后，马上进行状态，等待用户接听，用户接听之后，从通道中获取验证码，然后开始调用播放方法。\nd. 播放语音\nfunc (m *ARIManager) playVerificationCode(ctx context.Context, h *ari.ChannelHandle, code string) error { if err := play.Play(ctx, h, play.URI(\u0026#34;sound:beep\u0026#34;)).Err(); err != nil { return err } // 播放欢迎提示 if err := play.Play(ctx, h, play.URI(\u0026#34;sound:vcode/code\u0026#34;)).Err(); err != nil { return err } // 逐位播放数字 for _, digit := range code { digitSound := fmt.Sprintf(\u0026#34;sound:vcode/%c\u0026#34;, digit) if err := play.Play(ctx, h, play.URI(digitSound)).Err(); err != nil { return err } // 数字间短暂暂停 // time.Sleep(100 * time.Millisecond) } // 播放结束提示 if err := play.Play(ctx, h, play.URI(\u0026#34;sound:vcode/thanks\u0026#34;)).Err(); err != nil { return err } return nil } 这是真正播放验证码的地方，它调用系统的play函数将音频流传输给用户。\n四、 关键配置 (Asterisk 端) 为了让 Go 代码接管通话，你需要配置 ari.conf 开启接口，并在拨号方案中定义应用。\n1. http.conf [general] enabled=yes bindaddr=127.0.0.1 bindport=8088 2. ari.conf [general] enabled = yes pretty = yes [admin] type = user read_only = no password = password 3. extensions.conf 虽然是 API 触发，但涉及到内部呼转，还需要配置拨号计划，需要从通道中获取值：\n[from-ari] exten =\u0026gt; _X.,1,NoOp(External API Call) same =\u0026gt; n,Stasis(voice-verify) ; 进入 Go 监听的应用名 same =\u0026gt; n,Hangup() 当没有对应的拨号计划时，就转到这个应用。\n五、 这种方式的优势 无缝集成：通过标准 HTTP RESTful API，嵌入式设备（ESP8266/ESP32）只需发送一个简单的 POST 请求给 Go 后端，或者接收到MQTT某的指令，Go调用POST请求，就能让指定终端响起电话。 高到达率：语音验证码比短信更难被拦截软件屏蔽，且自带“强提醒”属性（响铃）。 动态可扩展： 本地测试：呼叫 PJSIP/8000 等 VoIP 软电话。 生产环境：对接运营商的 SIP 中继 (SIP Trunk)，呼叫地址改为 PJSIP/手机号@运营商网关，即可实现全国手机拨打。 成本控制：使用开源的 Asterisk，只需支付运营商的分钟数费用，无需支付昂贵的第三方聚合服务费。 六、 思考 通过 Go + ARI + Asterisk 的组合，成功将传统的 VoIP 通信转换为了 通信能力平台 (CPaaS)。这种方案不仅解决了用户主动操作的痛点，更为后续的自动化预警（如嵌入式传感器触发报警电话）打下了坚实的技术基础。\n","permalink":"https://blog.91demo.top/go/vcode-call.html","summary":"\u003cp\u003e在实现了使用VOIP客户端拨打\u003ccode\u003e8000\u003c/code\u003e号码后，播报语音验证码的功能后，我发现了一个最大的缺点，就是这需要用户主动去操作。这对于想使用API集成无法实现。\u003c/p\u003e\n\u003cp\u003e在思考之后，我决定使用一个可以调用API就呼叫VOIP客户端，当用户接通后，播报语音验证码的功能。当实现这个功能后，它的好处是显而易见的。比如，可以集成到嵌入式，集成到第三方网站。\u003c/p\u003e\n\u003cp\u003e那么该如何实现它呢？\u003c/p\u003e\n\u003ch2 id=\"一-系统原理\"\u003e一、 系统原理\u003c/h2\u003e\n\u003cp\u003e传统的拨号方案（Dialplan）是静态的，而 ARI 允许我们动态控制。整个“API 触发呼叫并播报”的流程如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e触发阶段：第三方系统通过 API 向 Go 服务发送呼叫请求（包含目标 ID 和验证码）。\u003c/li\u003e\n\u003cli\u003e呼叫发起（Originate）：Go 服务调用 Asterisk ARI 的 /channels 接口。此时 Asterisk 会尝试向 PJSIP 终端（或通过中继向手机）发起呼叫。\u003c/li\u003e\n\u003cli\u003e接通监听（Stasis Start）：一旦用户接起电话，该通道会被移交给一个名为 Stasis 的应用。此时 Go 服务会收到一个“通道已接通”的 WebSocket 事件。\u003c/li\u003e\n\u003cli\u003e语音合成与播放：Go 服务识别到接通后，调用播报指令（可以播放预录音文件，或对接 TTS 引擎生成的语音流）。\u003c/li\u003e\n\u003cli\u003e挂断处理：播报完毕后，服务发送挂断指令，释放资源。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"二系统架构\"\u003e二、系统架构\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[第三方API] --\u0026gt; [Go 后端服务] --(REST API)--\u0026gt; [Asterisk ARI]\n                      |                             |\n                 (WebSocket)                    (PJSIP/IMS)\n                      |                             |\n                 [接通状态回调]     \u0026lt;---         [用户终端接听]\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"三-核心代码实现-golang\"\u003e三、 核心代码实现 (Golang)\u003c/h2\u003e\n\u003cp\u003e假设你使用了 GitHub 上的 \u003ccode\u003ego-ari\u003c/code\u003e 库。\u003c/p\u003e\n\u003ch3 id=\"1-初始化-ari-客户端\"\u003e1. 初始化 ARI 客户端\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e (\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;github.com/v5\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;github.com/v5/client/native\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 连接到 Asterisk ARI\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003ecl\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003enative\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eConnect\u003c/span\u003e(\u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003enative\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eOptions\u003c/span\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eApplication\u003c/span\u003e:  \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;voice-verify\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#75715e\"\u003e// 必须与 asterisk.conf 配置一致\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eUsername\u003c/span\u003e:     \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;admin\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ePassword\u003c/span\u003e:     \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eURL\u003c/span\u003e:          \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;http://localhost:8088/ari\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e})\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"2-实现呼叫并播报逻辑\"\u003e2. 实现呼叫并播报逻辑\u003c/h3\u003e\n\u003cp\u003e这是核心逻辑：接收参数 -\u0026gt; 发起呼叫 -\u0026gt; 监听接通 -\u0026gt; 播放语音。\u003c/p\u003e","title":"深度解析：基于 Asterisk 与 SIP 协议的私有化语音验证码呼叫原理"},{"content":"一、 缘起：为什么需要 mole-go？ 在开发微信公众号、调试支付接口、以及演示本地开发网站时，或由于服务器资源限制需要在本地部署服务时，frp 是不可或缺的内网穿透神器。然而，原生的 frpc 存在几个显著的痛点：\n运行隐形性差：必须开启命令行窗口，一旦误关服务即中断。 配置门槛高：新手难以记忆复杂的 .toml 参数。 为了解决这些问题，我开发了 mole-go。它是一个轻量级、跨平台的桌面客户端，旨在实现 frp 的配置、启动与监控一体化。我选择 Wails v3 则是看中了其原生渲染、系统托盘支持、Go 强力后端以及极小的打包体积。\n二、 核心架构：Go + Wails v3 的化学反应 mole-go 采用了经典的“UI-Backend-Service”三层架构：\nWails UI：负责前端展示，通过事件驱动（Event-Driven）与后端交互。 Go Backend：核心大脑，负责业务逻辑、进程管理与系统级 API 调用。 frpc 二进制：底层服务，通过 Go 的 embed 特性内嵌到二进制文件中。 三、 关键实现细节：从命令行到图形化的进化 前端：从“面条代码”到模块化数据驱动\n早期版本中，我直接采用 window.startFrp，window.stopFrp这样的写法，导致代码碎片化严重，以及管理app运行状态不方便。在 mole-go 的正式版中，我将其重构为数据驱动模式，类似Vue，由数据驱动界面： 模块化封装：定义全局 window.App 对象，将数据状态与行为（Methods）统一封装，使代码结构清晰。 动态 UI 组件：针对 HTTP、TCP、UDP 等不同代理模型，不再机械地堆砌 HTML 片段，而是通过逻辑判断实现“按需渲染”，大大精简了 DOM 结构。 后端：全局实例与事件机制\n为了保证服务层（Service）能随时与 UI 通信，我设计了一个全局 App 实例，这样可以方便得调用和管理。 状态约定：前后端约定一套状态码，通过 Wails v3 的 Events 机制，后端可以主动向前端推送 frpc 的运行状态、日志等信息。 独立服务层：将 frp 相关逻辑抽离到专门的文件中，通过 Wails 的 Binding 暴露给前端，保持代码的解耦。 系统深度集成 系统托盘（System Tray）：利用 Wails v3 原生的托盘支持，实现了“关闭即隐藏”逻辑。 外部链接调用：使用 wails3自带的 Browser.OpenURL 方法，确保点击文档链接时能正确唤起系统浏览器。 可参考项目源码。\n四、 深度避坑指南：跨平台开发的硬核干货 在开发过程中，碰到了以下问题，我也写出了自己的解决思路。\n进程管理：优雅地“杀死”子进程\n坑：主程序直接退出时，frpc 没有正常关闭往往会变成“僵尸进程”。\n解：优化frpc关闭逻辑，并针对不同平台实现不同清理逻辑。 Windows：使用 taskkill /F /T /PID 确保杀掉整个进程树。 Unix-like：使用 syscall.Kill(-pid, syscall.SIGKILL) 杀掉进程组。 跨平台适配：Build Tags 的妙用\n坑：不同平台的路径、命令处理都不相同，需要区分平台，我代码中使用了大量的if runtime.GOOS代码，这个是运行时判断，但是在编译跨平台时，Github Action会报错。\n解：采用在文件中嵌入build标签，可以在go编译在相应平台编译相关平台代码，可以实现以下效果： 二进制嵌入：使用 go:embed 结合 Go Build Tags（如 //go:build windows），根据不同平台打包对应的 frpc 执行文件。 平台宏定义：启动逻辑和清理逻辑分别写在 service_windows.go 和 service_linux.go 中，编译时自动按需加载，避免了大量的 if runtime.GOOS == \u0026hellip; 代码，也解决了编译时报错的问题。 Wails v3 的生命周期管理\n坑：我先前在 Service 中释放资源，发现我无法有效杀掉frpc进程，我打印了日志，但是也没有发现相关输出日志。\n解：经过多种实践，我将资源释放逻辑（如关闭 frpc、清理临时文件）从 Service 层调整到 App 生命周期钩子中，在app创建的实例中，添加了属性OnShutdown，确保程序退出前所有子进程被正确销毁。\n日志 OOM 风险控制以及输出到前端优化\n坑：当frpc 日志量大时，前端 DOM 节点过多会有导致内存溢出的情况，以及当frpc大量日志输出时，会造成前端卡顿的情况。\n解：在Go后端添加了日志切片，用来存储日志，然后固定500毫秒发送到前端，同时在前端逻辑中加入循环队列缓存，固定保留最新的 1000 行日志，旧日志自动丢弃，也优化了渲染代码，确保 UI 长时间运行依然流畅。\n五、 性能与体验优化 单文件分发：以前计划是将frpc客户端通过打包的形式随主程序一块发布，但是发现这样有个很大的弊端，用户会看到两个文件，并且frpc很容易被本地电脑杀毒软件删除。我通过 embed 内嵌各平台 frpc 二进制方式，用户下载后只有一个文件，双击即可使用，同时在运行的时候释放frpc客户端，可以有效减缓杀毒问题发生。 静默运行：添加了系统托盘，通过托盘化设计，frpc 在后台静默工作，这个解决了frpc命令行窗口关闭就停止服务的问题，并且托盘可以仅在需要调整配置或查看日志时才唤起界面，极大地降低了心智负担。 六、 总结与未来计划 开发 mole 客户端的过程让我深切体会到：写出一个工具容易，但要做一个好用的工具很难。 从处理跨平台的路径差异，到精简前端的数据流，每一个细节的优化都是对开发者基本功的考验，也对技能提升有很大帮助。\n目前 mole 客户端已经能够满足日常生产环境的使用。未来的方向将专注于稳定性维护与兼容性测试，我希望它能成为 frp 用户手中最趁手的“瑞士军刀”。\n项目地址：GitHub - littletow/mole-go\n如果你觉得 mole-go 对你有帮助，欢迎前往 GitHub 为项目点个 Star ⭐，或者在 Issues 中提出你的建议！\n","permalink":"https://blog.91demo.top/go/mole-intro.html","summary":"\u003ch2 id=\"一-缘起为什么需要-mole-go\"\u003e一、 缘起：为什么需要 mole-go？\u003c/h2\u003e\n\u003cp\u003e在开发微信公众号、调试支付接口、以及演示本地开发网站时，或由于服务器资源限制需要在本地部署服务时，frp 是不可或缺的内网穿透神器。然而，原生的 frpc 存在几个显著的痛点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e运行隐形性差：必须开启命令行窗口，一旦误关服务即中断。\u003c/li\u003e\n\u003cli\u003e配置门槛高：新手难以记忆复杂的 .toml 参数。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e为了解决这些问题，我开发了 mole-go。它是一个轻量级、跨平台的桌面客户端，旨在实现 frp 的配置、启动与监控一体化。我选择 Wails v3 则是看中了其原生渲染、系统托盘支持、Go 强力后端以及极小的打包体积。\u003c/p\u003e\n\u003ch2 id=\"二-核心架构go--wails-v3-的化学反应\"\u003e二、 核心架构：Go + Wails v3 的化学反应\u003c/h2\u003e\n\u003cp\u003emole-go 采用了经典的“UI-Backend-Service”三层架构：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWails UI：负责前端展示，通过事件驱动（Event-Driven）与后端交互。\u003c/li\u003e\n\u003cli\u003eGo Backend：核心大脑，负责业务逻辑、进程管理与系统级 API 调用。\u003c/li\u003e\n\u003cli\u003efrpc 二进制：底层服务，通过 Go 的 embed 特性内嵌到二进制文件中。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三-关键实现细节从命令行到图形化的进化\"\u003e三、 关键实现细节：从命令行到图形化的进化\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e前端：从“面条代码”到模块化数据驱动\u003cbr\u003e\n早期版本中，我直接采用 window.startFrp，window.stopFrp这样的写法，导致代码碎片化严重，以及管理app运行状态不方便。在 mole-go 的正式版中，我将其重构为数据驱动模式，类似Vue，由数据驱动界面：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003e模块化封装：定义全局 window.App 对象，将数据状态与行为（Methods）统一封装，使代码结构清晰。\u003c/li\u003e\n\u003cli\u003e动态 UI 组件：针对 HTTP、TCP、UDP 等不同代理模型，不再机械地堆砌 HTML 片段，而是通过逻辑判断实现“按需渲染”，大大精简了 DOM 结构。\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e后端：全局实例与事件机制\u003cbr\u003e\n为了保证服务层（Service）能随时与 UI 通信，我设计了一个全局 App 实例，这样可以方便得调用和管理。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003e状态约定：前后端约定一套状态码，通过 Wails v3 的 Events 机制，后端可以主动向前端推送 frpc 的运行状态、日志等信息。\u003c/li\u003e\n\u003cli\u003e独立服务层：将 frp 相关逻辑抽离到专门的文件中，通过 Wails 的 Binding 暴露给前端，保持代码的解耦。\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e系统深度集成\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003e系统托盘（System Tray）：利用 Wails v3 原生的托盘支持，实现了“关闭即隐藏”逻辑。\u003c/li\u003e\n\u003cli\u003e外部链接调用：使用 wails3自带的 Browser.OpenURL 方法，确保点击文档链接时能正确唤起系统浏览器。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e可参考项目源码。\u003c/p\u003e","title":"深度实战：基于 Wails v3 与 Go 打造跨平台 FRP 桌面客户端 Mole-go 的技术架构与原理"},{"content":"Mole 项目已经开源，打算把仓库在 GitHub Release 页面中发布各个平台的安装包/二进制（像 frp 那样），已经完成脚本，可参考.github/workflows/release.yml，这个用来记录实现过程与遇到的坑，便于自己回顾。\n大致流程 查阅 GitHub Actions、goreleaser、actions/create-release、actions/upload-release-asset 等资料。 在仓库中写一个 matrix workflow，在 Windows / macOS / Ubuntu runner 上分别构建并打包。 修复构建中出现的依赖与环境差异问题。 将构建产物上传为 Release asset（先查找/创建 release，再上传各平台包）。 调试并保证上传在 Release 页面能看到各平台包。 今天遇到并解决的主要问题（按流程顺序） Ubuntu 图形库版本不一致\n问题：教程写的是 libwebkit2gtk 4.0，但在新版 Ubuntu runner 上只能安装 4.1。 处理：在 CI 的 apt 安装里兼容 4.0 和 4.1（尝试 4.1，或用 || 逻辑兼容两种包名）。 wails3 安装地址写错导致安装失败\n问题：go install 用错了模块路径，安装一直失败。 处理：修正为官方地址（例如 github.com/wailsapp/wails/v3/cmd/wails3@latest），并确保 GOPATH/bin 在 PATH 中。 wails3 的 -platform / -o 参数在我的环境不可用\n问题：官方文档的参数在实际运行时报错或不生效（不能指定输出名）。 处理：不完全依赖 CLI 的跨平台参数，在对应 runner 上运行构建命令并从 bin/ 下输出文件打包；如有必要向 upstream 提交 issue。 macOS 的平台特有代码需要 build tag，而不是运行时判断\n问题：以前用 runtime.GOOS 在运行时判断，但某些符号在编译期就需要区分，需用 build tags 来包含 macOS 特有实现。 处理：添加带 build tags 的文件来实现平台相关逻辑（避免运行时缺失符号）。 需要提前解压 resources/bin.zip（资源嵌入问题）\n场景：为避免本地误报或便于管理，部分资源以 zip 放在 resources/bin.zip，构建时需存在这些文件以便嵌入到二进制。 处理：在 workflow 中先解压到 resources/bin（使用 || true 容错），保证构建可以访问这些资源。 Windows runner 在 bash 环境下没有 zip，并且 PowerShell 与 bash 命令差异\n问题：在 Windows 上执行 zip 报 command not found，并且有些调试命令（如 ls -lh）在 PowerShell 不可用。 处理：把打包和调试脚本分为 Windows（使用 PowerShell 与 Compress-Archive）和非 Windows（bash + zip/tar）两套逻辑；Windows 的调试用 Get-Item / Get-ChildItem。 构建产物在 Actions 可见但不出现在 Release 页面（缺少上传到 Release 的步骤）\n问题：actions/upload-artifact 会把文件保存在 Actions 界面，但不会显示在仓库的 Release 页面。 处理：新增步骤——先创建或查找对应 tag 的 release（可创建为 draft），然后在每个 matrix job 中用 actions/upload-release-asset 上传各自平台的包到该 release 的 upload_url。 上传时出现 Validation Failed（already_exists）与权限问题\n问题：当 Release 已有同名 asset 时，API 会返回 already_exists。另外要确保 workflow 有写入 Release 的权限。 处理：在上传前用 API 列出 release 的 assets，若存在同名则先删除（用 actions/github-script 调用 API 删除），再上传；在 workflow 中设置 permissions: contents: write 确保有权限。 其他细节与注意点 使用 actions/github-script 时：脚本中不要再次 require('@actions/core') 等，因为该 action 会注入 core、github、context 等变量，重复声明会报错。 在每个 job 打包后加一个 debug 步骤（输出 PKG_PATH/PKG_NAME 并列出文件），确认文件路径与上传步骤一致。 upload-release-asset 必须使用 create-release 返回的 upload_url，并确保 asset_path 指向实际存在的文件。 Windows 的 PowerShell 步骤要用 pwsh 壳，文件路径与环境变量写法与 bash 不同。 为了未来改名更方便，把包名模板抽成变量（例如 PROJECT_NAME、VERSION、platform），构建脚本按变量生成包名，这样以后改名只要改一处模板。 Github Action 要点 create-release job：查找是否已有该 tag 的 release；若没有就创建为 draft（便于编辑），输出 upload_url 和 release_id。 build matrix job：各自 runner 上构建 -\u0026gt; 检测 bin/ 下的输出 -\u0026gt; 生成包（命名包含项目名、平台、版本）-\u0026gt; Windows 用 PowerShell 打包，非 Windows 用 zip/tar -\u0026gt; 上传前删除同名 asset（如存在）-\u0026gt; 上传到同一 release。 示例包名格式（当前）：mole_darwin_arm64_v1.0.0.zip（PROJECT_NAME 固定为 mole，VERSION 用 ${{ github.ref_name }}）。 后续可优化方向 若希望更多自动化特性（checksum、签名、Homebrew、snap、自动发布），考虑使用 goreleaser（对 Go 项目支持完善）。 保留 debug 步骤直到 workflow 稳定，再删除以节省日志与时间。 若担心覆盖历史 assets，可改包名策略（例如把版本放在前或加时间戳），或者在上传时保留旧资产并上传新名字。 如果 Wails CLI 在不同版本行为不同，关注 upstream issue 或官方示例，必要时固定 CLI 版本并在 CI 中使用该版本以避免不可预期改变。 小结 把 CI 做成能在 Release 页面自动产出多平台包并不简单：需要处理构建工具差异、runner 环境差异（依赖/压缩工具/PowerShell vs bash）、Release API 的并发与重名问题。今天的重点是把“分平台打包、单次创建/复用 release、上传前处理同名 asset”这三点做稳，琐碎但可复用，记录下来方便以后回顾与改进。\n","permalink":"https://blog.91demo.top/wiki/ghaction.html","summary":"\u003cp\u003e\u003ca href=\"https://github.com/littletow/mole-go\"\u003eMole 项目\u003c/a\u003e已经开源，打算把仓库在 GitHub Release 页面中发布各个平台的安装包/二进制（像 frp 那样），已经完成脚本，可参考.github/workflows/release.yml，这个用来记录实现过程与遇到的坑，便于自己回顾。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"大致流程\"\u003e大致流程\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e查阅 GitHub Actions、goreleaser、actions/create-release、actions/upload-release-asset 等资料。\u003c/li\u003e\n\u003cli\u003e在仓库中写一个 matrix workflow，在 Windows / macOS / Ubuntu runner 上分别构建并打包。\u003c/li\u003e\n\u003cli\u003e修复构建中出现的依赖与环境差异问题。\u003c/li\u003e\n\u003cli\u003e将构建产物上传为 Release asset（先查找/创建 release，再上传各平台包）。\u003c/li\u003e\n\u003cli\u003e调试并保证上传在 Release 页面能看到各平台包。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"今天遇到并解决的主要问题按流程顺序\"\u003e今天遇到并解决的主要问题（按流程顺序）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eUbuntu 图形库版本不一致\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e问题：教程写的是 libwebkit2gtk 4.0，但在新版 Ubuntu runner 上只能安装 4.1。\u003c/li\u003e\n\u003cli\u003e处理：在 CI 的 apt 安装里兼容 4.0 和 4.1（尝试 4.1，或用 \u003ccode\u003e||\u003c/code\u003e 逻辑兼容两种包名）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003ewails3 安装地址写错导致安装失败\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e问题：\u003ccode\u003ego install\u003c/code\u003e 用错了模块路径，安装一直失败。\u003c/li\u003e\n\u003cli\u003e处理：修正为官方地址（例如 \u003ccode\u003egithub.com/wailsapp/wails/v3/cmd/wails3@latest\u003c/code\u003e），并确保 \u003ccode\u003eGOPATH/bin\u003c/code\u003e 在 PATH 中。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003ewails3 的 \u003ccode\u003e-platform\u003c/code\u003e / \u003ccode\u003e-o\u003c/code\u003e 参数在我的环境不可用\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e问题：官方文档的参数在实际运行时报错或不生效（不能指定输出名）。\u003c/li\u003e\n\u003cli\u003e处理：不完全依赖 CLI 的跨平台参数，在对应 runner 上运行构建命令并从 \u003ccode\u003ebin/\u003c/code\u003e 下输出文件打包；如有必要向 upstream 提交 issue。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003emacOS 的平台特有代码需要 build tag，而不是运行时判断\u003c/p\u003e","title":"把项目做成像 frp 那样的多平台 Release"},{"content":"本文将带你从零开始，快速完成基于 Mole 的内网穿透服务部署，包含服务端 (frps) 与桌面客户端 (Mole/frpc) 的配置与排查要点。\n📖 核心概念 什么是 FRP？\nFRP (Fast Reverse Proxy) 是一款高性能的反向代理/内网穿透工具，它通过在公网服务器和内网客户端之间建立隧道，使外网可以访问内网服务（如 NAS、树莓派、开发环境等）。\n什么是 Mole？\nMole 是一款基于 wails3 开发的 FRP 桌面客户端，提供图形化管理界面，简化 frpc 的配置与使用：\n告别繁琐命令行 支持系统托盘常驻，防止误关窗口导致中断 支持 HTTP/HTTPS、TCP、UDP 等多种协议 🛠️ 部署前准备 一台拥有公网 IP 的低配云服务器（例如1核1G内存的阿里云、腾讯云等） FRP 服务端（frps）与客户端（frpc）二进制文件，建议使用 FRP Releases 的 v0.65.0 或更高版本 Mole 桌面客户端安装包（对应你操作系统的版本） 基本网络与防火墙管理权限 1. 服务端配置（frps） 在拥有公网IP的云服务器上部署frps服务端：\n下载并解压 FRP 服务端（以 Linux 为例）\n访问 FRP 的 Releases 页面下载对应版本并解压（示例为 v0.65.0 或更高）。\n编写 frps.toml 配置文件（示例）\n将以下内容写入 frps.toml，并根据实际需求调整端口与 token：\nbindPort = 7000 # 客户端连接端口（frpc 连接到此端口） vhostHTTPPort = 8080 # HTTP 映射的公网访问端口（可选） # 建议设置 auth.token 为复杂字符串，防止未授权连接 auth.token = \u0026#34;你的复杂密匙\u0026#34; 启动 frps（示例）\n在服务端目录运行：\n./frps -c frps.toml 推荐使用 systemd 或类似进程管理工具把 frps 设为服务以便开机自启与日志管理。 确保云服务器安全组、防火墙已放行 bindPort（本例为 7000）以及你需要的 HTTP/TCP 端口（例如 8080、其他映射端口）。 （可选）在服务端支持 HTTPS（使用nginx或者caddy反向代理）\n推荐做法：在服务器上使用 nginx 或 Caddy 负责 HTTPS（推荐），nginx/Caddy 监听 443，反向代理到 frps 的 vhostHTTPPort（例如 8080）。\nCaddy配置：\nexample.com { reverse_proxy 127.0.0.1:8080 } Nginx配置：\nserver { listen 80; server_name example.com; # 可直接重定向到 https return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:8080; # 转发到 frps 的 vhostHTTPPort } } Caddy 会自动为 example.com 获取并续期 Let’s Encrypt 证书（默认），配置极简，推荐用于快速部署。如果已经有Nginx环境，使用Nginx也是一个不错选择，它性能极高。\n使用HTTPS的工作原理：外部客户端访问 https://example.com -\u0026gt; nginx/Caddy 在服务器上完成 TLS -\u0026gt; 将解密后的 HTTP 请求以带 Host 头的形式代理到 frps 的 vhostHTTPPort -\u0026gt; frps 根据 Host（Host=example.com）将请求转发到对应 frpc（在内网的客户端）。\n它的好处是域名证书管理集中（nginx/Caddy 或 Caddy 自动化）、支持 HTTP/2、性能高、能在边缘添加 WAF、缓存、访问控制等。\n2. 客户端配置（Mole / frpc） 下载与安装 Mole\n前往 Mole 的 Release 页面下载适合你系统的安装包并安装或解压。\n基础连接设置（在 Mole 配置界面中）：\n服务器地址：填入云服务器的公网 IP（或域名） 服务器端口：填入 7000（须与服务端 bindPort 一致） Token：填入服务端 auth.token 中的值 添加代理规则（在 Mole 中新增条目）\nHTTP/HTTPS（适用于网站） 类型：HTTP/HTTPS 本地端口（Local Port）：你内网 Web 服务运行的端口（例如 80、8080） 自定义域名（如果使用域名映射） TCP/UDP（适用于 SSH、RDP、任意 TCP 服务或 UDP 服务） 类型：TCP 或 UDP 本地端口：内网服务监听端口（例如 SSH 的 22） 远程端口：映射到 frps 的公网可访问端口（如果不使用随机端口，可指定） 保存配置：点击 Mole 配置界面上的 “保存” 按钮，注意需要重新连接才能加载最新配置。\n可选：查看并编辑 frpc 配置\nMole 会在用户目录下解压 frpc 二进制并生成配置文件，若需要可手动修改后测试。 🚀 第三步：开启穿透服务 一键启动：在 Mole 控制页面点击 “启动” 按钮。 状态验证：在 Mole 的日志区域观察输出，若显示 start proxy success 或类似成功信息，表示连接已建立。 静默运行：关闭主窗口后，程序会缩至系统托盘继续运行（避免误关闭导致穿透中断）。 ❓ 常见问题排查（FAQ） 问题现象 解决方案 连接服务器失败 1. 检查云服务器安全组/防火墙是否放行 7000（bindPort）及所需映射端口。2. 检查服务端与客户端的 token 是否完全一致（注意空格/换行）。 如何查看底层报错 在 Mole 的 “日志” 页面可查看 frpc 的原生输出；也可到用户目录找到 frpc 的日志或手动运行 frpc 查看控制台输出。 如何手动验证 Mole 会在用户目录下生成 frpc 二进制和配置文件，你可以进入该目录手动运行 ./frpc -c frpc.toml（或对应配置文件）进行调试。 是否支持自启动 Mole 目前不支持系统开机自启，但支持软件启动后自动启动配置好的 frpc 服务（请在 Mole 设置中查看相关选项）。 🔗 相关资源 项目源码（示例）：littletow/mole-go FRP Releases（下载 frps/frpc）：fatedier/frp Mole Releases（下载桌面客户端）：littletow/mole-go 云服务器：阿里云 - 专享特惠 云服务器：腾讯云 - 专享特惠 💡 小贴士 强烈建议设置复杂的 auth.token 并妥善保管，避免被未授权的 frpc 连接。 在生产环境中，将 frps 配置为 systemd 服务以便可靠运行并自动重启。 使用基于域名的 HTTP 映射时，请确保 DNS 指向你的 frps 公网 IP，或在 vhost 配置上使用正确的域名。 如果你觉得 Mole 客户端对你有帮助，欢迎前往 GitHub 为项目点个 Star ⭐，或在 Issues 中提交建议和问题。 ","permalink":"https://blog.91demo.top/devops/mole-help.html","summary":"\u003cp\u003e本文将带你从零开始，快速完成基于 Mole 的内网穿透服务部署，包含服务端 (frps) 与桌面客户端 (Mole/frpc) 的配置与排查要点。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-核心概念\"\u003e📖 核心概念\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e什么是 FRP？\u003cbr\u003e\nFRP (Fast Reverse Proxy) 是一款高性能的反向代理/内网穿透工具，它通过在公网服务器和内网客户端之间建立隧道，使外网可以访问内网服务（如 NAS、树莓派、开发环境等）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e什么是 Mole？\u003cbr\u003e\nMole 是一款基于 wails3 开发的 FRP 桌面客户端，提供图形化管理界面，简化 frpc 的配置与使用：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e告别繁琐命令行\u003c/li\u003e\n\u003cli\u003e支持系统托盘常驻，防止误关窗口导致中断\u003c/li\u003e\n\u003cli\u003e支持 HTTP/HTTPS、TCP、UDP 等多种协议\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"-部署前准备\"\u003e🛠️ 部署前准备\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e一台拥有公网 IP 的低配云服务器（例如1核1G内存的\u003ca href=\"https://www.aliyun.com/minisite/goods?userCode=zrqh6alb\"\u003e阿里云\u003c/a\u003e、\u003ca href=\"https://curl.qcloud.com/XX6que5w\"\u003e腾讯云\u003c/a\u003e等）\u003c/li\u003e\n\u003cli\u003eFRP 服务端（frps）与客户端（frpc）二进制文件，建议使用 FRP Releases 的 v0.65.0 或更高版本\u003c/li\u003e\n\u003cli\u003eMole 桌面客户端安装包（对应你操作系统的版本）\u003c/li\u003e\n\u003cli\u003e基本网络与防火墙管理权限\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-服务端配置frps\"\u003e1. 服务端配置（frps）\u003c/h2\u003e\n\u003cp\u003e在拥有公网IP的云服务器上部署frps服务端：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e下载并解压 FRP 服务端（以 Linux 为例）\u003cbr\u003e\n访问 FRP 的 Releases 页面下载对应版本并解压（示例为 v0.65.0 或更高）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e编写 frps.toml 配置文件（示例）\u003cbr\u003e\n将以下内容写入 \u003ccode\u003efrps.toml\u003c/code\u003e，并根据实际需求调整端口与 token：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ebindPort = 7000           # 客户端连接端口（frpc 连接到此端口）\nvhostHTTPPort = 8080      # HTTP 映射的公网访问端口（可选）\n# 建议设置 auth.token 为复杂字符串，防止未授权连接\nauth.token = \u0026#34;你的复杂密匙\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e启动 frps（示例）\u003cbr\u003e\n在服务端目录运行：\u003c/p\u003e","title":"FRP 图形化管理新方案：基于 Wails 3 的 Mole-go 桌面客户端部署指南"},{"content":"如果你要开始项目开发，看看如下 Git 常用命令对你绝对有帮助。\n在每次开发新功能时，拉出一个新分支，分支名称为开发的功能名称，格式为 func-xxxx。当开发的功能完成并测试没有问题后，合并到主分支。\n当在开发新功能时，如果出现 bug，能够马上解决的，直接在主分支上修改，如果需要时间长，则新创建一个分支，格式为 bug-xxxx。当开发的功能完成并测试没有问题后，合并到主分支。\n1，开发分支。\n切换到 dev 分支，进行代码开发。开发完毕后，合并到主分支。\ngit checkout dev 2，合并到主分支。\ngit checkout master git merge dev git push origin master 3，主分支添加 tag，并发布到线上。\ngit checkout master git tag -a v1.0.0 -m \u0026#34;V1.0版本\u0026#34; git push origin v1.0.0 git push --tags 4，删除分支\ngit branch -d 分支名称 5，设置用户名和邮箱\ngit config --global user.name 你的用户名 git config --global user.email 你的邮箱 打标签 # 列出标签 git tag # 列出特定版本标签 git tag -l \u0026#34;v1.8.5*\u0026#34; # 创建标签 git tag -a v1.4 -m \u0026#34;my version 1.4\u0026#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 ","permalink":"https://blog.91demo.top/wiki/git.html","summary":"\u003cp\u003e如果你要开始项目开发，看看如下 Git 常用命令对你绝对有帮助。\u003c/p\u003e\n\u003cp\u003e在每次开发新功能时，拉出一个新分支，分支名称为开发的功能名称，格式为 func-xxxx。当开发的功能完成并测试没有问题后，合并到主分支。\u003c/p\u003e\n\u003cp\u003e当在开发新功能时，如果出现 bug，能够马上解决的，直接在主分支上修改，如果需要时间长，则新创建一个分支，格式为 bug-xxxx。当开发的功能完成并测试没有问题后，合并到主分支。\u003c/p\u003e\n\u003cp\u003e1，开发分支。\u003cbr\u003e\n切换到 dev 分支，进行代码开发。开发完毕后，合并到主分支。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit checkout dev\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e2，合并到主分支。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit checkout master\ngit merge dev\ngit push origin master\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e3，主分支添加 tag，并发布到线上。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit checkout master\ngit tag -a v1.0.0 -m \u0026#34;V1.0版本\u0026#34;\ngit push origin v1.0.0\ngit push --tags\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e4，删除分支\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit branch -d 分支名称\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e5，设置用户名和邮箱\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit config --global user.name 你的用户名\ngit config --global user.email 你的邮箱\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"打标签\"\u003e打标签\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# 列出标签\ngit tag\n\n# 列出特定版本标签\ngit tag -l \u0026#34;v1.8.5*\u0026#34;\n\n# 创建标签\ngit tag -a v1.4 -m \u0026#34;my version 1.4\u0026#34;\n\n# 显示标签\ngit show v1.4\n\n# 根据提交打标签\ngit tag -a v1.2 9fceb02\n\n# 推送标签\ngit push origin v1.5\n\n# 删除标签\ngit tag -d v1.4\n\n# 检出标签，浏览使用\ngit checkout v1.4\n\u003c/code\u003e\u003c/pre\u003e","title":"git开发常用命令"},{"content":"我们使用 Markdown 作为我们内容的格式，所以学习 Markdown 就显得很必要了。\nMarkdown 是一种轻量级标记语言，后缀为.md。因我常用 Markdown 来写一些内容，记录下来常用语法格式。方便自己查找使用，也增加自己的一些流量。Markdown 可以用来撰写电子书，文章，博客等。介绍标题、段落的使用。\n标题 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题 段落 Markd 段落没有特殊的格式，直接编写文件就好，段落的换行使用两个以上空格加上回车。也可以使用一个空行来表示重新开始一个段落。\n字体 使用如下格式分别表示斜体，粗体，粗斜体。\n*斜体文本* _斜体文本_ **粗体文本** __粗体文本__ ***粗斜体文本*** ___粗斜体文本___ 分割线 你可以在一行中用三个以上的星号、减号、底线来建立一个分割线，行内不能有其它东西。你也可以在星号或减号中间插入空格。\n*** * * * ***** - - - --------- 删除线 如果段落上的文字要添加删除线，只需要在文字的两端加上两个波浪线~~即可。\n~~删除线~~ 下划线 下划线可以通过 HTML 的标签来实现：\n\u0026lt;u\u0026gt;下划线文本\u0026lt;/u\u0026gt; 脚注 脚注是对文本的补充说明。\n[^要注明的文本] 列表 支持有序列表和无序列表。\n无序列表使用星号、加号、减号作为列表标记，这些标记后面要添加一个空格，然后再填写内容。\n* 第一项 * 第二项 * 第三项 + 第一项 + 第二项 + 第三项 - 第一项 - 第二项 - 第三项 有序列表使用数字并加上.号来表示。\n1. 第一项 2. 第二项 3. 第三项 列表嵌套需要在子列表中的选项签名添加两个或四个空格即可。\n1. 第一项： - 第一项嵌套的第一个元素 - 第一项嵌套的第二个元素 2. 第二项： - 第二项嵌套的第一个元素 - 第二项嵌套的第二个元素 区块 区块引用是在段落开头使用\u0026gt;符号，然后后面紧跟一个空格。\n\u0026gt; 区块引用 \u0026gt; 区块引用1 \u0026gt; 区块引用2 区块可以嵌套，一个\u0026gt;符号是最外层，两个\u0026gt;符号是第一层嵌套。\n\u0026gt; 最外层 \u0026gt; \u0026gt; 第一层嵌套 \u0026gt; \u0026gt; \u0026gt; 第二层嵌套 区块中也可以使用列表。\n\u0026gt; 区块中使用列表 \u0026gt; 1. 第一项 \u0026gt; 2. 第二项 \u0026gt; + 第一项 \u0026gt; + 第二项 \u0026gt; + 第三项 列表中使用区块，如果列表中要使用区块，请在\u0026gt;前添加四个空格即可。\n* 第一项 \u0026gt; 菜鸟教程 \u0026gt; 学的不仅是技术更是梦想 * 第二项 代码 段落上的一个函数或片段可以使用反引号包起来。\n`log()` 函数 代码区块使用四个空格或一个制表符（Tab 键），也可以使用三个反引号包裹一段代码，也可以指定一种语言。\n这里是代码片段区域 链接 链接可以用来跳转到网站。\n[链接名称](链接地址) \u0026lt;链接地址\u0026gt; 高级链接可以通过变量来设置，变量赋值内容在文档末尾设置。\n这个链接用 1 作为网址变量 [Blog][1] 这个链接用 x 作为网址变量 [X][x] [1]:https://www.91demo.top [x]:https://www.google.com 图片 可以显示图片。\n![alt 属性文本](图片地址) ![alt 属性文本](图片地址 \u0026#34;可选标题\u0026#34;) 图片居中\n![alt 属性文本](图片地址#pic_center) 图片居左\n![alt 属性文本](图片地址#pic_left) 图片居右\n![alt 属性文本](图片地址#pic_right) 设置宽和高\n![alt 属性文本](图片地址 =400x200) 只设置宽，会按比例进行缩放\n![alt 属性文本](图片地址 =400x) 如果你需要指定图片的高度和宽度，就需要使用 html 标签。\n\u0026lt;img src=\u0026#34;https://xxx.com/x.png\u0026#34; width=\u0026#34;50%\u0026#34;\u0026gt; 表格 制作表格使用|来分割不同的单元格，可以使用-来分割表头和其它行。\n| 表头 | 表头 | | ---- | ---- | | 单元格 | 单元格 | | 单元格 | 单元格 | 设置表格的对齐方式使用如下符号：\n-: 设置内容和标题栏居右对齐。 :- 设置内容和标题栏居左对齐。 :-: 设置内容和标题栏居中对齐。 示例： | 左对齐 | 右对齐 | 居中对齐 | | :-----| ----: | :----: | | 单元格 | 单元格 | 单元格 | | 单元格 | 单元格 | 单元格 | 支持 HTML 元素 不在 Markdown 涵盖范围之内的标签，都可以直接在文档里面用 HTML 撰写。目前支持的 HTML 元素有：\u0026lt;kbd\u0026gt; \u0026lt;b\u0026gt; \u0026lt;i\u0026gt; \u0026lt;em\u0026gt; \u0026lt;sup\u0026gt; \u0026lt;sub\u0026gt; \u0026lt;br\u0026gt;等。具体请参考官方文档。\n转义 Markdown 使用了很多特殊符号来表示特定的意义，如果需要显示特定的符号则需要使用转义字符，Markdown 使用反斜杠转义特殊字符：\n**文本加粗** \\*\\* 正常显示星号 \\*\\* 数学公式 使用 KaTex 或 MathJax 来渲染数学表达式。这部分内容需要参考对应官方文档。\n","permalink":"https://blog.91demo.top/wiki/markdown.html","summary":"\u003cp\u003e我们使用 Markdown 作为我们内容的格式，所以学习 Markdown 就显得很必要了。\u003c/p\u003e\n\u003cp\u003eMarkdown 是一种轻量级标记语言，后缀为.md。因我常用 Markdown 来写一些内容，记录下来常用语法格式。方便自己查找使用，也增加自己的一些流量。Markdown 可以用来撰写电子书，文章，博客等。介绍标题、段落的使用。\u003c/p\u003e\n\u003ch2 id=\"标题\"\u003e标题\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# 一级标题\n## 二级标题\n### 三级标题\n#### 四级标题\n##### 五级标题\n###### 六级标题\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"段落\"\u003e段落\u003c/h2\u003e\n\u003cp\u003eMarkd 段落没有特殊的格式，直接编写文件就好，段落的换行使用两个以上空格加上回车。也可以使用一个空行来表示重新开始一个段落。\u003c/p\u003e\n\u003ch3 id=\"字体\"\u003e字体\u003c/h3\u003e\n\u003cp\u003e使用如下格式分别表示斜体，粗体，粗斜体。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e*斜体文本*\n_斜体文本_\n**粗体文本**\n__粗体文本__\n***粗斜体文本***\n___粗斜体文本___\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"分割线\"\u003e分割线\u003c/h3\u003e\n\u003cp\u003e你可以在一行中用三个以上的星号、减号、底线来建立一个分割线，行内不能有其它东西。你也可以在星号或减号中间插入空格。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e***\n* * *\n*****\n- - -\n---------\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"删除线\"\u003e删除线\u003c/h3\u003e\n\u003cp\u003e如果段落上的文字要添加删除线，只需要在文字的两端加上两个波浪线~~即可。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e~~删除线~~\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"下划线\"\u003e下划线\u003c/h3\u003e\n\u003cp\u003e下划线可以通过 HTML 的\u003cu\u003e标签来实现：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026lt;u\u0026gt;下划线文本\u0026lt;/u\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"脚注\"\u003e脚注\u003c/h3\u003e\n\u003cp\u003e脚注是对文本的补充说明。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[^要注明的文本]\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"列表\"\u003e列表\u003c/h2\u003e\n\u003cp\u003e支持有序列表和无序列表。\u003c/p\u003e\n\u003cp\u003e无序列表使用星号、加号、减号作为列表标记，这些标记后面要添加一个空格，然后再填写内容。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e* 第一项\n* 第二项\n* 第三项\n\n+ 第一项\n+ 第二项\n+ 第三项\n\n- 第一项\n- 第二项\n- 第三项\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e有序列表使用数字并加上.号来表示。\u003c/p\u003e","title":"Markdown 语法教程"},{"content":"豆子碎片小程序很早就注册了，刚开始做的功能就是浏览 Go Web 框架 Beego 的教程。当时，正在自学 Beego，而 Beego 的网站时常打不开，就想做一个小程序，可以浏览 Beego 内容。当时看到了 towxml 组件，正好可以实现 Markdown 转 Wxml，于是就将 Beego 的 内容文档写成 Markdown 格式，做成了小程序。小程序的项目名叫 visit，就是浏览的意思。\n当时小程序还是使用的云环境，云环境免费，并提供 dev 和 pro 两个环境。将 Beego 的 Markdown 文件存在云存储中。使用云函数调用数据。在小程序端使用 towxml 解析 Markdown 数据。当时发布小程序后，还是很受欢迎的。\n中间歇了一段时间，没有更新文章，也没有维护小程序，小程序日浏览人数就下降了。云环境收费后，就彻底不管了。\n后来，买了一台云服务器，为了练习自己所学的 Golang 语言，就将小程序重新拾起来。新做的项目内容是浏览唐诗，使用 Gin 框架进行后端开发，小程序端的布局和现在的豆子碎片布局差不多，只有一个首页，首页包含标题、一个搜索框、几个快捷按钮。项目很快完成，将小学和初中的唐诗、宋词、古诗经都录入了数据库，维护了很长一段时间。小程序上线后，每天也有几个人访问。在录入高中的唐诗内容时，就无法坚持下去了，高中的唐诗等内容都比较长，页面排版非常不美，所以高中的内容就没有录入。小程序在展示唐诗内容时，刚开始使用 Markdown 展示。后来想添加拼音，就将 Markdown 去掉，手撸 wxml，最后，拼音也添加了，但是费了很大劲，自己不擅长前端，美工。本还计划添加录音，但实在没有素材，真正做起来，费时费力，没有精力和动力就搁置了。\n2024 年微信平台要求小程序都要备案。我有三个小程序，本想都放弃了，但后来，在工作中要用到音频格式转换，在小程序端实现操作起来是真的很方便，省时省力，不需要开电脑，也可以在任何时间操作。所以计划留下一两个吧。在备案的时候，注销了一个，剩下了两个，目前还在使用和维护，一个是豆子工具，用来制作工具，平时我也在用，一个是豆子碎片，记录技术经验和代码片段。在备案豆子碎片小程序时，小程序原名称叫找唐诗，提交备案后，微信备案客服人员说唐诗这两字不让用，让换个名称。在换名称的时候，我就在想这个小程序要不要放弃？不放弃做什么内容？已经有一个豆子工具小程序做工具了，没有必要再做一个工具类小程序吧。一直没有想好，等微信备案人员二次打电话时（真的很负责），才确定下来，做笔记类应用，记录日常开发经验和技巧以及代码片段，本想叫代码碎片，为了和豆子工具保持一致，小程序名称就叫豆子碎片，寓意像玩积木一样，学了本小程序内容，你也可以制作一款你想要的小程序。\n豆子碎片项目已经开源，项目地址：https://github.com/littletow/visit\n豆子碎片目前没有实名认证，如果你喜欢就收藏一下。豆子碎片面对的人群是有技术门槛的，后续看情况再进行实名认证。\n豆子碎片的文章也已经在我公众号发布。有兴趣可以查看一下，一个程序员之小程序的成长与蜕变\n想要查看豆子碎片，可以扫下面的小程序码：\n","permalink":"https://blog.91demo.top/wiki/visit-story.html","summary":"\u003cp\u003e豆子碎片小程序很早就注册了，刚开始做的功能就是浏览 Go Web 框架 Beego 的教程。当时，正在自学 Beego，而 Beego 的网站时常打不开，就想做一个小程序，可以浏览 Beego 内容。当时看到了 towxml 组件，正好可以实现 Markdown 转 Wxml，于是就将 Beego 的 内容文档写成 Markdown 格式，做成了小程序。小程序的项目名叫 visit，就是浏览的意思。\u003c/p\u003e\n\u003cp\u003e当时小程序还是使用的云环境，云环境免费，并提供 dev 和 pro 两个环境。将 Beego 的 Markdown 文件存在云存储中。使用云函数调用数据。在小程序端使用 towxml 解析 Markdown 数据。当时发布小程序后，还是很受欢迎的。\u003c/p\u003e\n\u003cp\u003e中间歇了一段时间，没有更新文章，也没有维护小程序，小程序日浏览人数就下降了。云环境收费后，就彻底不管了。\u003c/p\u003e\n\u003cp\u003e后来，买了一台云服务器，为了练习自己所学的 Golang 语言，就将小程序重新拾起来。新做的项目内容是浏览唐诗，使用 Gin 框架进行后端开发，小程序端的布局和现在的豆子碎片布局差不多，只有一个首页，首页包含标题、一个搜索框、几个快捷按钮。项目很快完成，将小学和初中的唐诗、宋词、古诗经都录入了数据库，维护了很长一段时间。小程序上线后，每天也有几个人访问。在录入高中的唐诗内容时，就无法坚持下去了，高中的唐诗等内容都比较长，页面排版非常不美，所以高中的内容就没有录入。小程序在展示唐诗内容时，刚开始使用 Markdown 展示。后来想添加拼音，就将 Markdown 去掉，手撸 wxml，最后，拼音也添加了，但是费了很大劲，自己不擅长前端，美工。本还计划添加录音，但实在没有素材，真正做起来，费时费力，没有精力和动力就搁置了。\u003c/p\u003e\n\u003cp\u003e2024 年微信平台要求小程序都要备案。我有三个小程序，本想都放弃了，但后来，在工作中要用到音频格式转换，在小程序端实现操作起来是真的很方便，省时省力，不需要开电脑，也可以在任何时间操作。所以计划留下一两个吧。在备案的时候，注销了一个，剩下了两个，目前还在使用和维护，一个是豆子工具，用来制作工具，平时我也在用，一个是豆子碎片，记录技术经验和代码片段。在备案豆子碎片小程序时，小程序原名称叫找唐诗，提交备案后，微信备案客服人员说唐诗这两字不让用，让换个名称。在换名称的时候，我就在想这个小程序要不要放弃？不放弃做什么内容？已经有一个豆子工具小程序做工具了，没有必要再做一个工具类小程序吧。一直没有想好，等微信备案人员二次打电话时（真的很负责），才确定下来，做笔记类应用，记录日常开发经验和技巧以及代码片段，本想叫代码碎片，为了和豆子工具保持一致，小程序名称就叫豆子碎片，寓意像玩积木一样，学了本小程序内容，你也可以制作一款你想要的小程序。\u003c/p\u003e\n\u003cp\u003e豆子碎片项目已经开源，\u003ca href=\"https://github.com/littletow/visit\"\u003e项目地址\u003c/a\u003e：https://github.com/littletow/visit\u003c/p\u003e\n\u003cp\u003e豆子碎片目前没有实名认证，如果你喜欢就收藏一下。豆子碎片面对的人群是有技术门槛的，后续看情况再进行实名认证。\u003c/p\u003e\n\u003cp\u003e豆子碎片的文章也已经在我公众号发布。有兴趣可以查看一下，\u003ca href=\"https://mp.weixin.qq.com/s/KoWnv1EejAeu210UkRej1g\"\u003e一个程序员之小程序的成长与蜕变\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e想要查看豆子碎片，可以扫下面的小程序码：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"豆子碎片\" loading=\"lazy\" src=\"https://ant.91demo.top/imgs/visit.webp#pic_center\"\u003e\u003c/p\u003e","title":"豆子碎片的故事"},{"content":"前面介绍了如何学习豆子碎片小程序，今天本文将介绍包括核心功能实现、技术方案对比和业务逻辑落地等方面。豆子碎片小程序是一款小程序技术和经验等内容展示类的小程序，主要用于展示 Markdown 文件解析后的内容。\n核心功能实现 豆子碎片小程序的核心功能主要包括以下几个方面：\n数据下载与本地缓存：每次小程序启动时，从服务器下载最新的 Markdown 文件，并缓存到本地，以便后续使用。 Markdown 解析与渲染：使用 towxml 组件解析下载的 Markdown 文件，并渲染成 wxml 文件，以便在小程序中展示。 首页布局与搜索：首页采用上中下布局，上方介绍小程序的用途，中间是搜索框，下方是快捷按钮。当用户在搜索框输入内容或点击快捷按钮时，查询出相关的文章列表。 文章详情展示：点击文章列表中的文章，跳转到文章详情页，展示文章的详细内容。 技术方案对比 在实现豆子碎片小程序的过程中，我们对多种技术方案进行了对比，最终选择了以下技术方案：\n数据下载与本地缓存 方案一：每次启动时都从服务器下载数据：这种方案实现简单，但会增加网络请求次数，影响小程序的启动速度和用户体验。 方案二：使用本地缓存：在小程序启动时，先检查本地缓存是否有最新数据，如果有则直接使用，否则从服务器下载数据并缓存到本地。最终我们选择了这种方案，以提高小程序的启动速度和用户体验。 Markdown 解析与渲染 方案一：自定义解析器：自行开发 Markdown 解析器，解析 Markdown 文件并生成 wxml 文件。这种方案实现复杂，维护成本高。 方案二：使用现有组件：我们对比了 wxParse 组件和 towxml 组件，最终选择使用开源的 towxml 组件进行 Markdown 解析和渲染，它组件成熟，易于集成和使用。最终我们选择了这种方案，以简化开发流程并提高开发效率。 首页布局与搜索 方案一：手动实现布局和搜索功能：自行编写代码实现首页布局和搜索功能，灵活性高，但开发成本较高。 方案二：使用微信小程序提供的组件：使用微信小程序提供的 UI 组件，如\u0026lt;view\u0026gt;、\u0026lt;input\u0026gt;、\u0026lt;button\u0026gt;等，实现首页布局和搜索功能，并使用 weui 样式。最终我们选择了这种方案，以简化开发工作并提高开发效率。 业务逻辑落地 下方只展示了代码片段，详情请查看 visit 项目。\n数据下载与本地缓存 在小程序的 app.js 文件中，我们实现了数据下载和本地缓存的逻辑：\n// 登入 async login() { const that = this; const tzms = utils.getTodayZeroMsTime(); // 获取今日零时毫秒时间戳 // 检查设备信息? that.logDevInfo(); // 检查Git服务是否正常？ const isAlive = await that.chkServerAlive(); console.log(\u0026#39;url,\u0026#39;, that.globalData.url, isAlive); // 检查版本更新? that.uptVer(tzms); // 加载用户信息 that.onLogin(); }, // 小程序每次启动都会调用 onLaunch: function () { const startTime = Date.now(); this.globalData.startTime = startTime; this.login(); const loginTime = Date.now(); const launchTime = loginTime - startTime; console.log(`app onLaunch: ${launchTime} ms`); }, 首页布局与搜索 在首页的 index.wxml 文件中，我们实现了上中下布局和搜索框：\n\u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;page__hd\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;page__title\u0026#34; bind:tap=\u0026#34;bindShowContact\u0026#34;\u0026gt; Coder \u0026lt;text class=\u0026#34;clickable\u0026#34;\u0026gt;加油\u0026lt;/text\u0026gt; \u0026lt;image class=\u0026#34;contact-image\u0026#34; src=\u0026#34;/images/email.png\u0026#34;\u0026gt;\u0026lt;/image\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;page__desc\u0026#34;\u0026gt;小程序开发全流程指南，碎片化学习也能掌握组件、调试、上线全链路技能。它是学习小程序开发的好工具，值得收藏！\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;page__bd page__bd_spacing\u0026#34;\u0026gt; \u0026lt;!-- 搜索框--\u0026gt; \u0026lt;view class=\u0026#34;weui-search-bar {{inputShowed ? \u0026#39;weui-search-bar_focusing\u0026#39; : \u0026#39;\u0026#39;}}\u0026#34; id=\u0026#34;searchBar\u0026#34;\u0026gt; \u0026lt;form class=\u0026#34;weui-search-bar__form\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-search-bar__box\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;weui-icon-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; confirm-type=\u0026#34;search\u0026#34; class=\u0026#34;weui-search-bar__input\u0026#34; placeholder=\u0026#34;请输入您要查找的内容\u0026#34; value=\u0026#34;{{inputVal}}\u0026#34; focus=\u0026#34;{{inputShowed}}\u0026#34; bindinput=\u0026#34;inputTyping\u0026#34; bindconfirm=\u0026#34;bindSearch\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;label class=\u0026#34;weui-search-bar__label\u0026#34; bindtap=\u0026#34;showInput\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;weui-icon-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;span class=\u0026#34;weui-search-bar__text\u0026#34;\u0026gt;搜索\u0026lt;/span\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;view class=\u0026#34;weui-search-bar__cancel-btn\u0026#34; bindtap=\u0026#34;hideInput\u0026#34;\u0026gt;取消\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 推荐搜索内容 --\u0026gt; \u0026lt;view aria-role=\u0026#34;listbox\u0026#34; id=\u0026#34;searchResult\u0026#34; class=\u0026#34;weui-cells searchbar-result\u0026#34; wx:if=\u0026#34;{{kwList.length \u0026gt; 0}}\u0026#34;\u0026gt; \u0026lt;block wx:for=\u0026#34;{{kwList}}\u0026#34; wx:key=\u0026#34;*this\u0026#34;\u0026gt; \u0026lt;view role=\u0026#34;option\u0026#34; class=\u0026#34;weui-cell weui-cell_active weui-cell_access\u0026#34; bindtap=\u0026#34;selectKeyword\u0026#34; data-kw=\u0026#34;{{item.kw}}\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__bd weui-cell_primary\u0026#34;\u0026gt; \u0026lt;view\u0026gt;{{item.kw}}\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/block\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 查询快捷按钮，用于快速查找文章--\u0026gt; \u0026lt;!-- 第一排快捷按钮--\u0026gt; \u0026lt;view class=\u0026#34;weui-flex\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;DevEnv\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;开发环境\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;CompDev\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;组件开发\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;APIInte\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;接口集成\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 第二排快捷按钮--\u0026gt; \u0026lt;view class=\u0026#34;weui-flex\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;ProjCase\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;项目实战\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;DebugTest\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;调试测试\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;AuditDeploy\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;审核上线\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 第三排快捷按钮--\u0026gt; \u0026lt;view class=\u0026#34;weui-flex\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;OperPromo\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;运营推广\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;MaintOpti\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;维护优化\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;MyProject\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;我的项目\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;block wx:if=\u0026#34;{{isShowContact}}\u0026#34;\u0026gt; \u0026lt;!--联系卡片--\u0026gt; \u0026lt;view class=\u0026#34;view-contact\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;left-content\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;icon-button\u0026#34; open-type=\u0026#34;feedback\u0026#34;\u0026gt; \u0026lt;image src=\u0026#34;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAABECAYAAAA4E5OyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAOESURBVHhe7ZrbSxZBFMD7D8uuUET1YFQPET300t20C2ZlCaIRqElWWGZERZYE2l1Fw8hSs4jKCgyp6KU4zWHO4VsP+nk+nZ2drfnBoHvmzNnZn3vD2SUQmUEUIohCBFGIIAoRRCGCKEQQhQj+HSFfp+mXxVFcyM5GgKUHs2/3BmlCs9A1ALC93uadvQXw7gt1LIziQuTEsmr9ozQhQdv9Qs6OBvtzWx3A2EdKKJ35hexuoo3AaDRnA8vofWFjLKi8FuDlexsrkXwKSZ4ZHQ8pSHDfxhqA5xMU1JM/IUkZ2LacARj9QJ0E56w7BjAwRkEd+RKSlIGXCW+XnwIYEZcI9606DPBkhILzkx8hUgbD8U0nAIbfUpDgvmUVAM9eU7A4+RAylwyG+9dXAwy9oSDBfTVXKFCc8IXMJ4PhvLXmvtEv7hs8XkHYQrQyGM5fbe4bT1/NjOFjWkG4QkqVwfC4FZUA1e2FGnO93AnCFLJQGUxyfIk1whOyWBlIssb1xxTUEZYQ1zIWUCMcIQHIQMIQEogMJHshAclAshXiSwbGlfWzE+LzzOAcBdkI8SkD4TwF/oX4loFwrgK/QrKQgXC+An9CspKB8BgFfoRkKQPhcQrSF5K1DITHKkhXSAgyEB6vID0hochAuIaCdISEJAPhOgrcCznZsfgDcSkD4VoK3Ar58cuumPEEWu5SRwm4loFwPQVuhQyO2zFJKXiAWtKQgXBNBW6FtPfYMU1dAHtaChNp76WEIqQlA+G6CtwKOXLJjuH11H3n7TaunBX732aaMhCurcCdkKnvdh11jWm//1DQcKDV1ikzUm73UTBB2jIQrq/AnRBcGML8yjYKJKi4YPvKDgF0D1HQ4EMGwvtQ4E5Is7lvYH7nIwoIqi7a/uVGCh68LxkI70eBOyH8ndfEJAVm4ehlm4OrajzJtGUgvC8FboRMfrO5W+soIEBJVx8A7E08ebD5kIHw/hS4EXKn3+bW36CAAb/VaDYvZnzmcNt82n7d2NpNiR7gfStwI4QvBfyL13YCbDhemMTKKoD95kmD94y+UYDpnzTIIzwXBW6E4IsX7xQbnhUNNwF6hs3lNEVJGcLzUuBGyGdzD6m9Zl++xj9RMCC8CwmdKEQQhQiiEEEUIohCBFGIIAoROBeS9+ZMyK5zhWJ5byhGQXEh/yFRiCAKEUQhgihEEIUIopAZAPwF2AJS9v7V7qoAAAAASUVORK5CYII=\u0026#34; class=\u0026#34;icon\u0026#34;\u0026gt;\u0026lt;/image\u0026gt; \u0026lt;/button\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;right-content\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;contact-button\u0026#34; bindtap=\u0026#34;showContactDialog\u0026#34;\u0026gt;联系我\u0026lt;/button\u0026gt; \u0026lt;text class=\u0026#34;support-text\u0026#34;\u0026gt;点击左侧按钮反馈，或联系我获取邮箱发邮件\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/block\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!--\u0026lt;view class=\u0026#34;page__ft\u0026#34;\u0026gt;\u0026lt;/view\u0026gt;--\u0026gt; \u0026lt;/view\u0026gt; 在页面的 index.js 文件中，我们实现了搜索功能：\nsearch(keyword) { if (utils.isEmpty(keyword)) { wx.showToast({ title: \u0026#39;内容不能为空\u0026#39;, }) return } const kw = keyword.toLowerCase(); this.setData({ kwList: [], keyword: kw, op: 1, }) this.searchArt(1, kw) }, 文章详情展示 Markdown 解析与渲染 在页面的 article.js 文件中，我们使用 towxml 组件解析 Markdown 数据并渲染：\n// 加载文章资源，现在从Git获取，下载Markdown文件，然后解析文件。 getArt(category, artId, label) { const that = this; // 修改此处可以切换Git地址 let fileUrl = app.globalData.url + category + \u0026#39;/\u0026#39; + artId; utils.downloadFile( fileUrl, 20000, // 超时时间20秒 (tmpfile) =\u0026gt; { // console.log(\u0026#39;Download successful, file saved at:\u0026#39;, tmpfile); // 可以在这里对下载的文件进行进一步处理 // 下载成功后，会存储为临时文件，需要使用微信API读取文件内容。 const fs = wx.getFileSystemManager() fs.readFile({ filePath: tmpfile, encoding: \u0026#39;utf8\u0026#39;, success(res) { // console.log(res.data) if (label == \u0026#39;md\u0026#39;) { let obj = app.towxml(res.data, \u0026#39;markdown\u0026#39;, { theme: \u0026#39;light\u0026#39;, events: { tap: (e) =\u0026gt; { console.log(\u0026#39;tap\u0026#39;, e); } } }); // 将文件内容赋值给towxml组件，它会自动进行解析渲染。然后将加载动画关闭。 that.setData({ article: obj, isLoading: false, }); } else if (label == \u0026#39;html\u0026#39;) { let obj = app.towxml(res.data, \u0026#39;html\u0026#39;, { theme: \u0026#39;light\u0026#39;, events: { tap: (e) =\u0026gt; { console.log(\u0026#39;tap\u0026#39;, e); } } }); // 将文件内容赋值给towxml组件，它会自动进行解析渲染。然后将加载动画关闭。 that.setData({ article: obj, isLoading: false, }); } }, }) }, (err) =\u0026gt; { console.error(\u0026#39;Download failed:\u0026#39;, err.message); log.error(\u0026#39;file url,\u0026#39;, fileUrl, \u0026#39;error message,\u0026#39;, err.message); const title = \u0026#39;下载Markdown文件时错误\u0026#39;; const content = \u0026#34;文件地址：\u0026#34; + fileUrl + \u0026#34;，错误信息：\u0026#34; + JSON.stringify(err.message); app.rptErrInfo(title, content); }, ); }, 在页面的 article.wxml 文件中，我们渲染解析后的内容：\n\u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;page__bd\u0026#34;\u0026gt; \u0026lt;!--loading 加载动画--\u0026gt; \u0026lt;view class=\u0026#34;loading\u0026#34; wx:if=\u0026#34;{{isLoading}}\u0026#34;\u0026gt; \u0026lt;image class=\u0026#34;loading__icon\u0026#34; src=\u0026#34;../../images/loading.svg\u0026#34;\u0026gt;\u0026lt;/image\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!--使用towxml--\u0026gt; \u0026lt;towxml nodes=\u0026#34;{{article}}\u0026#34; /\u0026gt; \u0026lt;view class=\u0026#34;weui-footer\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-footer__text\u0026#34;\u0026gt;--- 这是底线了 ---\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 豆子碎片内部逻辑 虽然项目已经完成，但是温故而知新。我们梳理一下。结合以前学到的基础知识。我们进行总结。\nAPP 启动操作流程如下：\nAPP 启动，加载设备信息，若没有则进行获取并缓存到系统，加载最新版本检查时间，若没有则下载版本文件，下载数据文件，然后缓存版本检查时间；若有，则和今天零时对比，若大于零时时间，则表示已经更新，从缓存中直接加载数据，若小于零时时间，则表示还未更新，从服务器下载版本文件，然后和本地的版本进行对比。如线上更新，则更新数据文件，并同步本地版本。然后将数据保存在文件系统中。如果没有更新，直接从文件系统中获取数据，然后存储到 app 的 artList 对象中。\n界面渲染完毕后，会显示首页，首页和用户交互的有搜索框和分类按钮。\n当使用搜索框搜索时，根据填写的内容作为关键字在 artList 列表中进行查询，查询步骤顺序，先查询标题，如果标题包含直接返回，然后查询关键词，如果包含直接返回，最后查询标签，如果包含直接返回。最后组成列表并进行分页，返回数据给页面。页面将跳转到列表页面，并渲染列表。列表中的每一项都可以点击，点击之后就进入到文章页。\n文章页的功能将文章 Markdown 数据下载到本地，然后使用 towxml 插件将数据渲染并显示。\n分类按钮是按照项目来分类，当点击按钮时，从文章索引列表中将相关的项目分类数据提取组成列表，分页之后返回给页面。\n总结 通过以上介绍，我们详细讲解了豆子碎片小程序的核心功能实现、技术方案对比和业务逻辑落地的过程。希望本文能为您在开发微信小程序时提供一些参考和帮助。如果在开发过程中遇到问题，可以查阅微信官方文档或在开发者社区寻求帮助。\n","permalink":"https://blog.91demo.top/wiki/visitdev.html","summary":"\u003cp\u003e前面介绍了如何学习豆子碎片小程序，今天本文将介绍包括核心功能实现、技术方案对比和业务逻辑落地等方面。豆子碎片小程序是一款小程序技术和经验等内容展示类的小程序，主要用于展示 Markdown 文件解析后的内容。\u003c/p\u003e\n\u003ch2 id=\"核心功能实现\"\u003e核心功能实现\u003c/h2\u003e\n\u003cp\u003e豆子碎片小程序的核心功能主要包括以下几个方面：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e数据下载与本地缓存\u003c/strong\u003e：每次小程序启动时，从服务器下载最新的 Markdown 文件，并缓存到本地，以便后续使用。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMarkdown 解析与渲染\u003c/strong\u003e：使用 towxml 组件解析下载的 Markdown 文件，并渲染成 wxml 文件，以便在小程序中展示。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e首页布局与搜索\u003c/strong\u003e：首页采用上中下布局，上方介绍小程序的用途，中间是搜索框，下方是快捷按钮。当用户在搜索框输入内容或点击快捷按钮时，查询出相关的文章列表。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e文章详情展示\u003c/strong\u003e：点击文章列表中的文章，跳转到文章详情页，展示文章的详细内容。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"技术方案对比\"\u003e技术方案对比\u003c/h2\u003e\n\u003cp\u003e在实现豆子碎片小程序的过程中，我们对多种技术方案进行了对比，最终选择了以下技术方案：\u003c/p\u003e\n\u003ch3 id=\"数据下载与本地缓存\"\u003e数据下载与本地缓存\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e方案一：每次启动时都从服务器下载数据\u003c/strong\u003e：这种方案实现简单，但会增加网络请求次数，影响小程序的启动速度和用户体验。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e方案二：使用本地缓存\u003c/strong\u003e：在小程序启动时，先检查本地缓存是否有最新数据，如果有则直接使用，否则从服务器下载数据并缓存到本地。最终我们选择了这种方案，以提高小程序的启动速度和用户体验。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"markdown-解析与渲染\"\u003eMarkdown 解析与渲染\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e方案一：自定义解析器\u003c/strong\u003e：自行开发 Markdown 解析器，解析 Markdown 文件并生成 wxml 文件。这种方案实现复杂，维护成本高。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e方案二：使用现有组件\u003c/strong\u003e：我们对比了 wxParse 组件和 towxml 组件，最终选择使用开源的 towxml 组件进行 Markdown 解析和渲染，它组件成熟，易于集成和使用。最终我们选择了这种方案，以简化开发流程并提高开发效率。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"首页布局与搜索\"\u003e首页布局与搜索\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e方案一：手动实现布局和搜索功能\u003c/strong\u003e：自行编写代码实现首页布局和搜索功能，灵活性高，但开发成本较高。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e方案二：使用微信小程序提供的组件\u003c/strong\u003e：使用微信小程序提供的 UI 组件，如\u003ccode\u003e\u0026lt;view\u0026gt;\u003c/code\u003e、\u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003e、\u003ccode\u003e\u0026lt;button\u0026gt;\u003c/code\u003e等，实现首页布局和搜索功能，并使用 weui 样式。最终我们选择了这种方案，以简化开发工作并提高开发效率。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"业务逻辑落地\"\u003e业务逻辑落地\u003c/h2\u003e\n\u003cp\u003e下方只展示了代码片段，详情请查看 visit 项目。\u003c/p\u003e\n\u003ch3 id=\"数据下载与本地缓存-1\"\u003e数据下载与本地缓存\u003c/h3\u003e\n\u003cp\u003e在小程序的 \u003ccode\u003eapp.js\u003c/code\u003e 文件中，我们实现了数据下载和本地缓存的逻辑：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 登入\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003elogin\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ethat\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003ethis\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etzms\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eutils\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003egetTodayZeroMsTime\u003c/span\u003e(); \u003cspan style=\"color:#75715e\"\u003e// 获取今日零时毫秒时间戳\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 检查设备信息?\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ethat\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elogDevInfo\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 检查Git服务是否正常？\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eisAlive\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eawait\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ethat\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003echkServerAlive\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003econsole\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elog\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;url,\u0026#39;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003ethat\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eglobalData\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eisAlive\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 检查版本更新?\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ethat\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003euptVer\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003etzms\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 加载用户信息\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ethat\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eonLogin\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e// 小程序每次启动都会调用\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003eonLaunch\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e () {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estartTime\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e Date.\u003cspan style=\"color:#a6e22e\"\u003enow\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ethis\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eglobalData\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003estartTime\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estartTime\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ethis\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elogin\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eloginTime\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e Date.\u003cspan style=\"color:#a6e22e\"\u003enow\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003elaunchTime\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eloginTime\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estartTime\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003econsole\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elog\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e`app onLaunch: \u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e${\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003elaunchTime\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e ms`\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"首页布局与搜索-1\"\u003e首页布局与搜索\u003c/h3\u003e\n\u003cp\u003e在首页的 \u003ccode\u003eindex.wxml\u003c/code\u003e 文件中，我们实现了上中下布局和搜索框：\u003c/p\u003e","title":"豆子碎片小程序开发实践：从核心功能到技术实现"},{"content":"在写笔记列表页面时，发现从服务器获取的笔记列表包含最新更新时间 lastts，当我们想将最新更新的条目展示在最上端时，我们需要排序数组，将数组按照时间重新排序，排序规则为最新的时间条目放在最前面。\n这样我们就可以在笔记列表中让用户看到最新更新的内容了。\n我们需要将其封装为函数，方便调用。\n函数代码如下：\n// 按照更新时间排序的函数 sortNotesByLastTs: function(notes) { return notes.slice().sort((a, b) =\u0026gt; { // 将时间字符串转为时间戳比较 const timeA = new Date(a.lastts).getTime(); const timeB = new Date(b.lastts).getTime(); // 降序排列（最新的在前） return timeB - timeA; }); }, 当运行之后，发现时间戳格式和服务器返回格式不一致，我们需要重新调整函数，服务器返回格式 YYYY-MM-DD HH:mm:ss，本地函数使用的时间格式 ISO 8601 格式。\n调整后的代码如下：\n// 按照更新时间排序的函数（适配\u0026#34;YYYY-MM-DD HH:mm:ss\u0026#34;格式） sortNotesByLastTs: function(notes) { return notes.slice().sort((a, b) =\u0026gt; { // 解析自定义时间格式 const parseTime = (timeStr) =\u0026gt; { if (!timeStr) return 0; // 替换空格为T，使其符合ISO格式 const isoFormat = timeStr.replace(\u0026#39; \u0026#39;, \u0026#39;T\u0026#39;); return new Date(isoFormat).getTime() || 0; }; const timeA = parseTime(a.lastts); const timeB = parseTime(b.lastts); // 降序排列（最新的在前） return timeB - timeA; }); }, 推荐还是调整服务器端，让返回的时间格式转为 ISO 8601 格式。\n","permalink":"https://blog.91demo.top/wiki/sortbytime.html","summary":"\u003cp\u003e在写笔记列表页面时，发现从服务器获取的笔记列表包含最新更新时间 lastts，当我们想将最新更新的条目展示在最上端时，我们需要排序数组，将数组按照时间重新排序，排序规则为最新的时间条目放在最前面。\u003c/p\u003e\n\u003cp\u003e这样我们就可以在笔记列表中让用户看到最新更新的内容了。\u003c/p\u003e\n\u003cp\u003e我们需要将其封装为函数，方便调用。\u003c/p\u003e\n\u003cp\u003e函数代码如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 按照更新时间排序的函数\n  sortNotesByLastTs: function(notes) {\n    return notes.slice().sort((a, b) =\u0026gt; {\n      // 将时间字符串转为时间戳比较\n      const timeA = new Date(a.lastts).getTime();\n      const timeB = new Date(b.lastts).getTime();\n\n      // 降序排列（最新的在前）\n      return timeB - timeA;\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e当运行之后，发现时间戳格式和服务器返回格式不一致，我们需要重新调整函数，服务器返回格式 YYYY-MM-DD HH:mm:ss，本地函数使用的时间格式 ISO 8601 格式。\u003c/p\u003e\n\u003cp\u003e调整后的代码如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 按照更新时间排序的函数（适配\u0026#34;YYYY-MM-DD HH:mm:ss\u0026#34;格式）\n  sortNotesByLastTs: function(notes) {\n    return notes.slice().sort((a, b) =\u0026gt; {\n      // 解析自定义时间格式\n      const parseTime = (timeStr) =\u0026gt; {\n        if (!timeStr) return 0;\n\n        // 替换空格为T，使其符合ISO格式\n        const isoFormat = timeStr.replace(\u0026#39; \u0026#39;, \u0026#39;T\u0026#39;);\n        return new Date(isoFormat).getTime() || 0;\n      };\n\n      const timeA = parseTime(a.lastts);\n      const timeB = parseTime(b.lastts);\n\n      // 降序排列（最新的在前）\n      return timeB - timeA;\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e推荐还是调整服务器端，让返回的时间格式转为 ISO 8601 格式。\u003c/p\u003e","title":"将数组按照时间重新降序排序"},{"content":"我们知道 Protobuf 文件可以减少体积，方便存储和传输，但在获取文件后，我们还需要将其转换为 JSON 格式，以适配应用进行处理数据。\n下面的代码是在微信小程序中使用，将 pb 文件转换为 JSON，然后再使用。\n// 小程序根目录执行安装（需开启 npm 支持） // npm install protobufjs // 创建 proto 文件 /proto/item.proto /* syntax = \u0026#34;proto3\u0026#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(\u0026#39;protobufjs\u0026#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(\u0026#39;/proto/item.proto\u0026#39;); const DataArray = root.lookupType(\u0026#39;DataArray\u0026#39;); // 4. 解析数据 const decoded = DataArray.decode(new Uint8Array(arrayBuffer)); // 5. 转换格式 const result = decoded.items.map(item =\u0026gt; ({ id: item.id || \u0026#39;\u0026#39;, name: item.name || \u0026#39;\u0026#39;, kw: item.kw || \u0026#39;\u0026#39;, lock: typeof item.lock === \u0026#39;boolean\u0026#39; ? item.lock : false, category: item.category || \u0026#39;\u0026#39;, label: item.label || \u0026#39;\u0026#39;, grade: item.grade || 1 })); this.setData({ items: result }); } catch (err) { console.error(\u0026#39;解析失败:\u0026#39;, err); } }, downloadFile() { return new Promise((resolve, reject) =\u0026gt; { wx.downloadFile({ url: \u0026#39;https://your-domain.com/data.bin\u0026#39;, success: resolve, fail: reject }); }); }, readFile(tempFilePath) { return new Promise((resolve, reject) =\u0026gt; { wx.getFileSystemManager().readFile({ filePath: tempFilePath, encoding: \u0026#39;binary\u0026#39;, success: res =\u0026gt; resolve(res.data), fail: reject }); }); } }); 当文件非常大时，解析速度会有点慢啊，为了更进一步的提高速度，我们可以使用 WebAssembly，这里是一个 Wasm 示例：\n// app.js 全局初始化 const { Parser } = require(\u0026#39;./libs/protobuf/protobuf\u0026#39;); App({ onLaunch() { // 预加载 WASM this.globalData.pbParser = new Parser(); this.globalData.pbParser.initWasm(\u0026#39;https://wasm-cdn.com/protobuf.wasm\u0026#39;); } }); // 页面逻辑 Page({ async onLoad() { const arrayBuffer = await this.downloadProtoData(); const items = await this.parseWithWasm(arrayBuffer); this.setData({ items }); }, async downloadProtoData() { const { tempFilePath } = await wx.downloadFile({ url: \u0026#39;https://your-cdn.com/data.bin\u0026#39; }); return new Promise((resolve, reject) =\u0026gt; { wx.getFileSystemManager().readFile({ filePath: tempFilePath, encoding: \u0026#39;binary\u0026#39;, success: res =\u0026gt; resolve(res.data), fail: reject }); }); }, async parseWithWasm(arrayBuffer) { const parser = getApp().globalData.pbParser; // WASM 内存直传（避免复制开销） const { instance } = await parser.wasmReady; const memPtr = instance.exports.malloc(arrayBuffer.byteLength); const heap = new Uint8Array( instance.exports.memory.buffer, memPtr, arrayBuffer.byteLength ); heap.set(new Uint8Array(arrayBuffer)); // 高性能解析 const resultPtr = instance.exports.parse_protobuf( memPtr, arrayBuffer.byteLength ); // 解析结果处理 const jsonStr = instance.exports.get_json(resultPtr); const result = JSON.parse(jsonStr); // 释放 WASM 内存 instance.exports.free(memPtr); instance.exports.free(resultPtr); return result; } }); 我们需要用到胶水代码，也可以优化，如下：\nclass Parser { constructor() { this.wasmInstance = null; this.memory = null; } async initWasm(wasmUrl) { const { instance } = await WebAssembly.instantiateStreaming( fetch(wasmUrl), { env: { emscripten_notify_memory_growth: (memory) =\u0026gt; { this.memory = memory; } } } ); this.wasmInstance = instance; } get wasmReady() { return new Promise(resolve =\u0026gt; { const check = () =\u0026gt; { if (this.wasmInstance) { resolve({ instance: this.wasmInstance }); } else { setTimeout(check, 50); } }; check(); }); } } module.exports = { Parser }; 这种方案适合 pb 文件大于 5MB 时使用，对于小型数据可直接使用 JS 解析。\n我们转了一圈，发现在小程序中也可以将 PB 文件直接转换为 JS 数组对象，这样可以省去中间的 JSON 转换耗时。\n下面是直接将 Protobuf 内容转换为 JavaScript 数组对象的完整解决方案：\n// 小程序页面 js 文件 const protobuf = require(\u0026#39;protobufjs\u0026#39;); Page({ data: { items: [] // 最终需要的数组 }, onLoad() { this.loadProtobufData(); }, async loadProtobufData() { try { // 1. 下载并读取二进制文件 const arrayBuffer = await this.getFileBuffer(\u0026#39;https://example.com/data.bin\u0026#39;); // 2. 加载 Proto 定义（精简版） const root = await protobuf.load({ nested: { DataArray: { fields: { items: { rule: \u0026#39;repeated\u0026#39;, type: \u0026#39;DataItem\u0026#39;, id: 1 } } }, DataItem: { fields: { id: { type: \u0026#39;string\u0026#39;, id: 1 }, name: { type: \u0026#39;string\u0026#39;, id: 2 }, kw: { type: \u0026#39;string\u0026#39;, id: 3 }, lock: { type: \u0026#39;bool\u0026#39;, id: 4 }, category: { type: \u0026#39;string\u0026#39;, id: 5 }, label: { type: \u0026#39;string\u0026#39;, id: 6 }, grade: { type: \u0026#39;int32\u0026#39;, id: 7 } } } } }); // 3. 创建解码器 const DataArray = root.lookupType(\u0026#34;DataArray\u0026#34;); // 4. 执行转换（核心操作） const decoded = DataArray.decode(new Uint8Array(arrayBuffer)); // 5. 转换为标准 JS 数组 const result = decoded.items.map(item =\u0026gt; ({ id: item.id || \u0026#39;\u0026#39;, name: item.name || \u0026#39;\u0026#39;, kw: item.kw || \u0026#39;\u0026#39;, lock: item.lock !== undefined ? item.lock : false, category: item.category || \u0026#39;\u0026#39;, label: item.label || \u0026#39;\u0026#39;, grade: Number(item.grade) || 1 // 确保数字类型 })); this.setData({ items: result }); } catch (err) { console.error(\u0026#39;转换失败:\u0026#39;, err); wx.showToast({ title: \u0026#39;数据加载失败\u0026#39;, icon: \u0026#39;none\u0026#39; }); } }, // 封装文件获取方法 async getFileBuffer(url) { const { tempFilePath } = await new Promise((resolve, reject) =\u0026gt; { wx.downloadFile({ url, success: resolve, fail: reject }); }); return new Promise((resolve, reject) =\u0026gt; { wx.getFileSystemManager().readFile({ filePath: tempFilePath, encoding: \u0026#39;binary\u0026#39;, success: res =\u0026gt; resolve(res.data), fail: reject }); }); } }); 上述方案可以直接应用于生产环境中，可稳定处理一下规模的数据，1 万条数据（约 3MB）解析耗时小于 200ms。\n","permalink":"https://blog.91demo.top/wiki/pb2json.html","summary":"\u003cp\u003e我们知道 Protobuf 文件可以减少体积，方便存储和传输，但在获取文件后，我们还需要将其转换为 JSON 格式，以适配应用进行处理数据。\u003c/p\u003e\n\u003cp\u003e下面的代码是在微信小程序中使用，将 pb 文件转换为 JSON，然后再使用。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 小程序根目录执行安装（需开启 npm 支持）\n// npm install protobufjs\n\n// 创建 proto 文件 /proto/item.proto\n/*\nsyntax = \u0026#34;proto3\u0026#34;;\nmessage DataItem {\n  string id = 1;\n  string name = 2;\n  string kw = 3;\n  bool lock = 4;\n  string category = 5;\n  string label = 6;\n  int32 grade = 7;\n}\nmessage DataArray { repeated DataItem items = 1; }\n*/\n\n// 在 app.js 中引入\nconst protobuf = require(\u0026#39;protobufjs\u0026#39;);\n\n// 页面 JS 文件\nPage({\n  data: { items: [] },\n\n  onLoad() {\n    this.loadProtoData();\n  },\n\n  async loadProtoData() {\n    try {\n      // 1. 下载二进制文件\n      const { tempFilePath } = await this.downloadFile();\n\n      // 2. 读取文件内容\n      const arrayBuffer = await this.readFile(tempFilePath);\n\n      // 3. 加载 Proto 定义\n      const root = await protobuf.load(\u0026#39;/proto/item.proto\u0026#39;);\n      const DataArray = root.lookupType(\u0026#39;DataArray\u0026#39;);\n\n      // 4. 解析数据\n      const decoded = DataArray.decode(new Uint8Array(arrayBuffer));\n\n      // 5. 转换格式\n      const result = decoded.items.map(item =\u0026gt; ({\n        id: item.id || \u0026#39;\u0026#39;,\n        name: item.name || \u0026#39;\u0026#39;,\n        kw: item.kw || \u0026#39;\u0026#39;,\n        lock: typeof item.lock === \u0026#39;boolean\u0026#39; ? item.lock : false,\n        category: item.category || \u0026#39;\u0026#39;,\n        label: item.label || \u0026#39;\u0026#39;,\n        grade: item.grade || 1\n      }));\n\n      this.setData({ items: result });\n    } catch (err) {\n      console.error(\u0026#39;解析失败:\u0026#39;, err);\n    }\n  },\n\n  downloadFile() {\n    return new Promise((resolve, reject) =\u0026gt; {\n      wx.downloadFile({\n        url: \u0026#39;https://your-domain.com/data.bin\u0026#39;,\n        success: resolve,\n        fail: reject\n      });\n    });\n  },\n\n  readFile(tempFilePath) {\n    return new Promise((resolve, reject) =\u0026gt; {\n      wx.getFileSystemManager().readFile({\n        filePath: tempFilePath,\n        encoding: \u0026#39;binary\u0026#39;,\n        success: res =\u0026gt; resolve(res.data),\n        fail: reject\n      });\n    });\n  }\n});\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e当文件非常大时，解析速度会有点慢啊，为了更进一步的提高速度，我们可以使用 WebAssembly，这里是一个 Wasm 示例：\u003c/p\u003e","title":"解析Protobuf文件并转为JSON格式"},{"content":"我需要将 JSON 字符串中的一个对象属性删除，如果手动删除，又累又无聊。\n一个 Python 脚本搞定。\nimport json def remove_filename_and_save(input_path, output_path): \u0026#34;\u0026#34;\u0026#34; 从JSON数组中移除所有对象的filename属性并保存到新文件 Args: input_path: 输入JSON文件路径 output_path: 输出JSON文件路径 \u0026#34;\u0026#34;\u0026#34; try: # 读取输入JSON文件 with open(input_path, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: data = json.load(f) # 检查是否是数组 if not isinstance(data, list): print(\u0026#34;错误：输入JSON不是数组对象\u0026#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 != \u0026#39;filename\u0026#39;} processed_data.append(new_item) else: processed_data.append(item) # 非字典元素保持不变 # 写入输出文件 with open(output_path, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: json.dump(processed_data, f, ensure_ascii=False, indent=2) print(f\u0026#34;成功处理并保存到 {output_path}\u0026#34;) return True except FileNotFoundError: print(f\u0026#34;错误：输入文件 {input_path} 未找到\u0026#34;) return False except json.JSONDecodeError: print(f\u0026#34;错误：输入文件 {input_path} 不是有效的JSON格式\u0026#34;) return False except Exception as e: print(f\u0026#34;处理文件时出错: {e}\u0026#34;) return False if __name__ == \u0026#34;__main__\u0026#34;: # 文件路径配置 input_json = \u0026#34;/data/data.json\u0026#34; # 输入文件路径 output_json = \u0026#34;/data/data-temp.json\u0026#34; # 输出文件路径 # 执行处理 if remove_filename_and_save(input_json, output_json): print(f\u0026#34;操作成功完成，结果已保存到 {output_json}\u0026#34;) else: print(\u0026#34;操作失败，请检查错误信息\u0026#34;) 可以基于该脚本进行扩展。\n","permalink":"https://blog.91demo.top/wiki/removejsonfield.html","summary":"\u003cp\u003e我需要将 JSON 字符串中的一个对象属性删除，如果手动删除，又累又无聊。\u003c/p\u003e\n\u003cp\u003e一个 Python 脚本搞定。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eimport json\n\ndef remove_filename_and_save(input_path, output_path):\n    \u0026#34;\u0026#34;\u0026#34;\n    从JSON数组中移除所有对象的filename属性并保存到新文件\n\n    Args:\n        input_path: 输入JSON文件路径\n        output_path: 输出JSON文件路径\n    \u0026#34;\u0026#34;\u0026#34;\n    try:\n        # 读取输入JSON文件\n        with open(input_path, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f:\n            data = json.load(f)\n\n        # 检查是否是数组\n        if not isinstance(data, list):\n            print(\u0026#34;错误：输入JSON不是数组对象\u0026#34;)\n            return False\n\n        # 处理每个对象\n        processed_data = []\n        for item in data:\n            if isinstance(item, dict):\n                # 创建新对象，排除filename属性\n                new_item = {k: v for k, v in item.items() if k != \u0026#39;filename\u0026#39;}\n                processed_data.append(new_item)\n            else:\n                processed_data.append(item)  # 非字典元素保持不变\n\n        # 写入输出文件\n        with open(output_path, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f:\n            json.dump(processed_data, f, ensure_ascii=False, indent=2)\n\n        print(f\u0026#34;成功处理并保存到 {output_path}\u0026#34;)\n        return True\n\n    except FileNotFoundError:\n        print(f\u0026#34;错误：输入文件 {input_path} 未找到\u0026#34;)\n        return False\n    except json.JSONDecodeError:\n        print(f\u0026#34;错误：输入文件 {input_path} 不是有效的JSON格式\u0026#34;)\n        return False\n    except Exception as e:\n        print(f\u0026#34;处理文件时出错: {e}\u0026#34;)\n        return False\n\nif __name__ == \u0026#34;__main__\u0026#34;:\n    # 文件路径配置\n    input_json = \u0026#34;/data/data.json\u0026#34;       # 输入文件路径\n    output_json = \u0026#34;/data/data-temp.json\u0026#34; # 输出文件路径\n\n    # 执行处理\n    if remove_filename_and_save(input_json, output_json):\n        print(f\u0026#34;操作成功完成，结果已保存到 {output_json}\u0026#34;)\n    else:\n        print(\u0026#34;操作失败，请检查错误信息\u0026#34;)\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e可以基于该脚本进行扩展。\u003c/p\u003e","title":"删除JSON数组对象中的某个字段"},{"content":"今天我们要讲到含金量非常高的技术了，将 Markdown 内容渲染为小程序页面，这是豆子碎片文章页面。\n在微信小程序中，渲染 Markdown 内容可以使文章展示更加美观和统一。本文将通过一个完整的示例，演示如何从服务器下载 Markdown 内容，并使用 towxml 组件在小程序端渲染为页面。\n目录 需求分析 页面布局 下载 Markdown 内容 使用 towxml 渲染 Markdown 代码与预览效果对比 1. 需求分析 在本次实战中，豆子碎片文章页面需要实现以下功能：\n从服务器下载 Markdown 内容 使用 towxml 组件将 Markdown 内容渲染为页面 2. 页面布局 豆子碎片文章页面需要包含以下几个部分：\n标题区域，用于展示文章标题 内容区域，用于展示渲染后的 Markdown 内容 示例代码 { \u0026#34;pages\u0026#34;: [\u0026#34;pages/index/index\u0026#34;, \u0026#34;pages/article/article\u0026#34;], \u0026#34;window\u0026#34;: { \u0026#34;navigationBarBackgroundColor\u0026#34;: \u0026#34;#ffffff\u0026#34;, \u0026#34;navigationBarTextStyle\u0026#34;: \u0026#34;black\u0026#34;, \u0026#34;navigationBarTitleText\u0026#34;: \u0026#34;豆子碎片文章\u0026#34; } } \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;article-title\u0026#34;\u0026gt;{{title}}\u0026lt;/view\u0026gt; \u0026lt;import src=\u0026#34;/towxml/entry.wxml\u0026#34; /\u0026gt; \u0026lt;template is=\u0026#34;entry\u0026#34; data=\u0026#34;{{...articleContent}}\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; .container { padding: 20px; } .article-title { font-size: 24px; font-weight: bold; margin-bottom: 20px; } 3. 下载 Markdown 内容 通过微信小程序的wx.request方法，从服务器下载 Markdown 内容。\n示例代码 const towxml = require(\u0026#34;/towxml/main\u0026#34;); // 引入towxml库 Page({ data: { title: \u0026#34;\u0026#34;, articleContent: {}, }, onLoad: function (options) { this.loadArticle(options.id); }, loadArticle: function (id) { const url = `https://your-server-url.com/articles/${id}.md`; wx.request({ url: url, success: (res) =\u0026gt; { const markdown = res.data; this.parseMarkdown(markdown); }, }); }, parseMarkdown: function (markdown) { const data = towxml.toJson(markdown, \u0026#34;markdown\u0026#34;); // 解析Markdown内容 this.setData({ title: data.title, articleContent: data, }); }, }); 4. 使用 towxml 渲染 Markdown 通过 towxml 组件，将下载并解析的 Markdown 内容渲染为页面。\n示例代码 const towxml = require(\u0026#34;/towxml/main\u0026#34;); // 引入towxml库 Page({ data: { title: \u0026#34;\u0026#34;, articleContent: {}, }, onLoad: function (options) { this.loadArticle(options.id); }, loadArticle: function (id) { const url = `https://your-server-url.com/articles/${id}.md`; wx.request({ url: url, success: (res) =\u0026gt; { const markdown = res.data; this.parseMarkdown(markdown); }, }); }, parseMarkdown: function (markdown) { const data = towxml.toJson(markdown, \u0026#34;markdown\u0026#34;); // 解析Markdown内容 this.setData({ title: data.title, articleContent: data, }); }, }); 5. 代码与预览效果对比 代码 { \u0026#34;pages\u0026#34;: [\u0026#34;pages/index/index\u0026#34;, \u0026#34;pages/article/article\u0026#34;], \u0026#34;window\u0026#34;: { \u0026#34;navigationBarBackgroundColor\u0026#34;: \u0026#34;#ffffff\u0026#34;, \u0026#34;navigationBarTextStyle\u0026#34;: \u0026#34;black\u0026#34;, \u0026#34;navigationBarTitleText\u0026#34;: \u0026#34;豆子碎片文章\u0026#34; } } \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;article-title\u0026#34;\u0026gt;{{title}}\u0026lt;/view\u0026gt; \u0026lt;import src=\u0026#34;/towxml/entry.wxml\u0026#34; /\u0026gt; \u0026lt;template is=\u0026#34;entry\u0026#34; data=\u0026#34;{{...articleContent}}\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; .container { padding: 20px; } .article-title { font-size: 24px; font-weight: bold; margin-bottom: 20px; } const towxml = require(\u0026#34;/towxml/main\u0026#34;); // 引入towxml库 Page({ data: { title: \u0026#34;\u0026#34;, articleContent: {}, }, onLoad: function (options) { this.loadArticle(options.id); }, loadArticle: function (id) { const url = `https://your-server-url.com/articles/${id}.md`; wx.request({ url: url, success: (res) =\u0026gt; { const markdown = res.data; this.parseMarkdown(markdown); }, }); }, parseMarkdown: function (markdown) { const data = towxml.toJson(markdown, \u0026#34;markdown\u0026#34;); // 解析Markdown内容 this.setData({ title: data.title, articleContent: data, }); }, }); 预览效果 打开微信开发者工具，创建一个新项目并将上述代码粘贴到相应文件中。 运行小程序，进入文章页面，页面会从服务器下载 Markdown 内容并显示解析后的文章。 通过以上示例，我们学习了如何在微信小程序中从服务器下载 Markdown 内容，并使用 towxml 组件进行渲染。希望这篇文章能帮助您更好地理解和应用这些技术，实现功能丰富的文章展示页面。\n","permalink":"https://blog.91demo.top/wiki/articlepage.html","summary":"\u003cp\u003e今天我们要讲到含金量非常高的技术了，将 Markdown 内容渲染为小程序页面，这是豆子碎片文章页面。\u003c/p\u003e\n\u003cp\u003e在微信小程序中，渲染 Markdown 内容可以使文章展示更加美观和统一。本文将通过一个完整的示例，演示如何从服务器下载 Markdown 内容，并使用 towxml 组件在小程序端渲染为页面。\u003c/p\u003e\n\u003ch2 id=\"目录\"\u003e目录\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e需求分析\u003c/li\u003e\n\u003cli\u003e页面布局\u003c/li\u003e\n\u003cli\u003e下载 Markdown 内容\u003c/li\u003e\n\u003cli\u003e使用 towxml 渲染 Markdown\u003c/li\u003e\n\u003cli\u003e代码与预览效果对比\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"1-需求分析\"\u003e1. 需求分析\u003c/h2\u003e\n\u003cp\u003e在本次实战中，豆子碎片文章页面需要实现以下功能：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从服务器下载 Markdown 内容\u003c/li\u003e\n\u003cli\u003e使用 towxml 组件将 Markdown 内容渲染为页面\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"2-页面布局\"\u003e2. 页面布局\u003c/h2\u003e\n\u003cp\u003e豆子碎片文章页面需要包含以下几个部分：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e标题区域，用于展示文章标题\u003c/li\u003e\n\u003cli\u003e内容区域，用于展示渲染后的 Markdown 内容\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"示例代码\"\u003e示例代码\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;pages\u0026#34;\u003c/span\u003e: [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;pages/index/index\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;pages/article/article\u0026#34;\u003c/span\u003e],\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;window\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;navigationBarBackgroundColor\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;#ffffff\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;navigationBarTextStyle\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;black\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;navigationBarTitleText\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;豆子碎片文章\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003eview\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;container\u0026#34;\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;\u003cspan style=\"color:#f92672\"\u003eview\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;article-title\u0026#34;\u003c/span\u003e\u0026gt;{{title}}\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eview\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/towxml/entry.wxml\u0026#34;\u003c/span\u003e /\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;\u003cspan style=\"color:#f92672\"\u003etemplate\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eis\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;entry\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;{{...articleContent}}\u0026#34;\u003c/span\u003e /\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eview\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-css\" data-lang=\"css\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e.\u003cspan style=\"color:#a6e22e\"\u003econtainer\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003epadding\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e20\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003epx\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e.\u003cspan style=\"color:#a6e22e\"\u003earticle-title\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003efont-size\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e24\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003epx\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003efont-weight\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003ebold\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003emargin-bottom\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e20\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003epx\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"3-下载-markdown-内容\"\u003e3. 下载 Markdown 内容\u003c/h2\u003e\n\u003cp\u003e通过微信小程序的\u003ccode\u003ewx.request\u003c/code\u003e方法，从服务器下载 Markdown 内容。\u003c/p\u003e","title":"使用towxml组件在小程序中渲染Markdown内容"},{"content":"我们已经知道如何上架，但是针对内容类小程序，我们还需要再特别注意一些内容。\n引用真实案例！对于内容展示类的小程序，根据微信小程序的审核规则，个人主体的小程序不能涉及“资讯类”内容（如直接展示文章列表、新闻聚合等），否则会被要求以企业主体重新提交。我是技术类文章展示小程序，即使这样，也被拒多次，以下是针对这一问题的优化方案，既能规避审核风险，又能合理引导用户访问技术文章：\n核心原则 内容不应牵涉政治、时事热点、色情暴力等信息，这个无需多说。 首页不直接展示文章内容列表，避免被判定为“资讯类”。这是资讯类默认布局，容易违反。 通过功能入口间接引导用户到二级页面（如搜索、分类、专题入口），在二级页面展示文章列表。 突出技术工具属性，而非内容聚合平台。一般指展示的内容没有相关性，不能突出主题。 内容应偏重于教程、经验性质，而非实时信息，不应具有时效性。比如相关技术最新消息很容易判定为资讯类。 在确定核心原则之后，以下几个推荐布局可以避免判定为资讯类布局。\n方案 1：搜索 + 分类入口 + 专题推荐 \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;!-- 搜索框（保持顶部） --\u0026gt; \u0026lt;view class=\u0026#34;search-box\u0026#34;\u0026gt; \u0026lt;input placeholder=\u0026#34;搜索技术文章\u0026#34; bindtap=\u0026#34;navigateToSearch\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 快捷分类入口（工具按钮） --\u0026gt; \u0026lt;view class=\u0026#34;category-buttons\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;button\u0026#34; bindtap=\u0026#34;navigateToCategory\u0026#34; data-category=\u0026#34;前端\u0026#34;\u0026gt;前端技术\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;button\u0026#34; bindtap=\u0026#34;navigateToCategory\u0026#34; data-category=\u0026#34;后端\u0026#34;\u0026gt;后端架构\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;button\u0026#34; bindtap=\u0026#34;navigateToCategory\u0026#34; data-category=\u0026#34;数据库\u0026#34;\u0026gt;数据库优化\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 专题推荐（以“技术专题”形式包装） --\u0026gt; \u0026lt;view class=\u0026#34;special-topics\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;section-title\u0026#34;\u0026gt;精选技术专题\u0026lt;/text\u0026gt; \u0026lt;view class=\u0026#34;topic\u0026#34; bindtap=\u0026#34;navigateToTopic\u0026#34; data-id=\u0026#34;1\u0026#34;\u0026gt; \u0026lt;image src=\u0026#34;/images/topic1.png\u0026#34; mode=\u0026#34;aspectFill\u0026#34; /\u0026gt; \u0026lt;text class=\u0026#34;topic-title\u0026#34;\u0026gt;前端性能优化实战\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;topic\u0026#34; bindtap=\u0026#34;navigateToTopic\u0026#34; data-id=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;image src=\u0026#34;/images/topic2.png\u0026#34; mode=\u0026#34;aspectFill\u0026#34; /\u0026gt; \u0026lt;text class=\u0026#34;topic-title\u0026#34;\u0026gt;高并发系统设计指南\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 方案 2：搜索 + 问题解答入口 + 技术资源导航 \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;!-- 搜索框 --\u0026gt; \u0026lt;view class=\u0026#34;search-box\u0026#34;\u0026gt; \u0026lt;input placeholder=\u0026#34;输入技术问题关键词\u0026#34; bindtap=\u0026#34;navigateToSearch\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 技术问题快速入口（做成问答功能） --\u0026gt; \u0026lt;view class=\u0026#34;qa-section\u0026#34;\u0026gt; \u0026lt;text class=\u0026#34;section-title\u0026#34;\u0026gt;常见技术问题\u0026lt;/text\u0026gt; \u0026lt;view class=\u0026#34;qa-item\u0026#34; bindtap=\u0026#34;navigateToArticle\u0026#34; data-id=\u0026#34;101\u0026#34;\u0026gt; \u0026lt;text\u0026gt;如何解决 Vue 3 响应式丢失问题？\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;qa-item\u0026#34; bindtap=\u0026#34;navigateToArticle\u0026#34; data-id=\u0026#34;102\u0026#34;\u0026gt; \u0026lt;text\u0026gt;MySQL 索引优化最佳实践\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 技术资源导航（强调工具属性） --\u0026gt; \u0026lt;view class=\u0026#34;resource-nav\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;nav-card\u0026#34; bindtap=\u0026#34;navigateToPage\u0026#34; data-page=\u0026#34;opensource\u0026#34;\u0026gt; \u0026lt;image src=\u0026#34;/images/opensource.png\u0026#34; /\u0026gt; \u0026lt;text\u0026gt;开源项目推荐\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;nav-card\u0026#34; bindtap=\u0026#34;navigateToPage\u0026#34; data-page=\u0026#34;tools\u0026#34;\u0026gt; \u0026lt;image src=\u0026#34;/images/tools.png\u0026#34; /\u0026gt; \u0026lt;text\u0026gt;在线开发工具\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 关键审核规避策略 避免“文章列表”字眼：\n用“技术专题”、“常见问题”、“开源项目”等替代描述。 二级页面标题不要用“文章列表”，改为“搜索结果”、“专题详情”等。 强化工具属性（可选）：\n在首页添加简单的开发者工具，可以根据设计目的来确定（如 JSON 格式化、时间戳转换）。 在页面描述中强调“技术问题解答”、“开发工具集合”。 内容分散化：\n不要集中展示文章，通过搜索、分类、专题入口间接引导。 单个文章入口包装成“问题解答”或“技术方案”。 审核敏感词处理：\n避免在首页出现“资讯”、“新闻”、“文章列表”等词汇。 使用“技术专题”、“开发指南”、“实战案例”等中性表述。 示例交互逻辑（JS 部分） // 首页跳转到搜索页 navigateToSearch() { wx.navigateTo({ url: \u0026#39;/pages/search/search\u0026#39; }) }, // 跳转到分类页（二级页面展示文章列表） navigateToCategory(e) { const category = e.currentTarget.dataset.category; wx.navigateTo({ url: `/pages/category/category?type=${category}` }) }, // 跳转到专题详情页 navigateToTopic(e) { const topicId = e.currentTarget.dataset.id; wx.navigateTo({ url: `/pages/topic/topic?id=${topicId}` }) } 过审后注意事项 二级页面内容控制：\n确保技术文章为原创或获得转载授权。 避免涉及政治、社会新闻等敏感话题。 定期更新内容：\n保持技术文章的实用性，避免被误判为“低质内容聚合”。 用户反馈通道：\n添加“问题反馈”入口，及时处理审核相关投诉。 通过以上方案，你可以既保留技术文章的核心功能，又符合个人小程序的审核规则。实际案例中，许多技术类小程序通过类似设计通过审核（如“豆子碎片”）。\n","permalink":"https://blog.91demo.top/wiki/visitauth.html","summary":"\u003cp\u003e我们已经知道如何上架，但是针对内容类小程序，我们还需要再特别注意一些内容。\u003c/p\u003e\n\u003cp\u003e引用真实案例！对于内容展示类的小程序，根据微信小程序的审核规则，\u003cstrong\u003e个人主体的小程序不能涉及“资讯类”内容\u003c/strong\u003e（如直接展示文章列表、新闻聚合等），否则会被要求以企业主体重新提交。我是技术类文章展示小程序，即使这样，也被拒多次，以下是针对这一问题的优化方案，既能规避审核风险，又能合理引导用户访问技术文章：\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"核心原则\"\u003e\u003cstrong\u003e核心原则\u003c/strong\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e内容不应牵涉政治、时事热点、色情暴力等信息\u003c/strong\u003e，这个无需多说。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e首页不直接展示文章内容列表\u003c/strong\u003e，避免被判定为“资讯类”。这是资讯类默认布局，容易违反。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e通过功能入口间接引导用户到二级页面\u003c/strong\u003e（如搜索、分类、专题入口），在二级页面展示文章列表。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e突出技术工具属性\u003c/strong\u003e，而非内容聚合平台。一般指展示的内容没有相关性，不能突出主题。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e内容应偏重于教程、经验性质\u003c/strong\u003e，而非实时信息，不应具有时效性。比如相关技术最新消息很容易判定为资讯类。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003cp\u003e在确定核心原则之后，以下几个推荐布局可以避免判定为资讯类布局。\u003c/p\u003e\n\u003ch4 id=\"方案-1搜索--分类入口--专题推荐\"\u003e\u003cstrong\u003e方案 1：搜索 + 分类入口 + 专题推荐\u003c/strong\u003e\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;container\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 搜索框（保持顶部） --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;search-box\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eplaceholder=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;搜索技术文章\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToSearch\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 快捷分类入口（工具按钮） --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;category-buttons\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;button\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToCategory\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-category=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;前端\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e前端技术\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;button\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToCategory\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-category=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;后端\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e后端架构\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;button\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToCategory\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-category=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;数据库\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e数据库优化\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 专题推荐（以“技术专题”形式包装） --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;special-topics\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;section-title\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e精选技术专题\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;topic\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToTopic\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-id=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;image\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/images/topic1.png\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emode=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;aspectFill\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;topic-title\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e前端性能优化实战\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;topic\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToTopic\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-id=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;2\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;image\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/images/topic2.png\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emode=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;aspectFill\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;topic-title\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e高并发系统设计指南\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch4 id=\"方案-2搜索--问题解答入口--技术资源导航\"\u003e\u003cstrong\u003e方案 2：搜索 + 问题解答入口 + 技术资源导航\u003c/strong\u003e\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;container\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 搜索框 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;search-box\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eplaceholder=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;输入技术问题关键词\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToSearch\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 技术问题快速入口（做成问答功能） --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;qa-section\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;section-title\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e常见技术问题\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;qa-item\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToArticle\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-id=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;101\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u0026gt;\u003c/span\u003e如何解决 Vue 3 响应式丢失问题？\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;qa-item\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToArticle\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-id=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;102\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u0026gt;\u003c/span\u003eMySQL 索引优化最佳实践\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 技术资源导航（强调工具属性） --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;resource-nav\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;nav-card\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToPage\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-page=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;opensource\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;image\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/images/opensource.png\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u0026gt;\u003c/span\u003e开源项目推荐\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;view\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eclass=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;nav-card\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebindtap=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;navigateToPage\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-page=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;tools\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;image\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/images/tools.png\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026lt;text\u0026gt;\u003c/span\u003e在线开发工具\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/text\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/view\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch3 id=\"关键审核规避策略\"\u003e\u003cstrong\u003e关键审核规避策略\u003c/strong\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e避免“文章列表”字眼\u003c/strong\u003e：\u003c/p\u003e","title":"优化内容类小程序快速审核通过"},{"content":"当我们创建笔记索引内容后，如何搜索是个关键的问题，目前的设计就是先搜索 name，看名称中是否包含关键字，然后查找 kws 数组，查看数组中是否有关键字，最后查看 tags 数组中是否有关键字？查到以后取出来，返回数组列表。\n数据的结构如下：\n[{ \u0026#34;id\u0026#34;:\u0026#34;id1\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;小星星\u0026#34;, \u0026#34;kws\u0026#34;:[\u0026#34;爱\u0026#34;,\u0026#34;崇拜\u0026#34;], \u0026#34;tags\u0026#34;:[\u0026#34;想法\u0026#34;,\u0026#34;感情\u0026#34;], }] 如果使用 JS 来实现，那么代码如下：\n/** * 搜索笔记 * @param {Array} notes 笔记数组 * @param {String} keyword 搜索关键词 * @returns {Array} 匹配的笔记数组 */ function searchNotes(notes, keyword) { if (!keyword || typeof keyword !== \u0026#39;string\u0026#39;) { return []; // 如果关键词无效，返回空数组 } // 统一转换为小写以便不区分大小写搜索 const lowerKeyword = keyword.toLowerCase(); return notes.filter(note =\u0026gt; { // 检查名称是否包含关键词 const nameMatch = note.name \u0026amp;\u0026amp; note.name.toLowerCase().includes(lowerKeyword); // 检查kws数组是否包含关键词 const kwsMatch = Array.isArray(note.kws) \u0026amp;\u0026amp; note.kws.some(kw =\u0026gt; kw \u0026amp;\u0026amp; kw.toLowerCase().includes(lowerKeyword)); // 检查tags数组是否包含关键词 const tagsMatch = Array.isArray(note.tags) \u0026amp;\u0026amp; note.tags.some(tag =\u0026gt; tag \u0026amp;\u0026amp; tag.toLowerCase().includes(lowerKeyword)); // 如果任一条件匹配，则包含该笔记 return nameMatch || kwsMatch || tagsMatch; }); } 这里支持多维度搜索，同时搜索名称、关键词和标签三个字段，当搜索时，先将关键字转为小写，这样比较时不区分大小写。为了性能优化，使用 filter 和 some 方法，减少不必须要的循环。\n","permalink":"https://blog.91demo.top/wiki/searchkw.html","summary":"\u003cp\u003e当我们创建笔记索引内容后，如何搜索是个关键的问题，目前的设计就是先搜索 name，看名称中是否包含关键字，然后查找 kws 数组，查看数组中是否有关键字，最后查看 tags 数组中是否有关键字？查到以后取出来，返回数组列表。\u003c/p\u003e\n\u003cp\u003e数据的结构如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[{\n    \u0026#34;id\u0026#34;:\u0026#34;id1\u0026#34;,\n    \u0026#34;name\u0026#34;:\u0026#34;小星星\u0026#34;,\n    \u0026#34;kws\u0026#34;:[\u0026#34;爱\u0026#34;,\u0026#34;崇拜\u0026#34;],\n    \u0026#34;tags\u0026#34;:[\u0026#34;想法\u0026#34;,\u0026#34;感情\u0026#34;],\n}]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如果使用 JS 来实现，那么代码如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e/**\n * 搜索笔记\n * @param {Array} notes 笔记数组\n * @param {String} keyword 搜索关键词\n * @returns {Array} 匹配的笔记数组\n */\nfunction searchNotes(notes, keyword) {\n    if (!keyword || typeof keyword !== \u0026#39;string\u0026#39;) {\n        return []; // 如果关键词无效，返回空数组\n    }\n\n    // 统一转换为小写以便不区分大小写搜索\n    const lowerKeyword = keyword.toLowerCase();\n\n    return notes.filter(note =\u0026gt; {\n        // 检查名称是否包含关键词\n        const nameMatch = note.name \u0026amp;\u0026amp; note.name.toLowerCase().includes(lowerKeyword);\n\n        // 检查kws数组是否包含关键词\n        const kwsMatch = Array.isArray(note.kws) \u0026amp;\u0026amp;\n            note.kws.some(kw =\u0026gt; kw \u0026amp;\u0026amp; kw.toLowerCase().includes(lowerKeyword));\n\n        // 检查tags数组是否包含关键词\n        const tagsMatch = Array.isArray(note.tags) \u0026amp;\u0026amp;\n            note.tags.some(tag =\u0026gt; tag \u0026amp;\u0026amp; tag.toLowerCase().includes(lowerKeyword));\n\n        // 如果任一条件匹配，则包含该笔记\n        return nameMatch || kwsMatch || tagsMatch;\n    });\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这里支持多维度搜索，同时搜索名称、关键词和标签三个字段，当搜索时，先将关键字转为小写，这样比较时不区分大小写。为了性能优化，使用 filter 和 some 方法，减少不必须要的循环。\u003c/p\u003e","title":"在列表中搜索符合条件的内容"},{"content":"这是一项含金量非常高的技能，在页面中动态添加广告，可以增加广告曝光量和提高点击率。\n小程序使用 Markdown 来编写文章内容，存储在服务器或者后端对象存储，小程序打开时，会从服务器拉取文章索引，然后展示文章列表页面给用户浏览。当用户点击列表上的某篇文章时，小程序下载该篇文章的 Markdown 内容，并在小程序端使用 towxml 组件进行解析渲染，该组件可以将 Markdown 内容渲染成 WXML 文件内容。\n文章页面集成了流量主原生模板广告。起初是在页面顶端显示，直接在 article.wxml 页面顶部添加模板广告代码，就可以集成广告了，非常的简单。当前文章页面布局如下：从上到下广告组件、Markdown 文章内容、页面底部说明。但是当实际上线后，发现这样的广告曝光量虽然非常高，但是页面整体看起来非常的不舒服，不美观。然后将广告位置调整到文章底部，因为文章底部和页面底部之间有自定义内容，广告看起来还比较舒服。当前文章页面布局如下：从上到下 Markdown 文章内容、广告组件、页面底部说明。上线之后，发现实际的曝光量降低了很多，因为很多用户是不会浏览到页面底部的。所以广告也不会曝光。得想想办法。在写公众号时，发现公众号插入到文章中间的广告模式不错。我感觉这是一个不错的方案。可以这样试试。\n现在就是如何将广告显示在文章中间。文章内容是使用 towxml 渲染的。比如，将广告显示在第一个段落之后。最初的想法，是查找文章页面的段落 p 元素，然后动态插入广告。根据微信官方文档内容，发现可以使用 wx.createSelectorQuery API，但当实际调试后，发现它无法穿透查询 towxml 生成的内容。然后使用 this.createSelectorQuery，这个是查询组件元素使用的，发现这个确实可以查询组件的元素，也确实查找到了 towxml 生成的内容的顶层元素，但是 towxml 生成的内容是多层嵌套，towxml 下面的组件是 decode，decode 然后再进行嵌套自定义组件。也就是说页面中存在着多个#shadow-root，它用来隔离元素和样式。感觉如果这样的布局使用这种方案行不通了。或者即使实现也非常的复杂，还是需要修改 towxml 组件。\n当上面通过查询页面元素行不通后，就得想其它办法。虽然这个方案没有解决问题，但是学到了很多东西，以后在同一个#shadow-root 下的元素查找技能就学会了。本来一直不想动 towxml 库，现在的方案只有修改这个库了，来适配自己的需求。towxml 库的原理如下，解析原始的 Markdown 文件或者 Html 文件生成 AST，然后将 AST 转换成小程序 WXML 标签和结构。这里有两种方法：一种是在解析时处理，生成对应的广告组件；另一种是解析成对象后再插入数据。第一种方法实现后，就可以在 HTML 或 Markdown 文件中加入广告标签，然后 towxml 自动解析。虽然这一种功能看起来更强大，但是我的实际场景不是太适合，我的文章来源 Markdown 内容不一定是我编写，我的小程序只是内容展示平台。所以只能选择第二种方法，这个方法更灵活，可以在解析后的 AST 里插入广告数据，然后在 WXML 页面里根据条件进行渲染。这样用户提交的内容，我不进行修改就可以显示广告。想想还是不错的。\n接下来就要考虑如何在 AST 插入数据，以及插入到哪个位置？如何查找段落位置？页面如何显示？我可以修改 towxml 库，以适配广告组件渲染，或者在自己的文章页面进行广告组件渲染。无论使用哪种方法，完整的数据流应该是这样的：Markdown 原始文件 -\u0026gt; Towxml 解析 -\u0026gt; 插入广告节点 -\u0026gt; 绑定广告数据 -\u0026gt; 渲染带广告的 WXML。剩下的内容就是想办法解决渲染广告组件这个问题了。\n想要浏览效果，可关注豆子小栈公众号，打开豆子碎片小程序（原名称豆子碎片）查看。\n","permalink":"https://blog.91demo.top/wiki/dynaddad.html","summary":"\u003cp\u003e这是一项含金量非常高的技能，在页面中动态添加广告，可以增加广告曝光量和提高点击率。\u003c/p\u003e\n\u003cp\u003e小程序使用 Markdown 来编写文章内容，存储在服务器或者后端对象存储，小程序打开时，会从服务器拉取文章索引，然后展示文章列表页面给用户浏览。当用户点击列表上的某篇文章时，小程序下载该篇文章的 Markdown 内容，并在小程序端使用 towxml 组件进行解析渲染，该组件可以将 Markdown 内容渲染成 WXML 文件内容。\u003c/p\u003e\n\u003cp\u003e文章页面集成了流量主原生模板广告。起初是在页面顶端显示，直接在 article.wxml 页面顶部添加模板广告代码，就可以集成广告了，非常的简单。当前文章页面布局如下：从上到下广告组件、Markdown 文章内容、页面底部说明。但是当实际上线后，发现这样的广告曝光量虽然非常高，但是页面整体看起来非常的不舒服，不美观。然后将广告位置调整到文章底部，因为文章底部和页面底部之间有自定义内容，广告看起来还比较舒服。当前文章页面布局如下：从上到下 Markdown 文章内容、广告组件、页面底部说明。上线之后，发现实际的曝光量降低了很多，因为很多用户是不会浏览到页面底部的。所以广告也不会曝光。得想想办法。在写公众号时，发现公众号插入到文章中间的广告模式不错。我感觉这是一个不错的方案。可以这样试试。\u003c/p\u003e\n\u003cp\u003e现在就是如何将广告显示在文章中间。文章内容是使用 towxml 渲染的。比如，将广告显示在第一个段落之后。最初的想法，是查找文章页面的段落 p 元素，然后动态插入广告。根据微信官方文档内容，发现可以使用 wx.createSelectorQuery API，但当实际调试后，发现它无法穿透查询 towxml 生成的内容。然后使用 this.createSelectorQuery，这个是查询组件元素使用的，发现这个确实可以查询组件的元素，也确实查找到了 towxml 生成的内容的顶层元素，但是 towxml 生成的内容是多层嵌套，towxml 下面的组件是 decode，decode 然后再进行嵌套自定义组件。也就是说页面中存在着多个#shadow-root，它用来隔离元素和样式。感觉如果这样的布局使用这种方案行不通了。或者即使实现也非常的复杂，还是需要修改 towxml 组件。\u003c/p\u003e\n\u003cp\u003e当上面通过查询页面元素行不通后，就得想其它办法。虽然这个方案没有解决问题，但是学到了很多东西，以后在同一个#shadow-root 下的元素查找技能就学会了。本来一直不想动 towxml 库，现在的方案只有修改这个库了，来适配自己的需求。towxml 库的原理如下，解析原始的 Markdown 文件或者 Html 文件生成 AST，然后将 AST 转换成小程序 WXML 标签和结构。这里有两种方法：一种是在解析时处理，生成对应的广告组件；另一种是解析成对象后再插入数据。第一种方法实现后，就可以在 HTML 或 Markdown 文件中加入广告标签，然后 towxml 自动解析。虽然这一种功能看起来更强大，但是我的实际场景不是太适合，我的文章来源 Markdown 内容不一定是我编写，我的小程序只是内容展示平台。所以只能选择第二种方法，这个方法更灵活，可以在解析后的 AST 里插入广告数据，然后在 WXML 页面里根据条件进行渲染。这样用户提交的内容，我不进行修改就可以显示广告。想想还是不错的。\u003c/p\u003e\n\u003cp\u003e接下来就要考虑如何在 AST 插入数据，以及插入到哪个位置？如何查找段落位置？页面如何显示？我可以修改 towxml 库，以适配广告组件渲染，或者在自己的文章页面进行广告组件渲染。无论使用哪种方法，完整的数据流应该是这样的：Markdown 原始文件 -\u0026gt; Towxml 解析 -\u0026gt; 插入广告节点 -\u0026gt; 绑定广告数据 -\u0026gt; 渲染带广告的 WXML。剩下的内容就是想办法解决渲染广告组件这个问题了。\u003c/p\u003e","title":"在小程序文章页面动态添加广告"},{"content":"为了实现在页面中动态中插入广告，我们需要学会在小程序中查找元素的技能。例如，我要将广告显示在某某元素之后。\n在微信小程序开发中，由于逻辑层（JavaScript）与视图层（WXML）分离的特性，开发者无法直接操作 DOM 元素。为了获取页面中某个元素的属性（如位置、尺寸等），小程序提供了 wx.createSelectorQuery API，通过创建节点查询对象来间接操作元素。\nwx.createSelectorQuery 用于创建一个节点查询对象（SelectorQuery 实例），开发者可以通过该对象选择页面中的元素，并获取其布局信息（如宽高、位置等）。除了 wx.createSelectorQuery 外，还有一个 this.createSelectorQuery，他们之间的区别是一个用在页面中，一个用在组件组件中。\n当我们创建了查询对象后，我们需要选择元素，选择元素有以下几种：\nselect(selector)：选择匹配选择器的第一个节点。 selectAll(selector)：选择所有匹配的节点。 selectViewport()：选择视口（可用于获取滚动容器的信息）。 当元素匹配到之后，我们可以获取元素属性，获取节点属性的方法有：\nboundingClientRect()：获取节点的位置和尺寸。 scrollOffset()：获取节点的滚动位置（适用于滚动容器）。 fields({\u0026hellip;})：自定义需要获取的字段（如 id，dataset，rect 等）。 最后，一定要记得调用 exec(callback)，这样结果才能查询出来。\n下面我们举个小示例，可以方便地看到它如何使用？在这个示例中，我们将获取元素的尺寸和位置。场景如下：点击页面按钮，获取页面中某个 view 元素的宽度、高度和位置信息。\n1，WXML 文件如下：\n\u0026lt;view class=\u0026#34;target-box\u0026#34; id=\u0026#34;target\u0026#34;\u0026gt;我是目标元素\u0026lt;/view\u0026gt; \u0026lt;button bindtap=\u0026#34;getElementInfo\u0026#34;\u0026gt;获取元素信息\u0026lt;/button\u0026gt; 2，JS 文件如下：\nPage({ getElementInfo() { // 1. 创建查询实例 const query = wx.createSelectorQuery(); // 2. 选择元素并指定获取的属性 query.select(\u0026#39;#target\u0026#39;).boundingClientRect(); // 3. 执行查询 query.exec((res) =\u0026gt; { // res 是包含查询结果的数组 const rect = res[0]; console.log(\u0026#39;元素宽度:\u0026#39;, rect.width); console.log(\u0026#39;元素高度:\u0026#39;, rect.height); console.log(\u0026#39;元素位置:\u0026#39;, rect.left, rect.top); }); } }); 如果代码没有错误的话，控制台将输出类似如下内容：\n元素宽度: 200 元素高度: 100 元素位置: 15 30 需要注意的事项：\n1，生命周期时机：需要在元素渲染完成后（如 onReady 生命周期或者按钮点击事件）执行查询，否则可能获取不到节点。\n2，滚动容器查询，若元素位于滚动容器（如 scroll-view）内，需使用 in 方法指定容器实例：\nconst query = wx.createSelectorQuery().in(this); // 指定当前组件实例 query.select(\u0026#39;.scroll-item\u0026#39;).scrollOffset(); 3，异步执行，exec 是异步操作，结果通过回调返回，避免在回调外直接使用查询结果。\n","permalink":"https://blog.91demo.top/wiki/queryelement.html","summary":"\u003cp\u003e为了实现在页面中动态中插入广告，我们需要学会在小程序中查找元素的技能。例如，我要将广告显示在某某元素之后。\u003c/p\u003e\n\u003cp\u003e在微信小程序开发中，由于逻辑层（JavaScript）与视图层（WXML）分离的特性，开发者无法直接操作 DOM 元素。为了获取页面中某个元素的属性（如位置、尺寸等），小程序提供了 wx.createSelectorQuery API，通过创建节点查询对象来间接操作元素。\u003c/p\u003e\n\u003cp\u003ewx.createSelectorQuery 用于创建一个节点查询对象（SelectorQuery 实例），开发者可以通过该对象选择页面中的元素，并获取其布局信息（如宽高、位置等）。除了 wx.createSelectorQuery 外，还有一个 this.createSelectorQuery，他们之间的区别是一个用在页面中，一个用在组件组件中。\u003c/p\u003e\n\u003cp\u003e当我们创建了查询对象后，我们需要选择元素，选择元素有以下几种：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eselect(selector)：选择匹配选择器的第一个节点。\u003c/li\u003e\n\u003cli\u003eselectAll(selector)：选择所有匹配的节点。\u003c/li\u003e\n\u003cli\u003eselectViewport()：选择视口（可用于获取滚动容器的信息）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e当元素匹配到之后，我们可以获取元素属性，获取节点属性的方法有：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eboundingClientRect()：获取节点的位置和尺寸。\u003c/li\u003e\n\u003cli\u003escrollOffset()：获取节点的滚动位置（适用于滚动容器）。\u003c/li\u003e\n\u003cli\u003efields({\u0026hellip;})：自定义需要获取的字段（如 id，dataset，rect 等）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e最后，一定要记得调用 exec(callback)，这样结果才能查询出来。\u003c/p\u003e\n\u003cp\u003e下面我们举个小示例，可以方便地看到它如何使用？在这个示例中，我们将获取元素的尺寸和位置。场景如下：点击页面按钮，获取页面中某个 view 元素的宽度、高度和位置信息。\u003c/p\u003e\n\u003cp\u003e1，WXML 文件如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026lt;view class=\u0026#34;target-box\u0026#34; id=\u0026#34;target\u0026#34;\u0026gt;我是目标元素\u0026lt;/view\u0026gt;\n\u0026lt;button bindtap=\u0026#34;getElementInfo\u0026#34;\u0026gt;获取元素信息\u0026lt;/button\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e2，JS 文件如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ePage({\n  getElementInfo() {\n    // 1. 创建查询实例\n    const query = wx.createSelectorQuery();\n\n    // 2. 选择元素并指定获取的属性\n    query.select(\u0026#39;#target\u0026#39;).boundingClientRect();\n\n    // 3. 执行查询\n    query.exec((res) =\u0026gt; {\n      // res 是包含查询结果的数组\n      const rect = res[0];\n      console.log(\u0026#39;元素宽度:\u0026#39;, rect.width);\n      console.log(\u0026#39;元素高度:\u0026#39;, rect.height);\n      console.log(\u0026#39;元素位置:\u0026#39;, rect.left, rect.top);\n    });\n  }\n});\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如果代码没有错误的话，控制台将输出类似如下内容：\u003c/p\u003e","title":"在小程序页面查找元素"},{"content":"完成网站搭建后，下一步便是集成 Google Analytics (GA4) 以及优化搜索引擎收录。对于我来说，这不仅仅是技术配置，更是将以前域名的冗余资源清理掉，赋予这个站点一个干净、纯粹的起点。\n1. 集成 Google Analytics 统计分析 在 FixIt 主题中，集成统计分析需要配置 Hugo 的内置服务与主题插件。\n在 hugo.toml 文件中进行如下配置：\n# 1. 开启 Hugo 内置的 Google Analytics 服务 [services] [services.googleAnalytics] ID = \u0026#39;G-XXXXXXXXXX\u0026#39; # 替换为您在 GA4 获取的“衡量 ID” # 2. 配置 FixIt 主题的分析插件 [params] [params.analytics] enable = true type = \u0026#34;google\u0026#34; # 指定类型为 google [params.analytics.google] id = \u0026#34;G-XXXXXXXXXX\u0026#34; async = true # 开启异步加载，避免统计代码影响网页首屏加载速度 站长提示：配置完成后，建议在本地运行 hugo server 并使用 F12 检查网络请求，确认是否有来自 google-analytics.com 的数据上报。\n2. 提交 Sitemap 与清理旧资源 为了让 Google 更好地爬取新内容，并彻底告别过去，我们需要通过 Google Search Console 进行以下操作。\n第一步：提交站点地图 (Sitemap) Hugo 会在执行 hugo 命令后，自动在 public 目录下生成 sitemap.xml。\n登录 Google Search Console。 在左侧菜单选择 “站点地图” (Sitemaps)。 输入您的站点地图 URL，通常为：https://你的域名/sitemap.xml。 点击提交。 第二步：清理旧资源（重新开始） 如果您使用的是旧域名，且希望删除 Google 索引中过时的残留信息：\n在 Search Console 中点击 “删除” (Removals)。 点击 “新请求”，输入您想要清理的旧路径或旧站点 URL。 选择“暂时移除”，这将让 Google 在未来几个月内停止展示这些结果，随后通过持续抓取您的新 sitemap.xml 来完成索引替换。 3. 为什么做这些？ 作为一名 2026 年重新出发的站长，我追求的是更具“能量”的表达：\n数据洞察：通过 GA4 了解读者的技术偏好（Go、Rust 还是部署方案）。 SEO 净化：清理旧索引不仅是为了美观，更是为了让搜索引擎的权重能够集中在当下的优质内容上。 一切准备就绪，剩下的就是深耕内容，用代码和文字连接全球开发者。\n","permalink":"https://blog.91demo.top/wiki/googleanalytics.html","summary":"\u003cp\u003e完成网站搭建后，下一步便是集成 \u003cstrong\u003eGoogle Analytics (GA4)\u003c/strong\u003e 以及优化搜索引擎收录。对于我来说，这不仅仅是技术配置，更是将以前域名的冗余资源清理掉，赋予这个站点一个干净、纯粹的起点。\u003c/p\u003e\n\u003ch2 id=\"1-集成-google-analytics-统计分析\"\u003e1. 集成 Google Analytics 统计分析\u003c/h2\u003e\n\u003cp\u003e在 \u003cstrong\u003eFixIt\u003c/strong\u003e 主题中，集成统计分析需要配置 Hugo 的内置服务与主题插件。\u003c/p\u003e\n\u003cp\u003e在 \u003ccode\u003ehugo.toml\u003c/code\u003e 文件中进行如下配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-toml\" data-lang=\"toml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 1. 开启 Hugo 内置的 Google Analytics 服务\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[\u003cspan style=\"color:#a6e22e\"\u003eservices\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [\u003cspan style=\"color:#a6e22e\"\u003eservices\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003egoogleAnalytics\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eID\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;G-XXXXXXXXXX\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 替换为您在 GA4 获取的“衡量 ID”\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 2. 配置 FixIt 主题的分析插件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[\u003cspan style=\"color:#a6e22e\"\u003eparams\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [\u003cspan style=\"color:#a6e22e\"\u003eparams\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eanalytics\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eenable\u003c/span\u003e = \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003etype\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;google\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 指定类型为 google\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [\u003cspan style=\"color:#a6e22e\"\u003eparams\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eanalytics\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003egoogle\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003eid\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;G-XXXXXXXXXX\u0026#34;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#a6e22e\"\u003easync\u003c/span\u003e = \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 开启异步加载，避免统计代码影响网页首屏加载速度\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e站长提示\u003c/strong\u003e：配置完成后，建议在本地运行 hugo server 并使用 F12 检查网络请求，确认是否有来自 google-analytics.com 的数据上报。\u003c/p\u003e\n\u003ch2 id=\"2-提交-sitemap-与清理旧资源\"\u003e2. 提交 Sitemap 与清理旧资源\u003c/h2\u003e\n\u003cp\u003e为了让 Google 更好地爬取新内容，并彻底告别过去，我们需要通过 Google Search Console 进行以下操作。\u003c/p\u003e","title":"Hugo 进阶：集成 Google Analytics 4 与站点收录优化"},{"content":"在完成第一篇文章的发布后，趁热打铁记录下 FixIt 主题的深度定制备忘，重点解决内容组织架构与导航体验。\n1. 内容目录架构：从逻辑到物理组织 为了方便管理 9 年来的技术沉淀，我决定不依赖 Hugo 的扁平化展示，而是通过文件夹在物理层面进行分类，将其划分为 “项目实战” 与 “技术随笔” 两大板块。\n项目实战 (Projects) 在 content/projects/ 目录下，采用“一项目一文件夹”的结构。每个文件夹内放置 _index.md 标识该板块为一个 Section。\n路径示例：content/projects/mole-client/_index.md 配置内容： +++ title = \u0026#34;Mole 客户端 (frp管理)\u0026#34; layout = \u0026#34;section\u0026#34; draft = false +++ 注意：在父级 projects/ 下也要放置一个 _index.md 作为项目总列表。 技术随笔 (Posts) 日常记录存放于 content/posts/。不需要 _index.md，直接通过文章开头的 Front Matter 进行分类。\n示例 Front Matter： categories: [\u0026#34;deploy\u0026#34;] # 将分类设为 deploy，Hugo 会自动将其归档至对应分类页 2. 导航菜单配置：构建多级下拉菜单 在 hugo.toml 中，通过 identifier 和 parent 的配合，我为“技术笔记”板块设计了下拉菜单，使导航更具条理性。\n[menu] # 首页入口 [[menu.main]] identifier = \u0026#34;home\u0026#34; name = \u0026#34;首页\u0026#34; url = \u0026#34;/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-home fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 1 # --- 技术笔记下拉菜单 --- # 1. 父级入口：点击不跳转，仅用于展开子项 [[menu.main]] identifier = \u0026#34;notes\u0026#34; name = \u0026#34;技术笔记\u0026#34; url = \u0026#34;#\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-book fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 2 # 2. 子菜单：小程序 [[menu.main]] parent = \u0026#34;notes\u0026#34; identifier = \u0026#34;mp\u0026#34; name = \u0026#34;小程序\u0026#34; url = \u0026#34;/categories/mp/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-mobile-screen-button fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 1 # 3. 子菜单：Go 语言 [[menu.main]] parent = \u0026#34;notes\u0026#34; identifier = \u0026#34;go\u0026#34; name = \u0026#34;Go 语言\u0026#34; url = \u0026#34;/categories/go/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-brands fa-golang fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 2 # 4. 子菜单：Rust 语言 [[menu.main]] parent = \u0026#34;notes\u0026#34; identifier = \u0026#34;rust\u0026#34; name = \u0026#34;Rust 语言\u0026#34; url = \u0026#34;/categories/rust/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-brands fa-rust fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 3 # 5. 子菜单：部署运维 [[menu.main]] parent = \u0026#34;notes\u0026#34; identifier = \u0026#34;deploy\u0026#34; name = \u0026#34;部署实战\u0026#34; url = \u0026#34;/categories/deploy/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-cloud-arrow-up fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 4 # --- 其他一级菜单 --- [[menu.main]] identifier = \u0026#34;projects\u0026#34; name = \u0026#34;项目实战\u0026#34; url = \u0026#34;/projects/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-layer-group fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 10 [[menu.main]] identifier = \u0026#34;about\u0026#34; name = \u0026#34;关于我\u0026#34; url = \u0026#34;/about/\u0026#34; pre = \u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-user-circle fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39; weight = 20 3. 视觉识别：更换站点 Logo 作为“ 豆子技术站”的标志，我选择了 Ostrich 图标。\n资源准备：将 favicon.ico 和 ostrich.png 放置在项目的 static/ 根目录下。 配置文件修改：在 hugo.toml 中定位到页眉配置： [params] [params.header] [params.header.title] # 引用 static/ 目录下的图片地址 logo = \u0026#34;/ostrich.png\u0026#34; name = \u0026#34; 豆子技术站\u0026#34; 结语 通过以上配置，网站的骨架已经完全搭建完毕。Hugo 的强大之处在于，一旦你制定了规则（如目录结构和分类逻辑），剩下的工作就只需专注于内容的产出。接下来，我将按照这个既定的“航线”，持续填充更多关于 Go 与 Rust 的技术深度内容。\n","permalink":"https://blog.91demo.top/wiki/hugofixit.html","summary":"\u003cp\u003e在完成第一篇文章的发布后，趁热打铁记录下 FixIt 主题的深度定制备忘，重点解决内容组织架构与导航体验。\u003c/p\u003e\n\u003ch2 id=\"1-内容目录架构从逻辑到物理组织\"\u003e1. 内容目录架构：从逻辑到物理组织\u003c/h2\u003e\n\u003cp\u003e为了方便管理 9 年来的技术沉淀，我决定不依赖 Hugo 的扁平化展示，而是通过文件夹在物理层面进行分类，将其划分为 \u003cstrong\u003e“项目实战”\u003c/strong\u003e 与 \u003cstrong\u003e“技术随笔”\u003c/strong\u003e 两大板块。\u003c/p\u003e\n\u003ch3 id=\"项目实战-projects\"\u003e项目实战 (Projects)\u003c/h3\u003e\n\u003cp\u003e在 \u003ccode\u003econtent/projects/\u003c/code\u003e 目录下，采用“一项目一文件夹”的结构。每个文件夹内放置 \u003ccode\u003e_index.md\u003c/code\u003e 标识该板块为一个 Section。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e路径示例\u003c/strong\u003e：\u003ccode\u003econtent/projects/mole-client/_index.md\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e配置内容\u003c/strong\u003e：\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-toml\" data-lang=\"toml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e+++\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003etitle\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Mole 客户端 (frp管理)\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003elayout\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;section\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003edraft\u003c/span\u003e = \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e+++\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e注意\u003c/strong\u003e：在父级 \u003ccode\u003eprojects/\u003c/code\u003e 下也要放置一个 \u003ccode\u003e_index.md\u003c/code\u003e 作为项目总列表。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"技术随笔-posts\"\u003e技术随笔 (Posts)\u003c/h3\u003e\n\u003cp\u003e日常记录存放于 \u003ccode\u003econtent/posts/\u003c/code\u003e。不需要 \u003ccode\u003e_index.md\u003c/code\u003e，直接通过文章开头的 Front Matter 进行分类。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e示例 Front Matter\u003c/strong\u003e：\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003ecategories\u003c/span\u003e: [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;deploy\u0026#34;\u003c/span\u003e] \u003cspan style=\"color:#75715e\"\u003e# 将分类设为 deploy，Hugo 会自动将其归档至对应分类页\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-导航菜单配置构建多级下拉菜单\"\u003e2. 导航菜单配置：构建多级下拉菜单\u003c/h2\u003e\n\u003cp\u003e在 \u003ccode\u003ehugo.toml\u003c/code\u003e 中，通过 \u003ccode\u003eidentifier\u003c/code\u003e 和 \u003ccode\u003eparent\u003c/code\u003e 的配合，我为“技术笔记”板块设计了下拉菜单，使导航更具条理性。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-toml\" data-lang=\"toml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 首页入口\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;home\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;首页\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-home fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# --- 技术笔记下拉菜单 ---\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 1. 父级入口：点击不跳转，仅用于展开子项\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;notes\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;技术笔记\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;#\u0026#34;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-book fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 2. 子菜单：小程序\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eparent\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;notes\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mp\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;小程序\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/categories/mp/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-mobile-screen-button fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 3. 子菜单：Go 语言\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eparent\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;notes\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;go\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Go 语言\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/categories/go/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-brands fa-golang fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 4. 子菜单：Rust 语言\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eparent\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;notes\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;rust\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Rust 语言\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/categories/rust/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-brands fa-rust fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e3\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 5. 子菜单：部署运维\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eparent\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;notes\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;deploy\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;部署实战\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/categories/deploy/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-cloud-arrow-up fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# --- 其他一级菜单 ---\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;projects\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;项目实战\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/projects/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-layer-group fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  [[\u003cspan style=\"color:#a6e22e\"\u003emenu\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e]]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eidentifier\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;about\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;关于我\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eurl\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/about/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003epre\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026lt;i class=\u0026#34;fa-solid fa-user-circle fa-fw\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eweight\u003c/span\u003e = \u003cspan style=\"color:#ae81ff\"\u003e20\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"3-视觉识别更换站点-logo\"\u003e3. 视觉识别：更换站点 Logo\u003c/h2\u003e\n\u003cp\u003e作为“ 豆子技术站”的标志，我选择了 Ostrich 图标。\u003c/p\u003e","title":"Hugo 进阶：FixIt 主题深度配置（目录结构、多级菜单与 Logo）"},{"content":"今天“ 豆子技术站”正式上线了！虽然部署过程只用了几个小时，但中间踩了不少坑。为了让大家少走弯路，我把这段折腾经历记录下来。\n为什么选择 Hugo + GitHub + Cloudflare？ 我有自己的服务器，最初想用 mdBook。但 mdBook 适合书籍展示，格式太固定，不适合折腾。最终我选择了 Hugo —— 虽有一段时间没用，但基础还在。\n在主题选型上我花了不少时间，从花哨的 Blowfish 转向了更严谨的 FixIt。虽然我的重心正转向 Go 和 Rust，但 2026 年初的首要任务是搭建好这个支持中英双语的“根据地”。\n站长忠告：切忌心急。 很多弯路其实是因为没读懂 Cloudflare 的英文提示就直接上手导致的。\n避坑指南：我踩过的六个坑 1. 误判服务：Workers 还是 Pages？ 这是最尴尬的一个坑。我一直在 Cloudflare Workers 里折腾，结果页面永远显示 Hello World。AI 告诉我是配置文件没生效，我折腾半天无果，最后通过 Google 搜索才发现：我要部署的是静态站（Pages），而不是函数脚本（Workers）。换成 Pages 服务后，一遍过。\n2. DNS 托管的“心理障碍” Cloudflare 的免费计划要求将 DNS 托管给它。起初我很抗拒，因为我还有阿里云的子域名（如小程序后端 mp）在运行，担心影响原有业务。\n解决方法：在域名注册商后台将 DNS 服务器换成 Cloudflare 提供的地址。 关键点：激活后，记得把原来的解析记录在 Cloudflare 重新添加一遍。以后新增解析都在 Cloudflare 完成。 3. 页面样式“裸奔”：baseURL 没填对 部署完成后，网站能打开但没有样式（CSS 加载失败）。\n原因：F12 检查发现 URL 地址不对。 解决：在 hugo.toml 中将 baseURL 修改为你的自定义域名。 进阶：在 Cloudflare 构建命令中使用 hugo -b $CF_PAGES_URL，系统会自动处理域名。 4. CNAME 无法直接生效 我起初打算在原域名商处使用 CNAME，但死活打不开。事实证明，为了享受 Cloudflare 的全套防护和证书服务，DNS 托管是最高效的选择。\n5. Error 1001 报错 配置好 DNS 依然报 1001 错误。这是因为你只在 DNS 做了指向，没在 Pages 后台“对暗号”。\n解决：进入 Cloudflare Pages 项目 -\u0026gt; 自定义域 -\u0026gt; 设置自定义域。 6. 证书错误与重定向循环 刚切换 DNS 时，浏览器提示证书不安全。\n解决：边缘证书签发需要 10-30 分钟。同时，在 SSL/TLS 设置中，建议将模式从 “Flexible” 改为 “Full”，可以避免很多重定向导致的死循环。 经验总结 一、 样式无法打开？检查 baseURL 确保 hugo.toml 中的 baseURL 与你的最终访问域名一致。\n二、 托管 DNS 后的业务兼容 好处： 获得免费 SSL、DDoS 防护和全球加速。 方案： 对于不想经过 Cloudflare 代理的子域名（如后端 API），只需在 Cloudflare DNS 界面将对应的“橙色小云朵”点成灰色（仅限 DNS），流量就会直连原服务器，完全不影响原有业务。 三、 解决 1001 报错 必须在 Pages 仪表盘手动添加自定义域名，Cloudflare 才会识别并放行请求。\n四、 证书与安全模式 耐心： 等待证书签发完毕。 模式设置： 优先选择 “Full” 模式，确保链路加密配置一致。 “种一棵树最好的时间是十年前，其次是现在。”\n2026 年的第一天，我的新站终于在不断的“趟坑”中起航了。\n","permalink":"https://blog.91demo.top/wiki/cloudflare.html","summary":"\u003cp\u003e今天“ 豆子技术站”正式上线了！虽然部署过程只用了几个小时，但中间踩了不少坑。为了让大家少走弯路，我把这段折腾经历记录下来。\u003c/p\u003e\n\u003ch2 id=\"为什么选择-hugo--github--cloudflare\"\u003e为什么选择 Hugo + GitHub + Cloudflare？\u003c/h2\u003e\n\u003cp\u003e我有自己的服务器，最初想用 \u003ccode\u003emdBook\u003c/code\u003e。但 \u003ccode\u003emdBook\u003c/code\u003e 适合书籍展示，格式太固定，不适合折腾。最终我选择了 \u003cstrong\u003eHugo\u003c/strong\u003e —— 虽有一段时间没用，但基础还在。\u003c/p\u003e\n\u003cp\u003e在主题选型上我花了不少时间，从花哨的 \u003ccode\u003eBlowfish\u003c/code\u003e 转向了更严谨的 \u003cstrong\u003eFixIt\u003c/strong\u003e。虽然我的重心正转向 \u003cstrong\u003eGo\u003c/strong\u003e 和 \u003cstrong\u003eRust\u003c/strong\u003e，但 2026 年初的首要任务是搭建好这个支持中英双语的“根据地”。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e站长忠告：切忌心急。\u003c/strong\u003e 很多弯路其实是因为没读懂 Cloudflare 的英文提示就直接上手导致的。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"避坑指南我踩过的六个坑\"\u003e避坑指南：我踩过的六个坑\u003c/h2\u003e\n\u003ch3 id=\"1-误判服务workers-还是-pages\"\u003e1. 误判服务：Workers 还是 Pages？\u003c/h3\u003e\n\u003cp\u003e这是最尴尬的一个坑。我一直在 \u003cstrong\u003eCloudflare Workers\u003c/strong\u003e 里折腾，结果页面永远显示 \u003ccode\u003eHello World\u003c/code\u003e。AI 告诉我是配置文件没生效，我折腾半天无果，最后通过 Google 搜索才发现：\u003cstrong\u003e我要部署的是静态站（Pages），而不是函数脚本（Workers）\u003c/strong\u003e。换成 Pages 服务后，一遍过。\u003c/p\u003e\n\u003ch3 id=\"2-dns-托管的心理障碍\"\u003e2. DNS 托管的“心理障碍”\u003c/h3\u003e\n\u003cp\u003eCloudflare 的免费计划要求将 DNS 托管给它。起初我很抗拒，因为我还有阿里云的子域名（如小程序后端 \u003ccode\u003emp\u003c/code\u003e）在运行，担心影响原有业务。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e解决方法\u003c/strong\u003e：在域名注册商后台将 DNS 服务器换成 Cloudflare 提供的地址。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e关键点\u003c/strong\u003e：激活后，记得把原来的解析记录在 Cloudflare 重新添加一遍。以后新增解析都在 Cloudflare 完成。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-页面样式裸奔baseurl-没填对\"\u003e3. 页面样式“裸奔”：baseURL 没填对\u003c/h3\u003e\n\u003cp\u003e部署完成后，网站能打开但没有样式（CSS 加载失败）。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e原因\u003c/strong\u003e：F12 检查发现 URL 地址不对。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解决\u003c/strong\u003e：在 \u003ccode\u003ehugo.toml\u003c/code\u003e 中将 \u003ccode\u003ebaseURL\u003c/code\u003e 修改为你的自定义域名。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e进阶\u003c/strong\u003e：在 Cloudflare 构建命令中使用 \u003ccode\u003ehugo -b $CF_PAGES_URL\u003c/code\u003e，系统会自动处理域名。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"4-cname-无法直接生效\"\u003e4. CNAME 无法直接生效\u003c/h3\u003e\n\u003cp\u003e我起初打算在原域名商处使用 CNAME，但死活打不开。事实证明，为了享受 Cloudflare 的全套防护和证书服务，\u003cstrong\u003eDNS 托管\u003c/strong\u003e是最高效的选择。\u003c/p\u003e","title":"从零到一：Hugo 项目部署 Cloudflare Pages 避坑指南"},{"content":"Eagle “种一棵树最好的时间是十年前，其次是现在。”\n欢迎来到我的技术领地 我是一名拥有多年经验的资深开发者，职业深耕Go语言，业余钟情 Rust、小程序 及 嵌入式 探索。\n在 2026 年之前，我的技术重心主要围绕微信小程序生态展开，积累了宝贵的实践经验。迈入 2026 年，我正战略性地拓展技术边界，回归更广阔、更开放的 Web 技术栈视野。未来，我的核心精力将投入到 Go 和 Rust 等底层技术的深度沉淀，旨在构建更稳健的基础设施与服务；而小程序则会作为前端生态中的一个高效节点，继续在我整体的技术版图中发挥其独特的连接作用。\n关于本站：我的技术演进与思考 本站基于 Hugo 构建。在此之前，我的技术输出主要集中在小程序和公众号，但随着思考的深入，我发现了各自的局限：\n小程序：虽然便捷，但在渲染复杂的 Markdown 代码片段时，性能瓶颈导致的卡顿往往会打断阅读的连贯性。 公众号：它更像是一个资讯窗口，并不适合大篇幅、深层次的代码演示与技术推演。 于是，我重新梳理了我的技术版图：\n小程序：定位为线上线下的连接工具，发挥其极致的触达效率。 公众号：作为宣传与引流的领地，负责捕捉灵感与传播声音。 本站 (Hugo)：这是我真正的思考领域。在这里，我会详细记录对技术的深度钻研、对复杂问题的复盘分析，以及那些更偏向底层、更硬核的技术研究。 在这里，文字不再受限于载体，只服务于逻辑与思想。\n实战作品集 (Mini-Programs) 这是我将技术想法落地的两个核心项目，也是我多年开发经验的缩影。\n豆子碎片 —— 实战模块代码集市 一句话简介：我多年的技术积累，包含了我在 Go、Rust、小程序及 Web 领域的全部实战项目代码。\n直观功能：\n项目全览：首页列表式展示我做过的所有成熟模组与完整项目。 深度阅读：点击即进入项目模块详情，完整的 Markdown 文档说明。 源码获取：支持直接下载源码。无论你是想参考架构还是寻找现成的解决方案，这里都能直接拿走。 体验入口：\n豆子工具 —— 我的微信口袋助手 一句话简介：基于微信账号体系开发的“全能口袋工具”，让我脱离电脑也能在手机上处理专业任务。\n直观功能：\n便捷转换：调用底层 API 快速实现音频、图片的格式转换。 安全鉴权：直接关联微信登录，内置私密密码本，确保敏感数据随身且安全。 随时随地：方便在移动端操作的实用技术小工具，弥补PC客户端的不足。 体验入口：\n欢迎交流与连接 如果你对 Go/Rust 底层开发、小程序架构或嵌入式方案感兴趣，欢迎通过以下方式与我联系：\nGitHub: @littletow —— 这里记录着我的代码足迹与开源实践。 Email: longfengfirst (at) gmail [dot] com —— 深度探讨或项目交流请致信于此。 公众号: 搜索 “技术源泉” —— 获取最新的技术动态与灵感推送。 基础设施 (Infrastructure) 本站坚持高可用与极速访问体验，目前稳定运行于：\n腾讯云： 领券新购特惠 阿里云： 云服务器精选特惠 版权与致谢 (Attributions) 本站的建设遵循开放与尊重的原则，特此说明站内使用的授权素材：\n视觉与图标(View \u0026amp; Icon) 站点 Logo (Ostrich): 由 Freepik 创作，来源于 Flaticon，遵循 Free License 署名授权。 技术栈标识: 图标: 部分图标基于 Font Awesome 免费版。图标遵循 CC BY 4.0 License，字体文件遵循 SIL OFL 1.1 License。 图标: 部分图标引用自 Iconfont 开源库。 站长注：如果您发现本站有任何引用不当或侵犯版权的内容，请及时联系我，我将第一时间处理。\n","permalink":"https://blog.91demo.top/about.html","summary":"\u003ch1 id=\"eagle\"\u003eEagle\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e“种一棵树最好的时间是十年前，其次是现在。”\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"欢迎来到我的技术领地\"\u003e欢迎来到我的技术领地\u003c/h2\u003e\n\u003cp\u003e我是一名拥有多年经验的资深开发者，职业深耕\u003cstrong\u003eGo\u003c/strong\u003e语言，业余钟情 \u003cstrong\u003eRust\u003c/strong\u003e、\u003cstrong\u003e小程序\u003c/strong\u003e 及 \u003cstrong\u003e嵌入式\u003c/strong\u003e 探索。\u003c/p\u003e\n\u003cp\u003e在 2026 年之前，我的技术重心主要围绕微信小程序生态展开，积累了宝贵的实践经验。迈入 2026 年，我正战略性地拓展技术边界，回归更广阔、更开放的 Web 技术栈视野。未来，我的核心精力将投入到 Go 和 Rust 等底层技术的深度沉淀，旨在构建更稳健的基础设施与服务；而小程序则会作为前端生态中的一个高效节点，继续在我整体的技术版图中发挥其独特的连接作用。\u003c/p\u003e\n\u003ch2 id=\"关于本站我的技术演进与思考\"\u003e关于本站：我的技术演进与思考\u003c/h2\u003e\n\u003cp\u003e本站基于 Hugo 构建。在此之前，我的技术输出主要集中在小程序和公众号，但随着思考的深入，我发现了各自的局限：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e小程序：虽然便捷，但在渲染复杂的 Markdown 代码片段时，性能瓶颈导致的卡顿往往会打断阅读的连贯性。\u003c/li\u003e\n\u003cli\u003e公众号：它更像是一个资讯窗口，并不适合大篇幅、深层次的代码演示与技术推演。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e于是，我重新梳理了我的技术版图：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e小程序：定位为线上线下的连接工具，发挥其极致的触达效率。\u003c/li\u003e\n\u003cli\u003e公众号：作为宣传与引流的领地，负责捕捉灵感与传播声音。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e本站\u003c/strong\u003e (Hugo)：这是我真正的\u003cstrong\u003e思考领域\u003c/strong\u003e。在这里，我会详细记录对技术的深度钻研、对复杂问题的复盘分析，以及那些更偏向底层、更硬核的技术研究。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在这里，文字不再受限于载体，只服务于逻辑与思想。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"实战作品集-mini-programs\"\u003e实战作品集 (Mini-Programs)\u003c/h2\u003e\n\u003cp\u003e这是我将技术想法落地的两个核心项目，也是我多年开发经验的缩影。\u003c/p\u003e\n\u003ch3 id=\"豆子碎片--实战模块代码集市\"\u003e豆子碎片 —— 实战模块代码集市\u003c/h3\u003e\n\u003cp\u003e一句话简介：我多年的技术积累，包含了我在 Go、Rust、小程序及 Web 领域的全部实战项目代码。\u003c/p\u003e\n\u003cp\u003e直观功能：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e项目全览：首页列表式展示我做过的所有成熟模组与完整项目。\u003c/li\u003e\n\u003cli\u003e深度阅读：点击即进入项目模块详情，完整的 Markdown 文档说明。\u003c/li\u003e\n\u003cli\u003e源码获取：支持直接下载源码。无论你是想参考架构还是寻找现成的解决方案，这里都能直接拿走。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e体验入口：\u003cbr\u003e\n\u003cimg src=\"/images/visit.webp\" width=\"200\" alt=\"豆子碎片小程序\"\u003e\u003c/p\u003e\n\u003ch3 id=\"豆子工具--我的微信口袋助手\"\u003e豆子工具 —— 我的微信口袋助手\u003c/h3\u003e\n\u003cp\u003e一句话简介：基于微信账号体系开发的“全能口袋工具”，让我脱离电脑也能在手机上处理专业任务。\u003c/p\u003e\n\u003cp\u003e直观功能：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e便捷转换：调用底层 API 快速实现音频、图片的格式转换。\u003c/li\u003e\n\u003cli\u003e安全鉴权：直接关联微信登录，内置私密密码本，确保敏感数据随身且安全。\u003c/li\u003e\n\u003cli\u003e随时随地：方便在移动端操作的实用技术小工具，弥补PC客户端的不足。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e体验入口：\u003cbr\u003e\n\u003cimg src=\"/images/wander.webp\" width=\"200\" alt=\"豆子工具小程序\"\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"欢迎交流与连接\"\u003e欢迎交流与连接\u003c/h2\u003e\n\u003cp\u003e如果你对 Go/Rust 底层开发、小程序架构或嵌入式方案感兴趣，欢迎通过以下方式与我联系：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGitHub: @littletow —— 这里记录着我的代码足迹与开源实践。\u003c/li\u003e\n\u003cli\u003eEmail: \u003ccode\u003elongfengfirst (at) gmail [dot] com\u003c/code\u003e —— 深度探讨或项目交流请致信于此。\u003c/li\u003e\n\u003cli\u003e公众号: 搜索 “技术源泉” —— 获取最新的技术动态与灵感推送。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"基础设施-infrastructure\"\u003e基础设施 (Infrastructure)\u003c/h2\u003e\n\u003cp\u003e本站坚持高可用与极速访问体验，目前稳定运行于：\u003c/p\u003e","title":"关于我 (About Me)"},{"content":"隐私政策 (Privacy Policy) 本隐私政策详细说明了 豆子技术站（以下简称“本站”）如何收集、使用和保护您的个人信息。\n1. 信息收集 本站本身不强制要求用户注册，也不会主动收集您的个人敏感信息。但我们会通过第三方服务（如 Google Analytics）记录基础的匿名访问数据（如 IP 地址、浏览器类型、停留时间等），用于优化内容体验。\n2. Google AdSense 与 Cookie 本站使用 Google AdSense 投放广告。\nGoogle 作为第三方广告供应商，使用 Cookie 根据用户对此网站及互联网上其他网站的访问情况来投放广告。 Google 对广告 Cookie 的使用（包括 DoubleClick Cookie）使 Google 及其合作伙伴能够根据用户对本站和/或互联网上其他网站的访问情况向用户投放广告。 您可以访问 Google 广告设置 来停用个性化广告。 3. 第三方链接 本站包含指向其他网站（如 GitHub、外部技术文档等）的链接。我们不对这些第三方网站的隐私做法负责，建议您查阅其各自的隐私政策。\n4. 政策更新 我们可能会不时更新本隐私政策。建议您定期查看此页面以了解任何更改。\n联系方式：如果您对本政策有任何疑问，请通过 关于我 页面中的方式联系。\n","permalink":"https://blog.91demo.top/privacy.html","summary":"\u003ch2 id=\"隐私政策-privacy-policy\"\u003e隐私政策 (Privacy Policy)\u003c/h2\u003e\n\u003cp\u003e本隐私政策详细说明了 \u003cstrong\u003e豆子技术站\u003c/strong\u003e（以下简称“本站”）如何收集、使用和保护您的个人信息。\u003c/p\u003e\n\u003ch3 id=\"1-信息收集\"\u003e1. 信息收集\u003c/h3\u003e\n\u003cp\u003e本站本身不强制要求用户注册，也不会主动收集您的个人敏感信息。但我们会通过第三方服务（如 Google Analytics）记录基础的匿名访问数据（如 IP 地址、浏览器类型、停留时间等），用于优化内容体验。\u003c/p\u003e\n\u003ch3 id=\"2-google-adsense-与-cookie\"\u003e2. Google AdSense 与 Cookie\u003c/h3\u003e\n\u003cp\u003e本站使用 \u003cstrong\u003eGoogle AdSense\u003c/strong\u003e 投放广告。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGoogle 作为第三方广告供应商，使用 Cookie 根据用户对此网站及互联网上其他网站的访问情况来投放广告。\u003c/li\u003e\n\u003cli\u003eGoogle 对广告 Cookie 的使用（包括 \u003cstrong\u003eDoubleClick Cookie\u003c/strong\u003e）使 Google 及其合作伙伴能够根据用户对本站和/或互联网上其他网站的访问情况向用户投放广告。\u003c/li\u003e\n\u003cli\u003e您可以访问 \u003ca href=\"https://www.google.com\"\u003eGoogle 广告设置\u003c/a\u003e 来停用个性化广告。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-第三方链接\"\u003e3. 第三方链接\u003c/h3\u003e\n\u003cp\u003e本站包含指向其他网站（如 GitHub、外部技术文档等）的链接。我们不对这些第三方网站的隐私做法负责，建议您查阅其各自的隐私政策。\u003c/p\u003e\n\u003ch3 id=\"4-政策更新\"\u003e4. 政策更新\u003c/h3\u003e\n\u003cp\u003e我们可能会不时更新本隐私政策。建议您定期查看此页面以了解任何更改。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003e联系方式\u003c/strong\u003e：如果您对本政策有任何疑问，请通过 \u003ca href=\"/about.html\"\u003e关于我\u003c/a\u003e 页面中的方式联系。\u003c/p\u003e","title":"隐私政策 (Privacy Policy)"},{"content":"随着“小程序激励演示版”的完成，我不仅验证了 Wails 3 的可靠性，也理清了内网穿透在实际应用中的边界。\n今天，我决定开启 Mole 客户端的新阶段：开发一个完全去商业化的“公版”frp 管理工具。 这一次，不再有广告，不再有限制，只有极致的配置体验和对 frp 协议的深度支持。\n一、 减法：去掉枷锁，回归纯粹 在之前的演示版中，为了跑通“看广告换带宽”的逻辑，我给工具加了很多业务代码。在新的公版计划中，我做的第一件事就是**“大扫除”**：\n移除激励系统：彻底删掉小程序码弹窗、后端握手逻辑和广告状态监听。 轻量化内核： UI 界面将变得更加清爽，让用户专注于配置本身。 零服务依赖：不再强制连接我的私有服务器，用户可以自由添加任何公网 frps 节点。 二、 加法：全协议支持与配置进化 frp 的强大之处在于其灵活的协议转发。之前的版本为了简化演示只支持 HTTP，但在公版中，我将补齐这些核心拼图：\n1. 从 HTTP 到 TCP/UDP 开发者不仅需要穿透网页，更多时候需要穿透 SSH (22)、数据库 (3306) 甚至 游戏服务器 (UDP)。\n新特性：配置页面将新增协议切换选项，动态支持 TCP 和 UDP 端口映射。 交互优化：针对不同协议，UI 会自动切换配置项（如 HTTP 需要填写域名，而 TCP 只需填端口）。 2. HTTPS 的优雅实现 虽然 frpc 本身可以处理 SSL，但主流做法依然是在服务器端使用 Nginx/Caddy 进行反向代理。\n方案升级：Mole 将内置一套“最佳实践文档”。在用户配置 HTTPS 映射时，客户端会引导用户如何在服务端配置 Nginx，并给出生成的示例配置代码。 3. 配置文件的“可视化”与“透明化” 公版将允许用户直接查看生成的 frpc.toml。\n你可以在 UI 界面通过表单操作，也可以一键切换到代码模式手动微调。这种“双模运行”能同时满足新手和老鸟的需求。 三、 稳定性：我自己的“日用级”保障 在开发演示版的这段时间里，Mole 实际上已经成为了我工作流程的一部分。\nWindows 端的稳定性：经过连续数周的每日测试，Wails 3 + Go 在处理进程守护、托盘运行方面表现极其稳健。 跨平台承诺：接下来，我将重点打磨 macOS 和 Linux 端的细节，确保在不同系统下，隐藏黑窗口、系统托盘等体验能保持一致。 四、 为什么我要坚持做公版？ 有人问我，既然没有收益了，为什么还要继续？\n因为我自己需要，而且我相信很多人也需要。 命令行虽好，但一个能静默在托盘、一键启停、直观查看各隧道流量和日志的 GUI 工具，能实实在在地提升开发效率。\n通过这个项目，我积累的不仅是代码，还有对 Wails 3 生态的深度理解。后续，我会将公版的功能逐步完善，并同步更新开发笔记。\n下一阶段重点：\n多配置 profiles 切换逻辑实现。 TCP/UDP 动态表单组件开发。 frpc 各版本二进制文件的自动下载与管理。 Stay tuned, 真正的工具才刚刚开始。\n","permalink":"https://blog.91demo.top/wiki/tool-intro.html","summary":"\u003cp\u003e随着“小程序激励演示版”的完成，我不仅验证了 Wails 3 的可靠性，也理清了内网穿透在实际应用中的边界。\u003c/p\u003e\n\u003cp\u003e今天，我决定开启 Mole 客户端的新阶段：\u003cstrong\u003e开发一个完全去商业化的“公版”frp 管理工具。\u003c/strong\u003e 这一次，不再有广告，不再有限制，只有极致的配置体验和对 frp 协议的深度支持。\u003c/p\u003e\n\u003ch2 id=\"一-减法去掉枷锁回归纯粹\"\u003e一、 减法：去掉枷锁，回归纯粹\u003c/h2\u003e\n\u003cp\u003e在之前的演示版中，为了跑通“看广告换带宽”的逻辑，我给工具加了很多业务代码。在新的公版计划中，我做的第一件事就是**“大扫除”**：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e移除激励系统\u003c/strong\u003e：彻底删掉小程序码弹窗、后端握手逻辑和广告状态监听。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e轻量化内核\u003c/strong\u003e： UI 界面将变得更加清爽，让用户专注于配置本身。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e零服务依赖\u003c/strong\u003e：不再强制连接我的私有服务器，用户可以自由添加任何公网 frps 节点。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二-加法全协议支持与配置进化\"\u003e二、 加法：全协议支持与配置进化\u003c/h2\u003e\n\u003cp\u003efrp 的强大之处在于其灵活的协议转发。之前的版本为了简化演示只支持 HTTP，但在公版中，我将补齐这些核心拼图：\u003c/p\u003e\n\u003ch3 id=\"1-从-http-到-tcpudp\"\u003e1. 从 HTTP 到 TCP/UDP\u003c/h3\u003e\n\u003cp\u003e开发者不仅需要穿透网页，更多时候需要穿透 \u003cstrong\u003eSSH (22)\u003c/strong\u003e、\u003cstrong\u003e数据库 (3306)\u003c/strong\u003e 甚至 \u003cstrong\u003e游戏服务器 (UDP)\u003c/strong\u003e。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e新特性\u003c/strong\u003e：配置页面将新增协议切换选项，动态支持 TCP 和 UDP 端口映射。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e交互优化\u003c/strong\u003e：针对不同协议，UI 会自动切换配置项（如 HTTP 需要填写域名，而 TCP 只需填端口）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-https-的优雅实现\"\u003e2. HTTPS 的优雅实现\u003c/h3\u003e\n\u003cp\u003e虽然 frpc 本身可以处理 SSL，但主流做法依然是在服务器端使用 Nginx/Caddy 进行反向代理。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e方案升级\u003c/strong\u003e：Mole 将内置一套“最佳实践文档”。在用户配置 HTTPS 映射时，客户端会引导用户如何在服务端配置 Nginx，并给出生成的示例配置代码。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-配置文件的可视化与透明化\"\u003e3. 配置文件的“可视化”与“透明化”\u003c/h3\u003e\n\u003cp\u003e公版将允许用户直接查看生成的 \u003ccode\u003efrpc.toml\u003c/code\u003e。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e你可以在 UI 界面通过表单操作，也可以一键切换到代码模式手动微调。这种“双模运行”能同时满足新手和老鸟的需求。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三-稳定性我自己的日用级保障\"\u003e三、 稳定性：我自己的“日用级”保障\u003c/h2\u003e\n\u003cp\u003e在开发演示版的这段时间里，Mole 实际上已经成为了我工作流程的一部分。\u003c/p\u003e","title":"回归初心：从演示版到公版，打造最纯粹的 frp 桌面管理工具"},{"content":"我的小程序“豆子碎片”，走到了第七个版本，也走到了它作为“不断迭代的产品”的终点。\n这一次，没有新的功能，没有架构调整，没有交互革新。在经历了从平台梦碎到零成本重生，从游戏化狂欢到工程化精修的一系列漫长探索后，我停下追逐“下一个版本”的脚步，进行了一次彻底的回顾。\n我决定，赋予它一个最平静、也最庄重的最终形态：一座以“时间轴”为展线的个人数字博物馆，小程序的个人作品集。\n为何是时间轴？\n因为它的价值，早已不在任何一个单独版本的功能里，而在于其完整的演进史诗本身。每一次看似推翻重来的“改版”，都不是失败，而是一次认知的跃进与边界的探索。它们共同构成了一部活生生的、关于一个开发者如何思考、试错、学习与成长的历程。\n这座博物馆的首页，就是一条清晰的时间轴。上面标记着七个重要的坐标：\nV1.0 起点：标记为“技术自留地”。那是一个朴素数字笔记本的剪影，代表着一切开始的初心——纯粹地记录。\nV2.0 膨胀：标记为“平台梦”。这里陈列着“创作者API”、“经济系统”等复杂的架构图碎片，象征着一次勇敢但脱离现实的雄心。\nV3.0 重生：标记为“Git驱动”。一个极简的、指向Git仓库的图标，记录着在服务器到期后的绝地求生，以及“零成本可持续”的技术洞察。\nV4.0 堡垒：标记为“自主枢纽”。一座微小的Nginx服务器模型，象征着从依赖走向可控，是稳定性与自主权的基石。\nV5.0 蜕变：标记为“知识闯关”。一个九宫格地图和动态表单的交互模型，代表着从“展示”到“游戏”的哲学转变，是最富创造力的一跃。\nV6.0 精修：标记为“稳健产品”。这里展示着规范的数据结构图纸和荣誉徽章，代表着从创意原型到可维护产品的工程化沉淀。\nV7.0 博物馆：它自己，就是最后一个展品。这个“时间轴”本身，便是终极的答案。\n博物馆的意义\n这个最终版本，不是开发的中止，而是价值的升华。它将“豆子碎片”从一个需要我不断维护的“项目”，转变为一个可以静静展示的“作品”。\n对自己，是完整的仪式：它给了我一个清晰的句点。顺着时间轴回溯，我能看到每一步选择的因果，看到技术热情如何与产品认知相互碰撞、塑造。这是一种宝贵的自我确认，做事有头有尾。\n对观者，是诚实的叙事：任何访客打开它，不再需要面对一个不知所谓的孤立功能。他们可以通过这条时间轴，在几分钟内读懂一个想法长达数年的生命旅程。它展示的不仅是结果，更是过程；不仅是成功，更是充满教训的尝试。这份诚实，比任何华丽的功能都更有力量。\n对内核，是信心的转移：我停止更新它，是因为我确信，真正的价值创造已转移至新的阵地——对知识体系本身的深度构建。这座博物馆的存在，恰恰解放了我，让我能心无旁骛地回归博客，去打造那个真正需要时间沉淀的、坚实的内核。\n“豆子碎片”的最终版，不再是一个工具，而是一本书的目录，一部纪录片的索引，或是一棵大树的年轮。\n它用一条时间轴，将散落的探索珍珠串成完整的项链，安静地陈列在数字世界的角落。它完成了自己的历史使命，从一个不断寻求下一站的行者，变成了一座记载着整个旅途风景的纪念碑。\n从此，版本号定格于 V7.0，而创造者的旅程，将在新的篇章里继续。这座时间轴博物馆，便是这段旧日史诗，最恰当的丰碑与终点。\n技术最终的本质还是知识的内涵和深度，不在于载体。\n","permalink":"https://blog.91demo.top/wiki/v7.html","summary":"\u003cp\u003e我的小程序“豆子碎片”，走到了第七个版本，也走到了它作为“不断迭代的产品”的终点。\u003c/p\u003e\n\u003cp\u003e这一次，没有新的功能，没有架构调整，没有交互革新。在经历了从平台梦碎到零成本重生，从游戏化狂欢到工程化精修的一系列漫长探索后，我停下追逐“下一个版本”的脚步，进行了一次彻底的回顾。\u003c/p\u003e\n\u003cp\u003e我决定，赋予它一个最平静、也最庄重的最终形态：一座以“时间轴”为展线的个人数字博物馆，小程序的个人作品集。\u003c/p\u003e\n\u003cp\u003e为何是时间轴？\u003c/p\u003e\n\u003cp\u003e因为它的价值，早已不在任何一个单独版本的功能里，而在于其完整的演进史诗本身。每一次看似推翻重来的“改版”，都不是失败，而是一次认知的跃进与边界的探索。它们共同构成了一部活生生的、关于一个开发者如何思考、试错、学习与成长的历程。\u003c/p\u003e\n\u003cp\u003e这座博物馆的首页，就是一条清晰的时间轴。上面标记着七个重要的坐标：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eV1.0 起点：标记为“技术自留地”。那是一个朴素数字笔记本的剪影，代表着一切开始的初心——纯粹地记录。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eV2.0 膨胀：标记为“平台梦”。这里陈列着“创作者API”、“经济系统”等复杂的架构图碎片，象征着一次勇敢但脱离现实的雄心。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eV3.0 重生：标记为“Git驱动”。一个极简的、指向Git仓库的图标，记录着在服务器到期后的绝地求生，以及“零成本可持续”的技术洞察。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eV4.0 堡垒：标记为“自主枢纽”。一座微小的Nginx服务器模型，象征着从依赖走向可控，是稳定性与自主权的基石。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eV5.0 蜕变：标记为“知识闯关”。一个九宫格地图和动态表单的交互模型，代表着从“展示”到“游戏”的哲学转变，是最富创造力的一跃。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eV6.0 精修：标记为“稳健产品”。这里展示着规范的数据结构图纸和荣誉徽章，代表着从创意原型到可维护产品的工程化沉淀。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eV7.0 博物馆：它自己，就是最后一个展品。这个“时间轴”本身，便是终极的答案。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e博物馆的意义\u003c/p\u003e\n\u003cp\u003e这个最终版本，不是开发的中止，而是价值的升华。它将“豆子碎片”从一个需要我不断维护的“项目”，转变为一个可以静静展示的“作品”。\u003c/p\u003e\n\u003cp\u003e对自己，是完整的仪式：它给了我一个清晰的句点。顺着时间轴回溯，我能看到每一步选择的因果，看到技术热情如何与产品认知相互碰撞、塑造。这是一种宝贵的自我确认，做事有头有尾。\u003c/p\u003e\n\u003cp\u003e对观者，是诚实的叙事：任何访客打开它，不再需要面对一个不知所谓的孤立功能。他们可以通过这条时间轴，在几分钟内读懂一个想法长达数年的生命旅程。它展示的不仅是结果，更是过程；不仅是成功，更是充满教训的尝试。这份诚实，比任何华丽的功能都更有力量。\u003c/p\u003e\n\u003cp\u003e对内核，是信心的转移：我停止更新它，是因为我确信，真正的价值创造已转移至新的阵地——对知识体系本身的深度构建。这座博物馆的存在，恰恰解放了我，让我能心无旁骛地回归博客，去打造那个真正需要时间沉淀的、坚实的内核。\u003c/p\u003e\n\u003cp\u003e“豆子碎片”的最终版，不再是一个工具，而是一本书的目录，一部纪录片的索引，或是一棵大树的年轮。\u003c/p\u003e\n\u003cp\u003e它用一条时间轴，将散落的探索珍珠串成完整的项链，安静地陈列在数字世界的角落。它完成了自己的历史使命，从一个不断寻求下一站的行者，变成了一座记载着整个旅途风景的纪念碑。\u003c/p\u003e\n\u003cp\u003e从此，版本号定格于 V7.0，而创造者的旅程，将在新的篇章里继续。这座时间轴博物馆，便是这段旧日史诗，最恰当的丰碑与终点。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e技术最终的本质还是知识的内涵和深度，不在于载体。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"豆子碎片小程序项目第七版"},{"content":"FRP Manager 是一款基于 Wails 3 开发的跨平台 frp 管理客户端。它不仅解决了 frp 配置繁琐的痛点，更探索出了一种“桌面工具 + 小程序生态”的全新闭环模式。\n🌟 核心业务流程 Mole 的核心逻辑在于其自动化配置流与激励机制的融合：\n1. 智能激励连接 (One-Click Connect) 当用户点击“一键连接”时，客户端会启动一套自动验证逻辑：\n广告触发：系统检查用户当前状态。若未满足条件，则弹出动态窗口显示专属小程序码。 多端协同：用户扫码后进入微信小程序观看激励视频。 自动握手：用户观看完毕后，小程序通过后端指令反馈给 Mole 客户端。 静默取消：客户端接收指令后自动关闭弹窗，进入连接阶段。 2. 云端配置自动下发 Mole 不再需要用户手动编辑复杂的 TOML 文件：\n动态获取：从服务器端安全获取分配的随机子域名和 frp 配置 Token。 本地注入：Go 后端自动将配置写入内置的 frpc.toml。 二进制调用：自动调用内嵌的 frpc 核心组件启动服务。 3. 全链路事件反馈 利用 Wails 3 的高效事件机制，实现深度的 UI 反馈：\n实时日志：Go 后端捕获 frpc 的标准输出，通过事件流（Events）实时推送到前端页面，用户可以清晰看到连接建立过程。 状态监控：实时反馈隧道运行状态，确保连接稳定性。 🛠️ 板块功能详情 连接控制台：一键启停，集成小程序激励流程。 端口配置页：灵活配置本地服务端口（如 8080, 3000 等），满足开发者调试需求。 实时日志页：直观展示 frp 运行日志，方便故障排查。 帮助中心：详尽的操作指南，降低内网穿透的使用门槛。 技术结缘 (About \u0026amp; Support)： 集成优质云服务器推广（为用户提供可靠的 frp 服务端选择）。 扫码直达开发笔记，分享 Wails 3 与 Go 的底层实战。 👀 软件界面概览 1. 软件主界面 2. 小程序激励连接 3. 控制台 4. 配置 5. 日志 🏗️ 技术选型背后的思考 Wails 3: 相比 Electron，我更看重其极小的包体积和对 Go 原生性能的直接调用。 Go 驱动: 处理文件 I/O、进程管理及网络协议栈时，Go 表现出了惊人的开发效率。 小程序闭环: 避开了复杂的桌面支付系统，利用微信成熟的广告生态实现工具的良性循环。 下面我将讲讲它的来龙去脉。\n一、困境与契机：低配服务器、高性能需求与共享经济的思考 我的云服务器配置很“低”：1 核 CPU、1G 内存、1M 带宽。它完美地满足了我个人博客的需求，直到我需要演示一套视频会议系统。该系统最低要求 2 核 4G。\n我的服务器是包年做活动时购买，升级配置成本高昂，而我的本地电脑性能过剩，为了一次演示，我升级我的云服务器，我感觉不值。为了解决这个问题，frp（Fast Reverse Proxy） 成为了我的救星，它允许我将本地高性能服务安全地映射到公网。\n在成功通过命令行 frpc 实现了内网穿透演示后，我发现了一系列生产力问题：\n进程管理难题： 命令行窗口一旦关闭，frpc 进程随之终止，服务中断。这在日常使用中非常不便，因为会经常手误关闭窗口。 潜在的商业价值： 我有闲置资源服务器，公网 IP 以及域名。对于那些临时需要公网映射的用户来说，这是一个刚需。 流量变现与服务化： 我有一个小程序并集成了广告。如果能让用户通过看广告来获取临时的 frps 使用权（子域名分配），可以减少我的服务器支出，这将是一个双赢的模式。 为了解决这些问题并实现我的想法，我意识到可以做一个稳定、可后台运行、拥有友好 UI 的客户端，来替代原始的命令行操作。\n二、技术选型坎坷路：Fyne、Tauri 的尝试与碰壁 我的技术栈主要集中在 Go、Rust 和 JavaScript，其中 Go 最擅长。我希望使用这些技术栈实现一个跨平台的客户端。\n第一次尝试：Go \u0026amp; Fyne。Fyne 是一款使用 Go 语言编写的跨平台 GUI 库。我首先尝试了它。\n优点： 纯 Go 编写，上手快。 痛点： 在处理 frpc 实时打印的日志时，Fyne 对中文字符的支持不理想；复杂的列表和表格布局实现起来很痛苦。最终放弃。 第二次尝试：Rust \u0026amp; Tauri。Tauri 是当时（也是现在）非常热门的跨平台框架，结合 Rust 的后端性能和 Web 前端技术。\n优点： 性能强劲，打包体积小，前端界面开发体验好。 痛点： 我完成了界面设计，但在核心的 frp 控制模块上遇到了巨大的技术难题。Rust 的所有权（Ownership） 和生命周期管理让我头疼不已，始终无法优雅地实现 frp 进程的启动、停止和状态共享。项目卡壳。 三、柳暗花明：Wails v3 成为“天选之子” 在技术选型陷入僵局时，有读者告诉我可以尝试下 wails 3，我关注了 Wails 项目的 v3 版本。虽然现在它还处于 Alpha 阶段，但它宣传的特性完美契合我的所有需求：\n极致轻量与原生性能： 生成的可执行文件体积小，内存占用低。 跨平台能力： 一套代码，多端运行，完美契合我的需求。 系统托盘支持： 这是我最核心的需求之一，允许应用在关闭主窗口后，静默在后台运行。 Go 生态集成： Wails v3 基于 Go 后端，我可以使用我熟练的 Go 无缝地将 frp 管理，彻底解决 Tauri 中遇到的所有权问题。 熟读 Wails v3 Alpha 文档后，我兴奋异常。我将之前在 Fyne 和 Tauri 中已经梳理完善的业务逻辑和流程，迅速迁移到了 Wails v3 框架下。\n利用 Go 的便利性，我成功实现了：\n系统托盘的后台运行能力。 可视化配置管理（HTTP 映射），动态获取子域名和 frp 多用户 token。 一键启停 frp 客户端进程。 至此，一个功能完善的原型客户端基本完成。它优雅地解决了命令行和之前技术选型带来的所有痛点。\n在这里，我想详细聊一下Wails3。Wails 3 的启动逻辑高度模块化。在 main.go 中，一切从 application.New 开始。\n首先，我们需要通过 embed.FS 将前端生成物（Vite 构建后的 dist 目录）打包进二进制文件：\n//go:embed all:frontend/dist var assets embed.FS func main() { app := application.New(application.Options{ Name: \u0026#34;Mole\u0026#34;, Description: \u0026#34;Fast Reverse Proxy Manager\u0026#34;, Services: []application.Service{ application.NewService(\u0026amp;FrpService{}), // 绑定核心服务 }, Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, }) // 创建窗口逻辑... } 关键点： Services 是 Wails 3 的精髓。它是一个切片，支持绑定多个服务实例。每个服务中定义的公开方法，前端都可以通过自动生成的 JS 绑定直接调用。\n在开发 Mole 的原型过程中，我踩了不少坑，总结出以下 5 个核心经验：\n经验 1：系统托盘与“假关闭”逻辑。对于穿透工具，用户习惯点击“关闭”后应用依然在后台运行。我们需要在 application.WindowOptions 中将 DisableQuitOnLastWindowClosed 设为 true。然后监听 WindowsClosing 事件。当用户点击关闭图标时，调用 window.Hide() 而非销毁窗口。这样配合系统托盘（System Tray），可以实现应用的常驻运行。\n经验 2：OnShutdown 与资源回收的“终点站”。最初我尝试在 Service 的 OnShutdown 中释放资源，但在实现托盘模式后，发现生命周期管理变得复杂。我是在 main.go 的应用级别配置 OnShutdown 回调。在这里统一清理 frp 进程、临时文件或断开云端连接，能确保程序退出时干干净净。\n经验 3： ServiceStartup：预加载的黄金位置。每个 Service 都可以实现 ServiceStartup(ctx context.Context, options application.ServiceOptions) error 接口。在 Mole 中，我利用这个阶段完成以下任务：\n读取本地 config.yaml。 从云端同步最新的子域名与 Token。 初始化内部状态机。 经验 4： 完美封装：Embed 编译与二进制调用。为了实现“单文件分发”，我将 frpc 原始二进制文件也通过 embed.FS 打包。在交叉编译时，根据目标平台（Windows/macOS/Linux）选择性打包对应的 frpc，避免生成物过于臃肿。静默运行（Windows 特供）优化，启动 exec.Command 时，必须添加 syscall.SysProcAttr{HideWindow: true}。这能彻底隐藏那个令人生厌的命令行黑窗口。\n经验 5： 实时日志流：双 Goroutine 与事件机制。这是用户感知最强的功能。如何将 frp 的输出实时显示在前端？首先获取管道，通过 exec.Command 的 StdoutPipe 和 StderrPipe获取。然后并发读取，开启两个 Goroutine 分别读取这两个管道，防止阻塞。最后是事件推送，当读取到内容后，利用 app.Emit(\u0026ldquo;frp-logs\u0026rdquo;, content) 将数据推送到前端。前端只需要监听 frp-logs 事件，即可实现类似 Linux tail -f 的丝滑体验。\nWails 3 提供的 Services 和 Events 机制，极大地简化了 Go 与前端的通信。通过合理的生命周期管理（Startup/Shutdown）和进程控制，我们成功地为 frp 这个硬核工具披上了一层优雅的“外衣”。\n此时我们将转战前端，聊聊如何用 JS 配合 Wails 事件流构建实时日志看板，以及如何实现那个关键的“小程序激励视频”弹窗交互。在完成了 Go 后端的硬核逻辑后，压力来到了前端。Mole 的前端并没有复杂的全家桶，而是回归本质，将精力集中在与 Wails 3 的运行时（Runtime）交互上。\n在 Wails 3 中，前端不再需要通过 HTTP 接口访问后端，而是通过自动生成的 Bindings。在 main.js 中，这几行代码是整个应用的灵魂：\nimport { Events, Browser } from \u0026#34;@wailsio/runtime\u0026#34;; // 由 Wails 3 自动生成的绑定代码 import { HandleStopFrp, HandleStartFrp, GetDomainURL, } from \u0026#34;../bindings/mole/MoleService\u0026#34;; 其中，Events实核心组件，用于监听后端的主动推送到前端的消息（如日志、状态变更）。HandleStartFrp 等它们看起来是 JS 函数，但执行时会直接触发 Go 后端对应 Service 的方法。\n这样前后端就连接了起来。如果没有广告，那么开发工作就告一段落。但是开发 Mole 客户端的初衷之一，是希望探索一条个人开发者工具变现的路径。\n四、关于集成广告变现的思考 我最初的想法是利用自己的 3M 带宽服务器，结合微信小程序的广告生态，实现一个“共享经济”的模式：\n用户痛点： 临时需要公网 IP 、域名和稳定带宽进行本地调试。 我的资源： 闲置的服务器和域名资源。 解决方案： 用户在 Mole 客户端点击“连接”时，触发小程序激励广告。看完广告后，后端 API 自动为用户分配一个临时的、随机的子域名，并下发 frp 配置 Token。 这种 “桌面工具 + 小程序广告” 的模式，避开了复杂的桌面端支付系统，利用了微信成熟的广告体系。\n当我准备将这个服务正式上线时，我意识到一个致命的风险：内容合规性。\nfrp 是一个中立的工具，但它提供的“内网穿透”能力具有双面性。如果用户利用我的服务器进行不法活动（如诈骗、传播违规内容），作为服务器的所有者，域名备案的所有者，我将承担直接的法律责任。服务器和域名随时可能被封，甚至可能面临法律风险。\n对于个人开发者而言，这种风险是不可承受的。\n为了规避风险，我做出了一个艰难的决定：放弃动态配置。我将客户端锁定为只能穿透由我指定的、本地启动的特定服务（例如一个我开发的本地 Web 服务器）。这样我能控制内容源，降低风险。但这导致了项目核心吸引力的丧失。用户使用 frp 是为了灵活性，锁定端口后，这个工具对懂技术的开发者来说毫无吸引力。项目陷入僵局。\n既然提供“带宽服务”行不通，我决定回归“纯工具”本质。但我依然希望能产生收益。赞赏功能（Donation）是一个选项，但在竞争激烈的 frp GUI 市场，我的优势不大。\n我找到了一个新的平衡点：将开发过程本身作为内容输出。我决定将项目坚持写完，并将整个开发过程、技术难点、踩坑经验整理成系列文章，发布到我的个人网站上。\nMole 项目的商业化之路虽然坎坷，但其衍生的价值却超出了我的预期。我不仅熟练掌握了 Wails 3、Go 进程管理、跨端通信等技术栈，还找到了一个可持续发展的方向。\n我将继续开发一个公版（Generic Version）的 frp 管理工具——一个纯粹的、无商业绑定的、优雅的 frp GUI 客户端，并继续分享我的开发笔记。\n在演示版中还有一个极其重要的环节没有交代：服务端（frps）的架构设计。如果说 Mole 客户端是用户看到的“门脸”，那么服务端就是支撑多用户安全、有序运行的“大脑”。即便在未来的纯工具版中不再强制使用我的服务器，但这套多用户鉴权与动态域名的方案，依然值得每一位开发者备忘。\nfp-multiuser：实现多用户隔离的关键。在服务型工具中，不可能让所有用户共享一个 Token，否则无法追踪流量，也无法实现精准的权限控制。我选择了 frp 官方推荐的插件方案：fp-multiuser。它的核心逻辑是基于 OpLogin 事件的鉴权\n。fp-multiuser 实际上是一个基于 HTTP 协议的外部插件。它的精妙之处在于：当 frpc 尝试连接 frps 时，插件会拦截相关事件。\n在我的实现中，我重点使用了 OpLogin 事件：\n分配 Token：当用户通过 Mole 客户端（或小程序激励后）请求连接时，后端 API 会动态生成一个唯一的 Token 并下发给客户端。 校验映射：当 frpc 发起登录，fp-multiuser 插件会接收到这个 Token。插件通过查表或调用我的管理接口，确认该 Token 是否合法、对应哪个子域名。 唯一映射：这样就确保了 A 用户只能使用 A 子域名，彻底解决了多用户环境下域名冲突和越权访问的问题。 还有一个就是泛域名证书：让每个子域名都拥有 HTTPS。演示版支持用户通过 HTTPS 访问本地服务。面对随时可能生成的成百上千个二级域名（如 user1.example.com, user2.example.com），手动配置证书显然是不现实的。我使用了 Let\u0026rsquo;s Encrypt 的泛域名证书。通过 DNS-01 验证方式（利用 Certbot 或 acme.sh）申请 *.example.com 的证书。一个 .pem 文件即可覆盖所有二级域名，无需为每个新用户重新申请。\n最后，需要通过Nginx 反向代理配置打通链路。在服务端，我并没有让 frps 直接监听 443 端口，而是将其置于 Nginx 之后。Nginx 监听 443 端口，配置泛域名证书。利用 Nginx 的变量特性，将所有子域名的流量统一转发给 frps 的 vhost 端口。\nserver { listen 443 ssl; server_name *.example.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; location / { proxy_pass http://127.0.0.1:8080; # frps 的 vhost_http_port proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } 通过这种方式，客户端只需配置简单的 HTTP 映射，而在公网访问时，用户看到的是受保护的 HTTPS 链接。\n最后，我还想说下在集成小程序广告时，如何实现？它以前的核心业务是“看广告换带宽”。这就带来一个挑战：用户点击“连接”后，由于没看广告，流程会中断并弹出小程序码。前端如何知道用户什么时候看完了广告？\n拦截与判断\n我们不能在前端写 if(adWatched)，因为前端代码是透明的。逻辑必须在 Go 后端：如果后端判断该用户需要看广告，HandleStartFrp 会返回特定的状态码（如 2）。 状态机：从“连接中”到“等待验证”\n看看这段核心的 connect 逻辑： async function connect() { const hint = document.getElementById(\u0026#34;hint\u0026#34;); try { const res = await HandleStartFrp(); // 发起 Go 调用 if (res.code === 1) { // 情况 A: 验证通过，直接分配域名并连接 document.getElementById(\u0026#34;subdomain-url\u0026#34;).innerText = res.content; setUIConnected(true); addLog(\u0026#34;成功连接到服务器\u0026#34;, \u0026#34;system\u0026#34;); } else if (res.code === 2) { // 情况 B: 触发激励逻辑，弹出小程序码 showAdModal(res.content); // res.content 包含小程序码 Base64 或 URL addLog(\u0026#34;请扫码完成验证后继续\u0026#34;, \u0026#34;info\u0026#34;); // 【关键】注册一次性监听事件，等待后端“发令枪” const unsubscribe = Events.On(\u0026#34;ad-status\u0026#34;, (data) =\u0026gt; { closeAdModal(); // 关闭弹窗 unsubscribe(); // 销毁监听，防止重复触发 if (data.status === \u0026#34;done\u0026#34;) { addLog(\u0026#34;验证成功，正在连接...\u0026#34;, \u0026#34;system\u0026#34;); connect(); // 再次发起连接请求，此时后端将通过验证 } else { // 处理异常（如超时、未看完） hint.innerText = data.message; hint.classList.add(\u0026#34;error-shake\u0026#34;); addLog(`验证未完成: ${data.message}`, \u0026#34;error\u0026#34;); setUIConnected(false); } }); } } catch (err) { setUIConnected(false); } } 为什么这样设计？（经验总结）\n安全性（Security First）：\n连接逻辑的“入场券”完全由 Go 后端控制。前端只是一个 UI 展示层，即便用户强行修改 JS 调用 connect()，如果后端没有收到广告系统的回调信号，依然不会分配 frp 配置。 订阅-发布模式（Events）：\n使用 Events.On 而不是轮询（Polling）。当用户在手机上看完广告，后端 API 收到微信的回调后，会通过 app.Emit(\u0026ldquo;ad-status\u0026rdquo;, \u0026hellip;) 通知前端。这种实时性让用户体验非常丝滑——手机看完，电脑屏幕上的弹窗瞬间自动消失并连接。 UI 细节：单页面布局（Zero-Config UI）：\n由于 Mole 的界面追求极致精简，所有的 CSS 样式、HTML 布局和 JS 逻辑都高度集成在 index.html 和 main.js 中。对于这类工具软件，减少资源加载链比追求组件化更重要。 至此，FRP管理客户端从“为何而生”到“后端驱动”再到“前端交互”的开发逻辑已经全部分享完毕。这种 “桌面工具 + 小程序生态” 的模式，为个人开发者如何平衡“服务器成本”与“用户体验”提供了一个全新的思路。\n","permalink":"https://blog.91demo.top/go/demo-intro.html","summary":"\u003cp\u003e\u003cstrong\u003eFRP Manager\u003c/strong\u003e 是一款基于 \u003cstrong\u003eWails 3\u003c/strong\u003e 开发的跨平台 frp 管理客户端。它不仅解决了 frp 配置繁琐的痛点，更探索出了一种“桌面工具 + 小程序生态”的全新闭环模式。\u003c/p\u003e\n\u003ch2 id=\"-核心业务流程\"\u003e🌟 核心业务流程\u003c/h2\u003e\n\u003cp\u003eMole 的核心逻辑在于其\u003cstrong\u003e自动化配置流\u003c/strong\u003e与\u003cstrong\u003e激励机制\u003c/strong\u003e的融合：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Mole 流程图\" loading=\"lazy\" src=\"/images/frp-manager/mole-client.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-智能激励连接-one-click-connect\"\u003e1. 智能激励连接 (One-Click Connect)\u003c/h3\u003e\n\u003cp\u003e当用户点击“一键连接”时，客户端会启动一套自动验证逻辑：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e广告触发\u003c/strong\u003e：系统检查用户当前状态。若未满足条件，则弹出动态窗口显示专属小程序码。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e多端协同\u003c/strong\u003e：用户扫码后进入微信小程序观看激励视频。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e自动握手\u003c/strong\u003e：用户观看完毕后，小程序通过后端指令反馈给 Mole 客户端。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e静默取消\u003c/strong\u003e：客户端接收指令后自动关闭弹窗，进入连接阶段。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-云端配置自动下发\"\u003e2. 云端配置自动下发\u003c/h3\u003e\n\u003cp\u003eMole 不再需要用户手动编辑复杂的 TOML 文件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e动态获取\u003c/strong\u003e：从服务器端安全获取分配的\u003cstrong\u003e随机子域名\u003c/strong\u003e和 \u003cstrong\u003efrp 配置 Token\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e本地注入\u003c/strong\u003e：Go 后端自动将配置写入内置的 \u003ccode\u003efrpc.toml\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e二进制调用\u003c/strong\u003e：自动调用内嵌的 \u003ccode\u003efrpc\u003c/code\u003e 核心组件启动服务。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-全链路事件反馈\"\u003e3. 全链路事件反馈\u003c/h3\u003e\n\u003cp\u003e利用 Wails 3 的高效事件机制，实现深度的 UI 反馈：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e实时日志\u003c/strong\u003e：Go 后端捕获 frpc 的标准输出，通过事件流（Events）实时推送到前端页面，用户可以清晰看到连接建立过程。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e状态监控\u003c/strong\u003e：实时反馈隧道运行状态，确保连接稳定性。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-板块功能详情\"\u003e🛠️ 板块功能详情\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e连接控制台\u003c/strong\u003e：一键启停，集成小程序激励流程。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e端口配置页\u003c/strong\u003e：灵活配置本地服务端口（如 8080, 3000 等），满足开发者调试需求。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e实时日志页\u003c/strong\u003e：直观展示 frp 运行日志，方便故障排查。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e帮助中心\u003c/strong\u003e：详尽的操作指南，降低内网穿透的使用门槛。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术结缘 (About \u0026amp; Support)\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e集成优质云服务器推广（为用户提供可靠的 frp 服务端选择）。\u003c/li\u003e\n\u003cli\u003e扫码直达开发笔记，分享 Wails 3 与 Go 的底层实战。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-软件界面概览\"\u003e👀 软件界面概览\u003c/h2\u003e\n\u003ch3 id=\"1-软件主界面\"\u003e1. 软件主界面\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"Mole UI 截图\" loading=\"lazy\" src=\"/images/frp-manager/mole-main.webp\"\u003e\u003c/p\u003e","title":"基于 Wails 3 和微信小程序的一次frp 智能管理实战"},{"content":"在 mosquitto.conf 配置文件中，加入如下内容：\nlistener 18083 protocol websockets 这样，就可以开启 Websocket 服务。\n如果要开启 wss，需要添加证书，可以使用 nginx 代理，在 nginx 上设置证书。nginx 配置如下：\n# 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 \u0026#34;upgrade\u0026#34;; proxy_read_timeout 300s; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ","permalink":"https://blog.91demo.top/wiki/mqttws.html","summary":"\u003cp\u003e在 mosquitto.conf 配置文件中，加入如下内容：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elistener 18083\nprotocol websockets\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这样，就可以开启 Websocket 服务。\u003c/p\u003e\n\u003cp\u003e如果要开启 wss，需要添加证书，可以使用 nginx 代理，在 nginx 上设置证书。nginx 配置如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e    # mqtt\n\tlocation = /mqtt/ws {\n\t\tproxy_redirect off;\n        proxy_pass http://localhost:8083;\n        proxy_set_header Host $host:$server_port;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \u0026#34;upgrade\u0026#34;;\n        proxy_read_timeout 300s;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t}\n\u003c/code\u003e\u003c/pre\u003e","title":"Mosquitto 配置WebSocket连接"},{"content":"用户可以对接消息通知。集成 MQTT 后，就可以接收到 Visit 项目（豆子碎片小程序）文章审核消息。\n上线消息 JSON 格式如下：\n// flag 1 在线 2 离线 { \u0026#34;icode\u0026#34;:\u0026#34;你的icode\u0026#34;, \u0026#34;flag\u0026#34;:1 } 审核消息 JSON 格式如下：\n// 审核消息内容为标题+审核结果 { \u0026#34;icode\u0026#34;:\u0026#34;你的icode\u0026#34;, \u0026#34;content\u0026#34;:\u0026#34;审核消息内容\u0026#34; } 从豆子碎片小程序获取 MQTT 账户及地址，就可以连接到 MQTT 代理服务器。\n当你的 client 连接代理服务器成功后，你需要先发送一条上线通知，当你的 client 断开连接前，需要先发送一条下线通知。\n当你上传文章审核后，后台会推送一条审核消息内容。你有可能收到其它用户的消息。请通过 icode 来区分消息是否是自己的？\n","permalink":"https://blog.91demo.top/wiki/mqttvisit.html","summary":"\u003cp\u003e用户可以对接消息通知。集成 MQTT 后，就可以接收到 Visit 项目（豆子碎片小程序）文章审核消息。\u003c/p\u003e\n\u003cp\u003e上线消息 JSON 格式如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// flag 1 在线 2 离线\n{\n    \u0026#34;icode\u0026#34;:\u0026#34;你的icode\u0026#34;,\n    \u0026#34;flag\u0026#34;:1\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e审核消息 JSON 格式如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 审核消息内容为标题+审核结果\n{\n    \u0026#34;icode\u0026#34;:\u0026#34;你的icode\u0026#34;,\n    \u0026#34;content\u0026#34;:\u0026#34;审核消息内容\u0026#34;\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e从豆子碎片小程序获取 MQTT 账户及地址，就可以连接到 MQTT 代理服务器。\u003c/p\u003e\n\u003cp\u003e当你的 client 连接代理服务器成功后，你需要先发送一条上线通知，当你的 client 断开连接前，需要先发送一条下线通知。\u003c/p\u003e\n\u003cp\u003e当你上传文章审核后，后台会推送一条审核消息内容。你有可能收到其它用户的消息。请通过 icode 来区分消息是否是自己的？\u003c/p\u003e","title":"豆子碎片小程序使用MQTT接收文章审核消息"},{"content":" Mosquitto Go Auth 是 Mosquitto MQTT 代理的身份验证和授权插件。之所以叫这个名字，是因为历史的原因，现在改名字太晚了，影响太大。\nMosquitto 是一个很流行的开源 MQTT 代理。go-auth 主要使用 Go 语言编写，它使用 cgo 来调用 mosquitto 的 auth-plugin 函数。\n它支持并实现了以下后端：\n文件 Postgresql 数据库 Mysql 数据库 Sqlite3 数据库 MongoDB 数据库 Redis JWT HTTP gRPC Javascript 解释器 定制后端 每个后端都提供用户、超级用户和 ACL 检查，并包括适当的测试。\n我们先来构建它，构建它需要 cgo 环境，并且需要先构建 mosquitto。我们在 Linux 主机上安装 gcc，并安装 golang 环境，golang 版本要求 1.18 以上。\n我们从https://github.com/eclipse/mosquitto下载源码文件。然后参考说明文档编译安装 mosquitto。我使用的 CentOS7，直接通过yum install mosquitto安装，安装的 mosquitto 版本为 1.6.10。所以我要下载 mosquitto1.6.10 的源代码。将代码中的 mosquitto.h，mosqutto_plugin.h 和 mosquitto_broker.h 文件复制到/usr/local/include 目录。\n我们从https://github.com/iegomez/mosquitto-go-auth下载 go auth 的源码文件。我下载的当前版本为 2.1.0，解压缩并进入到源文件目录。输入命令make，将编译一个 pw 二进制文件和一个 go-auth.so 文件。我们将 go-auth.so 文件复制到/usr/local/lib目录，将 pw 二进制文件复制到/usr/local/bin目录。pw 可以用来生成密码，go-auth.so 是 mosquitto 调用的库。\n现在我们调整配置，启用 go auth 插件。在/etc/mosquitto/mosquitto.conf 文件中添加如下内容：\ninclude_dir /etc/mosquitto/conf.d 然后，在/etc/mosquitto 目录下，创建 conf.d 文件夹。进入 conf.d 文件夹，创建 go-auth.conf 文件。在该文件中，先加入如下内容：\nauth_plugin /etc/mosquitto/conf.d/go-auth.so auth_opt_backends files, postgres ## 添加日志 auth_opt_log_level debug auth_opt_log_dest file auth_opt_log_file /var/log/mosquitto.log ## 添加加密算法 auth_opt_hasher bcrypt auth_opt_hasher_cost 10 我只使用了文件和 PG 数据库这两种后端，所以先讲这两种的配置方法。先来配置文件。\nauth_opt_files_password_path /etc/mosquitto/conf.d/password_file auth_opt_files_acl_path /etc/mosquitto/conf.d/acl_file 先给这两个文件修改拥有者和属性。这样 mosquitto 服务启动后，可以有权限调用这两个文件。\nchown mosquitto:mosquitto password_file chown mosquitto:mosquitto acl_file chmod 0600 password_file chmod 0600 acl_file 使用 pw 为用户生成密码，\npw -h brcrypt -p 123456(你的密码) password_file 格式如下：\ntest1:$2a$10$ATi9XlzxcevYuiznP90cuuWDCvrKJKguF6KBhMKrIgWWtBSjd44XO test2:$2a$10$DSbIaUqwJ8nTBFHyEt.5GOvSbOcRVREJGNFdquOnRdk9.4QopcOjC test3:$2a$10$do17Uiopj9kQfPsmRIboh.3KwbUNHAS/7cUtxN8a44kUBQiAqbZ6i 注意，你的密码肯定和我的不一样。\nacl_file 格式如下：\nuser test1 topic write test/topic/1 topic read test/topic/2 user test2 topic read test/topic/+ user test3 topic read test/# pattern read test/%u pattern read test/%c 重启 mosquitto 服务，文件后端配置就生效了。\n下面我们再来讲讲如何配置 PG 数据库。需要先创建两个表，表的结构如下：\ncreate table test_user( id bigserial primary key, username character varying (100) not null, password_hash character varying (200) not null, is_admin boolean not null); create table test_acl( id bigserial primary key, test_user_id bigint not null references test_user on delete cascade, topic character varying (200) not null, rw int not null); 更具体的 PG 创建数据库和用户就不细节了。在库中创建这两个表。然后添加如下配置到 go-auth.conf 文件中。\nauth_opt_pg_host localhost auth_opt_pg_port 5432 auth_opt_pg_dbname appserver auth_opt_pg_user appserver auth_opt_pg_password appserver auth_opt_pg_connect_tries 5 auth_opt_pg_userquery select password_hash from test_user where username = $1 limit 1 auth_opt_pg_superquery select count(*) from test_user where username = $1 and is_admin = true auth_opt_pg_aclquery SELECT a.topic FROM test_user u left join test_acl a on u.id = a.test_user_id WHERE (u.username = $1) AND a.rw = $2 上面的配置信息仅作为参考，请根据你实际的情况进行调整。\n在实际调试连接时，碰到两个问题，记录下来，以备查询。\n1，连接 pg 数据库后，使用 mqtt 客户端无法连接，一直报错没有认证，数据报文 mqtt3.1.1 返回 5，mqtt5.0 返回 135。\n经过排查，是配置文件问题，我使用的 mosquitto 的版本为 1.6.10。在 mosquitto 配置文件中，将下面两行注释掉，让代理取 go-auth 中的配置。\nallow_anonymous false #password_file /etc/mosquitto/pwfile #acl_file /etc/mosquitto/aclfile include_dir /etc/mosquitto/conf.d 2，使用 pg 的账户连接成功后，一直无法订阅主题，提示权限问题。\n经过排查，是在 pg 数据库设置订阅权限问题，Readme 中有提到，但没有注意。\nMosquitto 1.5 introduced a new ACL access value, MOSQ_ACL_SUBSCRIBE, which is similar to the classic MOSQ_ACL_READ value but not quite the same: Also, these are the current available values from mosquitto: #define MOSQ_ACL_NONE 0x00 #define MOSQ_ACL_READ 0x01 #define MOSQ_ACL_WRITE 0x02 #define MOSQ_ACL_SUBSCRIBE 0x04 所以，如果你使用 1.5 版本以后，需要添加 MOSQ_ACL_SUBSCRIBE 0x04。否则没有权限。\n下面是调通之后的正常日志：\ntime=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;Superuser check with backend Postgres\u0026#34; time=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;Acl check with backend Postgres\u0026#34; time=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;user c7a610f7176ebf8f0056 acl authenticated with backend Postgres\u0026#34; time=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;Acl is true for user c7a610f7176ebf8f0056\u0026#34; ","permalink":"https://blog.91demo.top/wiki/mosquittogoauth.html","summary":"\u003ch1\u003e\u003c/h1\u003e\n\u003cp\u003eMosquitto Go Auth 是 Mosquitto MQTT 代理的身份验证和授权插件。之所以叫这个名字，是因为历史的原因，现在改名字太晚了，影响太大。\u003c/p\u003e\n\u003cp\u003eMosquitto 是一个很流行的开源 MQTT 代理。go-auth 主要使用 Go 语言编写，它使用 cgo 来调用 mosquitto 的 auth-plugin 函数。\u003c/p\u003e\n\u003cp\u003e它支持并实现了以下后端：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e文件\u003c/li\u003e\n\u003cli\u003ePostgresql 数据库\u003c/li\u003e\n\u003cli\u003eMysql 数据库\u003c/li\u003e\n\u003cli\u003eSqlite3 数据库\u003c/li\u003e\n\u003cli\u003eMongoDB 数据库\u003c/li\u003e\n\u003cli\u003eRedis\u003c/li\u003e\n\u003cli\u003eJWT\u003c/li\u003e\n\u003cli\u003eHTTP\u003c/li\u003e\n\u003cli\u003egRPC\u003c/li\u003e\n\u003cli\u003eJavascript 解释器\u003c/li\u003e\n\u003cli\u003e定制后端\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e每个后端都提供用户、超级用户和 ACL 检查，并包括适当的测试。\u003c/p\u003e\n\u003cp\u003e我们先来构建它，构建它需要 cgo 环境，并且需要先构建 mosquitto。我们在 Linux 主机上安装 gcc，并安装 golang 环境，golang 版本要求 1.18 以上。\u003c/p\u003e\n\u003cp\u003e我们从\u003ccode\u003ehttps://github.com/eclipse/mosquitto\u003c/code\u003e下载源码文件。然后参考说明文档编译安装 mosquitto。我使用的 CentOS7，直接通过\u003ccode\u003eyum install mosquitto\u003c/code\u003e安装，安装的 mosquitto 版本为 1.6.10。所以我要下载 mosquitto1.6.10 的源代码。将代码中的 mosquitto.h，mosqutto_plugin.h 和 mosquitto_broker.h 文件复制到/usr/local/include 目录。\u003c/p\u003e\n\u003cp\u003e我们从\u003ccode\u003ehttps://github.com/iegomez/mosquitto-go-auth\u003c/code\u003e下载 go auth 的源码文件。我下载的当前版本为 2.1.0，解压缩并进入到源文件目录。输入命令\u003ccode\u003emake\u003c/code\u003e，将编译一个 pw 二进制文件和一个 go-auth.so 文件。我们将 go-auth.so 文件复制到\u003ccode\u003e/usr/local/lib\u003c/code\u003e目录，将 pw 二进制文件复制到\u003ccode\u003e/usr/local/bin\u003c/code\u003e目录。pw 可以用来生成密码，go-auth.so 是 mosquitto 调用的库。\u003c/p\u003e","title":"配置数据库源，给 Mosquitto 配置 Go Auth 插件"},{"content":"为什么选择了Wails 3 ？ Wails 3 最大的改变在于它不再强绑定于某个特定的前端框架，且引入了多窗口支持和更轻量级的 Runtime。它允许你在不启动主窗口的情况下运行后端服务，这正是我们实现“系统托盘”和“后台演示服务”的基础。\n在 v2 中，我们习惯于自动生成的 wailsjs 文件夹。但在 v3 中，这一逻辑被进一步标准化。\n当你运行开发指令时，Wails 会扫描你的 Go 结构体方法，并将其映射为前端可以调用的 JavaScript 函数。这个过程在 v3 中被称为 Generate 过程。\nWails 3 通信灵魂 我们在前端调用在后端定义的 HandleConnect 返回自定义结构体，这在 Wails 3 中是前后端通信的灵魂。\n在 Wails 3 开发中，最核心的动作就是：后端做功，前端表现。\n当你调用 MoleService.HandleConnect() 时，Go 后端会产生一个结果。在本项目中，我们需要同时返回一个 Code（状态码）和一个 Content（数据内容）。\n为了实现这一点，我们定义了一个结构体：\ntype Response struct { Code int; Content string }\n虽然 Go 内部使用的是结构体，但前端 JavaScript 只能读懂 JSON 对象。Wails 3 内部会自动帮你完成这个“翻译”过程。\n但是，如果你想让前端看到的字段名是小写的（例如 res.code 而不是 res.Code），你必须在 Go 结构体定义时加上“注解”。\ntype Response struct { Code int `json:\u0026#34;code\u0026#34;` Content string `json:\u0026#34;content\u0026#34;` } 记住，所有通过 bindings 调用的 Go 方法，在前端返回的都是一个 Promise 对象。这意味着你必须使用 await 或者 .then() 来接收数据，否则你拿到的将是一个永不开启的“盲盒”。\n制作客户端界面 我们定义了客户端的宽度为600px，对于固定 600px 宽度的桌面应用，如何平衡“功能堆积”与“空间留白”是 UI 设计的核心。在 Wails 3 中，我们通过 application.WebviewWindowOptions 固定了窗口宽度为 600px。这是一个经典尺寸，但也给排版带来了挑战。\n呼吸感来自于一致的间距比例。在固定宽度 UI 中，最忌讳使用绝对像素（px）去堆砌每一个角落。我们应该定义一套全局的 CSS 变量，例如：\n--gap-sm: 8px; --gap-md: 16px; 避免文字窒息，你在本项目中看到的“帮助指引”和“入门说明”，我们显式设置了 text-indent: 0。这是因为在 600px 宽度下，首行缩进会严重破坏左侧视觉基准线，导致每一行看上去都长短不一。保持左侧对齐，利用 line-height（行高）来提供纵向的呼吸空间，才是桌面端 UI 的上策。\n响应式与动态计算，即使宽度固定，内部组件（如日志列表、配置表单）的宽度依然需要根据父容器动态调整。现代 CSS 不再需要依赖 JavaScript 来计算宽度，我们可以直接在样式表里写逻辑。\n例如，让一个容器宽度保持在“父级总宽减去两侧边距”：\nwidth: calc(100% - 40px);\n视觉伪装：禁止缩放，为了保持“App 感”，我们通常会在 HTML 的 Meta 标签中禁止用户缩放，并使用 CSS 属性来防止文本被用户意外选中导致界面发蓝：\nuser-select: none;\n系统托盘 在桌面应用中，系统托盘（System Tray）是区分“网页”与“真正的桌面软件”的关键标志，它赋予了应用“长驻后台”的能力。Wails 3 对系统托盘的支持比 v2 更加模块化。在本项目中，由于我们要维持 FRP 隧道的连接，不能让窗口一关就杀掉进程。\n拦截关闭动作，在 main.go 的窗口配置中，有一个关键的回调函数 ShouldClose。\n通常情况下，点击关闭按钮会销毁窗口。但在演示工具中，我们通过以下逻辑实现“隐遁”： ShouldClose: func(window *application.WebviewWindow) bool { window.Hide() // 仅仅是隐藏 return false // 告诉系统：不要真的关闭我 } 彻底退出的逻辑，当应用“隐遁”后，用户只能通过系统托盘菜单来彻底退出。在托盘菜单的“彻底退出”回调中，我们需要手动调用 app.Quit()。注意，此时必须显式停止我们开启的本地 Demo 服务（52025 端口），否则可能导致端口残留。\n托盘的视觉反馈，你可以为托盘设置图标（Icon）和菜单（Menu）。在 Wails 3 中，这可以通过简单的几行代码实现：\nsystemTray := app.NewSystemTray() systemTray.SetMenu(myMenu) 时空隧道：FRP 隧道原理与子进程管理 我们开始硬核的技术深挖。我们将揭开内网穿透的神秘面纱，并探讨在 Wails 3 后端如何启动和管理外部二进制程序。要实现内网穿透，本质上是在一台拥有公网 IP 的服务器 (frps) 和你的本地机器 (frpc) 之间建立一条加密的 TCP 长连接隧道。\nFRP 的角色分配 服务端 (frps)： 运行在公网，负责接收外部请求并转发。 客户端 (frpc)： 运行在本地，负责接收服务端转发的流量并传给你的本地服务（如 9090 端口）。 在 Wails 3 中集成 frpc 在 Wails 3 开发中，很多底层功能（如 FRP、ffmpeg、docker）我们选择直接调用成熟的二进制程序。这种“胶水开发”的核心在于子进程管理。我们通常不建议重新写一遍 FRP 的逻辑，而是直接调用其编译好的二进制文件。在 Go 后端，我们可以使用标准库来“拉起”这个程序。\n// 构建命令：./frpc -c ./frpc.toml cmd := exec.Command(\u0026#34;./frpc\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;config.toml\u0026#34;) cmd.Start() 这里使用cmd.Run()，会阻塞主进程，直到外部程序退出。这在 UI 应用中不可接受。使用cmd.Start()，立即启动外部程序并继续执行后面的代码。这正是 Wails 应用需要的。\n生命周期绑定 管理子进程最怕“孤儿进程”。当你的 Wails 应用崩溃或意外退出时，必须确保启动的 frpc 也能随之关闭。\n解决方法： 在应用退出事件中显式调用进程终止命令。\nfunc (s *MoleService) KillFRP() { if s.cmd != nil \u0026amp;\u0026amp; s.cmd.Process != nil { // 强制终结子进程 s.cmd.Process.Kill() } } 实时日志捕获 为了让前端能看到 FRP 的运行状态，我们需要重定向子进程的 Stdout。通过管道（Pipe）读取数据，再利用 Wails 的 Events 实时推送到界面。\n有了后台运行的子进程，如果前端是一片死寂，用户会感到不安。我们将攻克 Wails 3 的 Events（事件） 系统，实现日志的实时“流水线”。在桌面应用中，后端主动与前端沟通的唯一桥梁就是 事件系统 (Events)。\n当我们通过 os/exec 启动 FRP 后，可以使用 StdoutPipe 获取它的输出流。这就像是在子进程和主进程之间接了一根水管。\nstdout, _ := cmd.StdoutPipe() scanner := bufio.NewScanner(stdout) go func() { for scanner.Scan() { line := scanner.Text() // 将这一行发送给前端 s.pushLogToFrontend(line) } }() 在 Wails 3 中，后端推送事件不再需要复杂的上下文，直接通过全局应用对象即可操作。\n这种模式被称为 Pub/Sub (发布/订阅)。\n// 后端推送 app.EmitEvent(\u0026#34;frp-log\u0026#34;, message) 前端通过 Events.On 建立监听。为了避免内存泄漏，当组件销毁或连接断开时，我们需要调用返回的取消监听函数。\n在你的 main.js 中，这就是为什么我们写了 const unsubscribe = Events.On(...)。\n为了提升用户体验，日志列表通常需要“自动吸附底部”。\n我们在使用 calc 和 JavaScript 的 scrollHeight 属性在这里配合使用，能确保最新的日志永远出现在用户视野内。\n客户端集成小程序广告 纯粹的免费服务难以支撑高昂的服务器成本。通过“扫码验证 + 激励广告”构建闭环，是开发者与用户双赢的选择。\n在我们的 Wails 3 应用中，HandleConnect 接口不再是简单的“开关”。\n后端检查： 收到连接请求，先查 Redis：该用户（OpenID）是否已获得当天的授权？ 触发验证： 若未授权，返回自定义状态码 1002，前端捕获后立即展示小程序码。 用户扫码进入小程序后，触发 激励视频广告 (Rewarded Video Ad)。\n这种模式的优势在于：用户通过贡献约 15-30 秒的注意力，换取后续数小时的免广告服务。\n只有当小程序端确认广告播放完毕后，才会通知 Go 后端：\n// 小程序端逻辑 videoAd.onClose((res) =\u0026gt; { if (res \u0026amp;\u0026amp; res.isEnded) { // 用户完整观看了广告 wx.request({ url: \u0026#34;/api/v1/ad/success\u0026#34;, data: { sid: this.data.sid } }); } }); 后端收到请求，在 Redis 中将对应的 SessionID 标记为 authorized: true，此时桌面端的“连接”按钮才会真正生效。\n在桌面端显示扫码弹窗时，应用应通过 WebSocket 实时监听状态。一旦扫码或广告完成，弹窗应自动消失并立即启动 FRP 隧道，这种“无缝感”是提升软件高级感的关键。\n我们利用微信小程序作为“安全网关”，通过 OpenID 实现身份标识，并确保演示通道不被滥用。在本项目中，扫码不仅仅是为了展示广告，更核心的目的是建立安全审计追踪。\n当用户扫描小程序码并授权登录后，微信服务器会返回一个加密的字符串。对于同一个小程序，每个微信用户的这个字符串是固定且唯一的。\n作用： 它是我们在后台区分“张三”和“李四”的唯一凭证。 隐私性： 它不同于微信号，不会泄露用户的真实联系方式，具有极高的安全性。 它的扫码验证流程如下：\n触发： 前端调用 HandleConnect，后端检测到需要验证，返回 Code: 2。 展示： 前端弹出带有参数（如 SessionID）的小程序码。 核验： 用户扫码，小程序将 OpenID 与该 SessionID 绑定。 通知： 后端验证通过，通过 app.EmitEvent 通知桌面端关闭弹窗并继续。 通过在 Go 后端将 OpenID 与当前 FRP 隧道绑定，我们可以实现：\n频率限制： 同一个用户每小时只能连接 3 次。 内容追溯： 确保演示环境符合法律法规要求。 在我们的演示类软件中，安全性往往不仅取决于代码逻辑，更取决于对用户权限的限制。为了防止演示工具被用于非法用途（如搭建违规代理），我们需要在 UI 和后端实施“双重锁定”。作为开发者，我们不仅要实现功能，更要学会限制功能。\n锁定配置。为什么要锁定配置？内网穿透技术若被恶意利用（如映射敏感端口），开发者可能面临封号甚至更严重的法律责任。通过将端口锁定为特定的演示端口（如 52025），我们可以确保： 可控性： 流量只能流向我们预设的本地演示页面。 不可篡改： 即使是资深用户，也无法通过 UI 更改映射目标。 UI 层的“物理隔离”。在 HTML 中，我们通过两个关键动作锁定输入框： 逻辑锁定： 使用核心属性让用户无法输入。 视觉锁定： 通过 CSS 设置 cursor: not-allowed 和颜色变灰，给予用户明确的不可操作反馈。 \u0026lt;!-- 核心实现 --\u0026gt; \u0026lt;input type=\u0026#34;number\u0026#34; value=\u0026#34;52025\u0026#34; ________ tabindex=\u0026#34;-1\u0026#34; /\u0026gt; 后端内核的“硬编码”。千万不要只在前端做限制。在 Go 后端逻辑中，我们应该直接使用常量定义端口，而不是读取前端传来的参数。即使黑客通过浏览器控制台绕过 UI 修改了值，后端的“硬核”逻辑依然会保持服务在安全范围内运行。\n明确告知，不要让用户在猜测中点击。在配置页面增加“演示模式：配置已锁定”的显著提示，是一种专业的交互体现，能极大减少无效的技术支持请求。\n为了让用户不看看到运行成功后一片空白。当内网穿透成功、本地服务启动后，用户看到的第一个页面就是落地页（Landing Page）。这不仅是成功的证明，更是推广“广告和资源”的黄金位置。在 Wails 3 中，我们不希望用户运行程序时还要带着一堆 .html 文件夹。我们需要实现“单文件即走”。\n当用户看到“🎉 穿透成功”时，他们的多巴胺分泌会处于高峰期。此时是展示 Wails 3 实战开发笔记的最佳时机。\n为了防止图片路径丢失或 HTML 文件被误删，我们使用 Go 的原生嵌入功能。这能将你的 index.html 变成二进制文件中的一段切片。\n//go:embed index.html var landingHTML string func handler(w http.ResponseWriter, r *http.Request) { w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;text/html\u0026#34;) fmt.Fprint(w, landingHTML) } 由于落地页是在用户本地浏览器打开的，为了避免相对路径导致的加载失败，我们将小程序码等图片转换为 Base64 编码直接嵌入 HTML 标签中。\nsrc=\u0026ldquo;data:image/png;base64,iVBORw\u0026hellip;\u0026rdquo;\n在编写落地页 CSS 时，尽量使用 内联样式 或 \u0026lt;style\u0026gt; 标签。这样可以保证无论用户的网络状况如何，页面渲染的视觉效果始终如一，不会出现样式缺失的问题。\n落地页面完成后，我们需要提供服务来运行这个落地页。我们没有使用大型框架，仅用 Go 原生标准库搭建一个轻量级的“临时驿站”。在 Wails 3 项目中，我们有时需要在应用内部启动一个真正的 HTTP 服务。\n为什么不用 Gin 或 Fiber？虽然第三方框架很强大，但对于“演示页面”这种极其简单的需求，Go 原生的 net/http 是最完美的选择。它无需额外依赖，且能让二进制文件保持极其轻量的体积。\n我们可以把 ServeMux 想象成一个交通指挥官。它根据用户访问的 URL 路径（如 / 或 /status），将流量指引到不同的 Go 函数中。\n// 创建一个私有的多路复用器 mux := http.NewServeMux() mux.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { // 渲染你的演示成功 HTML }) 我们在客户端的配置页面锁定了本地52025 端口，现在我们将使用这个端口启动本地服务。通过 http.Server 结构体，我们可以更优雅地控制服务的开启与关闭，而不是直接使用简单的 ListenAndServe。\n我们可以利用 Go 的 embed 特性，将 HTML、CSS 和图片直接打包进二进制文件中，实现真正的“单文件分发”。用户无需额外携带一个 index.html 即可看到精美的演示效果。\n在开启本地服务端口时，虽然我们指定了52025端口，但是最让用户沮丧的就是点击连接后提示“端口已被占用”。所以我们需要编写一套自动化的“避障”逻辑。例如我们的演示服务默认监听 52025 端口，但如果用户的 Prometheus 或其他程序占用了它，服务就会直接崩溃。\n判断一个端口是否可用的最稳妥方法，就是尝试去“占有”它，如果成功了再立刻释放。\n在 Go 中，我们使用网络库的监听功能：\n// 核心探测函数 func IsPortOpen(port int) bool { address := \u0026#34;:\u0026#34; + strconv.Itoa(port) ln, err := net.Listen(\u0026#34;tcp\u0026#34;, address) if err != nil { return false // 端口已被占用 } ln.Close() // 探测完毕，立即释放 return true } 如果 52025 被占用了，我们不应该报错退出，而是应该尝试 52026、52027\u0026hellip; 直到找到一个干净的端口。\n这种“自动偏移”逻辑能极大提升软件的鲁棒性。\n为什么不使用 8080？8080、8000、9090 是开发者的“重灾区”。在设计演示工具时，选择 49152 之后 的动态端口段（Dynamic Ports）是国际公认的最佳实践，这能规避 90% 以上的常见软件冲突。\n资源清理 在桌面应用开发中，生命周期的完整性决定了软件的品质。我们应该让应用在退出时“体面”地释放所有资源。管理应用的“死法”和“活法”同样重要。\n为什么不能直接关闭窗口？我们设置了托盘模式，点击窗口的 X 只是隐藏。因此，我们必须在托盘菜单中设计一个【彻底退出】按钮。\n对于我们前面启动的 HTTP 服务，不能直接暴力杀掉进程，否则可能会导致 TCP 连接处于 TIME_WAIT 状态，长时间占用端口。\n在 Go 中，我们应该调用 server.Shutdown()：\n// 优雅关闭演示服务 func (s *MoleService) StopLocalDemoServer() { if s.server != nil { s.server.Shutdown(context.Background()) } } 我们在前面启动的 FRP 子进程，必须在应用退出前被显式杀死。\n利用 Wails 3 的事件监听，可以实现自动化清理：\napp.OnEvent(events.Common.AppWillTerminate, func(event *application.CustomEvent) { frpCmd.Process.Kill() // 终结 FRP }) 当一切资源（端口、进程、临时文件）清理完毕后，调用 Wails 的退出指令，应用将安全地从系统进程列表中消失。\n封装打包 我们已经完成了所有功能，是时候发布它了。在 Wails 3 中，打包（Build）不仅仅是编译，它是一次资源的重组与合规的审查。\n静态资源与二进制嵌入，我们在前面使用了 go:embed。在打包阶段，Wails 3 会自动处理前端的编译（npm build）并将其嵌入 Go 代码。\n重点： 确保你的 frpc 二进制文件也被放入了正确的目录，并随应用一同分发。 在 Windows 下，一个没有图标、没有版本信息的 .exe 看起来就像是木马。我们需要定义图标和打包信息：\nwails.json： 你需要在这里定义应用的图标路径、版本号以及是否需要管理员权限（UAC）。 优化： 使用 UPX 压缩技术可以显著减小体积，让原本 50MB 的程序瘦身至 15MB 左右。 我们有条件更应该给应用签名与公证。即使你本地编译成功，发给别人时也会提示“无法验证开发者”。你需要使用开发证书进行签名，对于MacOS还需要上传至 Apple 官方服务器进行公证。\n在跨平台发布的“坑”\n路径问题： 不同系统的资源路径斜杠不同（/ vs \\），建议使用 path/filepath 处理。 乱码问题： 在 Windows 终端输出时，确保字符集已处理为 UTF-8。 一个Wails应用基本介绍和应用就完毕了。感谢你的阅读！\n","permalink":"https://blog.91demo.top/go/wails3-demo.html","summary":"\u003ch2 id=\"为什么选择了wails-3-\"\u003e为什么选择了Wails 3 ？\u003c/h2\u003e\n\u003cp\u003eWails 3 最大的改变在于它不再强绑定于某个特定的前端框架，且引入了\u003cstrong\u003e多窗口支持\u003c/strong\u003e和\u003cstrong\u003e更轻量级的 Runtime\u003c/strong\u003e。它允许你在不启动主窗口的情况下运行后端服务，这正是我们实现“系统托盘”和“后台演示服务”的基础。\u003c/p\u003e\n\u003cp\u003e在 v2 中，我们习惯于自动生成的 \u003ccode\u003ewailsjs\u003c/code\u003e 文件夹。但在 v3 中，这一逻辑被进一步标准化。\u003c/p\u003e\n\u003cp\u003e当你运行开发指令时，Wails 会扫描你的 Go 结构体方法，并将其映射为前端可以调用的 JavaScript 函数。这个过程在 v3 中被称为 \u003cstrong\u003eGenerate\u003c/strong\u003e 过程。\u003c/p\u003e\n\u003ch3 id=\"wails-3-通信灵魂\"\u003eWails 3 通信灵魂\u003c/h3\u003e\n\u003cp\u003e我们在前端调用在后端定义的 HandleConnect 返回自定义结构体，这在 Wails 3 中是前后端通信的灵魂。\u003c/p\u003e\n\u003cp\u003e在 Wails 3 开发中，最核心的动作就是：\u003cstrong\u003e后端做功，前端表现\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e当你调用 \u003ccode\u003eMoleService.HandleConnect()\u003c/code\u003e 时，Go 后端会产生一个结果。在本项目中，我们需要同时返回一个 \u003ccode\u003eCode\u003c/code\u003e（状态码）和一个 \u003ccode\u003eContent\u003c/code\u003e（数据内容）。\u003c/p\u003e\n\u003cp\u003e为了实现这一点，我们定义了一个结构体：\u003cbr\u003e\n\u003ccode\u003etype Response struct { Code int; Content string }\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e虽然 Go 内部使用的是结构体，但前端 JavaScript 只能读懂 JSON 对象。Wails 3 内部会自动帮你完成这个“翻译”过程。\u003c/p\u003e\n\u003cp\u003e但是，如果你想让前端看到的字段名是小写的（例如 \u003ccode\u003eres.code\u003c/code\u003e 而不是 \u003ccode\u003eres.Code\u003c/code\u003e），你必须在 Go 结构体定义时加上“注解”。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003etype\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eResponse\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eCode\u003c/span\u003e    \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e    \u003cspan style=\"color:#e6db74\"\u003e`json:\u0026#34;code\u0026#34;`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eContent\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`json:\u0026#34;content\u0026#34;`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e记住，所有通过 bindings 调用的 Go 方法，在前端返回的都是一个 Promise 对象。这意味着你必须使用 await 或者 .then() 来接收数据，否则你拿到的将是一个永不开启的“盲盒”。\u003c/p\u003e","title":"跨平台桌面开发新选择：Wails 3 初体验及在 FRP 管理客户端中的选型实践"},{"content":"在 Mosquitto 实例上配置身份验证非常重要，这样未经授权的客户端就无法连接。\n在 Mosquitto 2.0 及更高版本中，你必须明确选择身份验证选项，然后客户端才能连接。早期版本中，默认设置是允许客户端无需身份验证即可连接。\n身份验证有三种选择：密码文件、身份验证插件和匿名访问。可以使用三个选项的组合。\n在 Mosquitto2.0 及更高版本中，可以通过配置文件中将 per_listener_settings 设置为 true，让不同的侦听器使用不同的身份验证方法。\n除了身份验证，您还应该考虑访问权限控制，以确定哪些客户端可以访问哪些主题。\n密码文件设置 密码文件是一种将用户名和密码存储在单个文件中的简单机制。适合于有相对较少的静态用户。请注意，修改了密码文件后，需要重新加载 mosquitto。\n创建密码文件，可以使用mosquitto_passwd工具，命令如下：\nmosquitto_passwd -c \u0026lt;password file\u0026gt; \u0026lt;username\u0026gt; 请注意，使用-c 标志将覆盖已存在的文件，如果向已存在文件添加，请去掉-c 标志。\n要开始使用密码文件，需要配置 broker，在配置文件中，添加如下项：\npassword_file \u0026lt;path to the configuration file\u0026gt; 密码文件必须能够被运行 mosquitto 的用户读取。\n身份验证插件设置 如果你需要更多的控制来认证用户，你需要使用认证插件。具体的特性依赖于你使用的认证插件。\n可使用的插件：\nmosquitto-go-auth ，它提供了各种后端来存储用户数据，例如 mysql,postgresql,jwt 或 redis 等。 Dynamic security，动态安全插件，仅适用于 2.0 及更高版本，可提供原创管理的灵活的代理客户端、组和角色。 配置身份验证插件依赖你的 Mosquitto 的版本。\n版本 1.6.x 和以前的版本，使用auth_plugin选项。这个选项在版本 2.0 也被支持。\nlistener 1883 auth_plugin \u0026lt;path to plugin\u0026gt; 版本 2.0 及更高，使用plugin选项。\nlistener 1883 plugin \u0026lt;path to plugin\u0026gt; 匿名访问设置 配置匿名访问，请使用allow_anonymous选项。\nlistener 1883 allow_anonymous true 允许对同一代理进行匿名和身份验证访问是有效的。特别是，动态安全插件允许您为匿名用户分配与经过身份验证的用户不同的权限，例如，这可能对数据的只读访问有用。\n限制采集器只能发布数据到自己的路径，且不能订阅别人的数据 场景：基于 ClientID 的细粒度 ACL 权限控制\n不要只开启全局密码，要增加一个 acl_file。假设你有两类设备：采集器（Sensor）和控制端（Admin）。\n操作步骤：\n在 mosquitto.conf 中指定：\nacl_file /etc/mosquitto/aclfile 在 /etc/mosquitto/aclfile 中写入：\n# pattern readwrite devices/%c/telemetry pattern read devices/%c/commands # 限制管理后台可以查看所有数据 user admin topic readwrite devices/# 注：%c 会自动匹配连接时的 ClientID，实现了一套配置管理成千上万个设备。\n站长提醒：在实际布署 ESP8266/ESP32 接入 Mosquitto 时，务必注意以下两点：\n连接超时：开启认证后，握手报文变大，若网络环境极差（如粮仓深处），需适当调大客户端的 setKeepAlive 时间。 明文风险：若不配合 TLS/SSL，密码在内网是明文传输的。在工业现场，建议至少配合内网隔离或简单的 username 混淆。 ","permalink":"https://blog.91demo.top/devops/mosquitto_config.html","summary":"\u003cp\u003e在 Mosquitto 实例上配置身份验证非常重要，这样未经授权的客户端就无法连接。\u003c/p\u003e\n\u003cp\u003e在 Mosquitto 2.0 及更高版本中，你必须明确选择身份验证选项，然后客户端才能连接。早期版本中，默认设置是允许客户端无需身份验证即可连接。\u003c/p\u003e\n\u003cp\u003e身份验证有三种选择：密码文件、身份验证插件和匿名访问。可以使用三个选项的组合。\u003c/p\u003e\n\u003cp\u003e在 Mosquitto2.0 及更高版本中，可以通过配置文件中将 per_listener_settings 设置为 true，让不同的侦听器使用不同的身份验证方法。\u003c/p\u003e\n\u003cp\u003e除了身份验证，您还应该考虑访问权限控制，以确定哪些客户端可以访问哪些主题。\u003c/p\u003e\n\u003ch2 id=\"密码文件设置\"\u003e密码文件设置\u003c/h2\u003e\n\u003cp\u003e密码文件是一种将用户名和密码存储在单个文件中的简单机制。适合于有相对较少的静态用户。请注意，修改了密码文件后，需要重新加载 mosquitto。\u003c/p\u003e\n\u003cp\u003e创建密码文件，可以使用\u003ccode\u003emosquitto_passwd\u003c/code\u003e工具，命令如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emosquitto_passwd -c \u0026lt;password file\u0026gt; \u0026lt;username\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e请注意，使用-c 标志将覆盖已存在的文件，如果向已存在文件添加，请去掉-c 标志。\u003c/p\u003e\n\u003cp\u003e要开始使用密码文件，需要配置 broker，在配置文件中，添加如下项：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epassword_file \u0026lt;path to the configuration file\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e密码文件必须能够被运行 mosquitto 的用户读取。\u003c/p\u003e\n\u003ch2 id=\"身份验证插件设置\"\u003e身份验证插件设置\u003c/h2\u003e\n\u003cp\u003e如果你需要更多的控制来认证用户，你需要使用认证插件。具体的特性依赖于你使用的认证插件。\u003c/p\u003e\n\u003cp\u003e可使用的插件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003emosquitto-go-auth ，它提供了各种后端来存储用户数据，例如 mysql,postgresql,jwt 或 redis 等。\u003c/li\u003e\n\u003cli\u003eDynamic security，动态安全插件，仅适用于 2.0 及更高版本，可提供原创管理的灵活的代理客户端、组和角色。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e配置身份验证插件依赖你的 Mosquitto 的版本。\u003c/p\u003e\n\u003cp\u003e版本 1.6.x 和以前的版本，使用\u003ccode\u003eauth_plugin\u003c/code\u003e选项。这个选项在版本 2.0 也被支持。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elistener 1883\nauth_plugin \u0026lt;path to plugin\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e版本 2.0 及更高，使用\u003ccode\u003eplugin\u003c/code\u003e选项。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elistener 1883\nplugin \u0026lt;path to plugin\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"匿名访问设置\"\u003e匿名访问设置\u003c/h2\u003e\n\u003cp\u003e配置匿名访问，请使用\u003ccode\u003eallow_anonymous\u003c/code\u003e选项。\u003c/p\u003e","title":"Mosquitto 安全加固指南：从匿名访问到多用户 ACL 细粒度权限控制"},{"content":"“豆子碎片”的第五次生命是一场华丽的冒险。它将知识封装成关卡，将学习转化为闯关，用动态表单和激励广告构建了一个充满探索感的游戏世界。九宫格的首页地图，更是带来了直观的视觉冲击。\n然而，当最初的兴奋感褪去，我以一个用户和开发者的双重身份重新审视它时，一些粗糙的边缘开始显现。网格布局虽有趣，但标题字数受限，信息表达不全；快速迭代下诞生的数据结构有些随意；用户进度的存储也让人不那么放心。它像一个充满创意的“原型”，但距离一个“稳健的产品”还有距离。\n于是，第六版的使命清晰了：不改变游戏的核心乐趣，而是对它的框架、体验与代码进行一次全面的“精修”与“加固”。这是一次从“颠覆性创新”转向“系统性优化”的旅程。\n一、界面优化：从网格到列表，信息优先 我做出的第一个显著改变，是首页布局。\n为何改变？\n第五版的九宫格网格，在内容增多后略显拥挤，且格子空间限制了关卡标题的完整展示，影响了一瞥之间的信息获取效率。\n如何改变？\n我将布局重构为清晰的单列列表。每个关卡独占一行，标题可以完整显示，描述更复杂的挑战成为可能。同时，我保留了核心的分页逻辑（如每页4-6条），确保了浏览的节奏感，避免了无尽滚动的疲惫。\n带来什么？\n新的列表布局，以牺牲部分游戏感为代价，换来了更高的信息密度和可读性。它更像个严肃的“挑战清单”，让用户能快速扫描和定位目标，体验更加沉稳、高效。\n二、架构优化：数据结构的“工艺化”规范 如果说界面是外表，数据结构就是骨架。我着手对第五版快速搭建的“骨架”进行加固和标准化。\n核心索引的规范化：我重新设计了描述整个游戏地图的核心数据文件（如 levels_index.json）。为每个关卡定义了严格、统一的字段结构，包括ID、标题、难度、标签、题型、答案模式等。这就像为所有零件制定了标准的图纸，让未来的扩展和维护有章可循。\n考题格式的统一化：动态表单背后的“考题”定义被统一为结构化的规范。无论是选择题、填空题还是代码题，都遵循同一套描述语言。这为批量管理考题、未来开发可视化编辑器、甚至支持更多题型打下了坚实的基础。\n用户进度存储的可靠性升级：通关状态是用户体验的核心。我优化了存储策略，结合本地缓存与轻量服务端校验，确保用户的闯关记录更安全、更可靠，减少了因设备或缓存问题导致进度丢失的风险。\n三、代码优化：删繁就简，追求稳健 最后，也是所有优化的基础，是一次代码层面的“大扫除”。\n“删减逻辑”：我回顾并简化了部分在快速迭代中产生的冗余或过度设计的逻辑。删除了不再使用的历史代码，合并了功能相似的函数，让代码的执行路径更加清晰。\n增强健壮性：对关键流程增加了更完善的错误处理与边界情况检查。让整个小程序在面对异常数据或网络波动时，行为更加可预测，从“能运行”走向“运行得稳健”。\n第六版的“豆子碎片”，没有改变第五版那颗“知识闯关游戏”的有趣灵魂。它所做的，是为这个灵魂打造了一副更结实、更得体、更耐用的躯壳。\n通过更清晰的界面（列表）、更稳固的基础（数据结构）、更可靠的体验（进度存储）、以及更简洁的代码，它完成了一次从“创意原型”到“可维护产品”的成熟蜕变。\n这标志着这个个人项目的重心，已经从“追逐新奇想法”的探索期，进入了“打磨用户体验与工程质量”的深耕期。它依然是一个充满个人色彩的工具，但已悄然具备了长期生命力的稳健形态。\n","permalink":"https://blog.91demo.top/wiki/v6.html","summary":"\u003cp\u003e“豆子碎片”的第五次生命是一场华丽的冒险。它将知识封装成关卡，将学习转化为闯关，用动态表单和激励广告构建了一个充满探索感的游戏世界。九宫格的首页地图，更是带来了直观的视觉冲击。\u003c/p\u003e\n\u003cp\u003e然而，当最初的兴奋感褪去，我以一个用户和开发者的双重身份重新审视它时，一些粗糙的边缘开始显现。网格布局虽有趣，但标题字数受限，信息表达不全；快速迭代下诞生的数据结构有些随意；用户进度的存储也让人不那么放心。它像一个充满创意的“原型”，但距离一个“稳健的产品”还有距离。\u003c/p\u003e\n\u003cp\u003e于是，第六版的使命清晰了：不改变游戏的核心乐趣，而是对它的框架、体验与代码进行一次全面的“精修”与“加固”。这是一次从“颠覆性创新”转向“系统性优化”的旅程。\u003c/p\u003e\n\u003ch2 id=\"一界面优化从网格到列表信息优先\"\u003e一、界面优化：从网格到列表，信息优先\u003c/h2\u003e\n\u003cp\u003e我做出的第一个显著改变，是首页布局。\u003c/p\u003e\n\u003cp\u003e为何改变？\u003c/p\u003e\n\u003cp\u003e第五版的九宫格网格，在内容增多后略显拥挤，且格子空间限制了关卡标题的完整展示，影响了一瞥之间的信息获取效率。\u003c/p\u003e\n\u003cp\u003e如何改变？\u003c/p\u003e\n\u003cp\u003e我将布局重构为清晰的单列列表。每个关卡独占一行，标题可以完整显示，描述更复杂的挑战成为可能。同时，我保留了核心的分页逻辑（如每页4-6条），确保了浏览的节奏感，避免了无尽滚动的疲惫。\u003c/p\u003e\n\u003cp\u003e带来什么？\u003c/p\u003e\n\u003cp\u003e新的列表布局，以牺牲部分游戏感为代价，换来了更高的信息密度和可读性。它更像个严肃的“挑战清单”，让用户能快速扫描和定位目标，体验更加沉稳、高效。\u003c/p\u003e\n\u003ch2 id=\"二架构优化数据结构的工艺化规范\"\u003e二、架构优化：数据结构的“工艺化”规范\u003c/h2\u003e\n\u003cp\u003e如果说界面是外表，数据结构就是骨架。我着手对第五版快速搭建的“骨架”进行加固和标准化。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e核心索引的规范化：我重新设计了描述整个游戏地图的核心数据文件（如 levels_index.json）。为每个关卡定义了严格、统一的字段结构，包括ID、标题、难度、标签、题型、答案模式等。这就像为所有零件制定了标准的图纸，让未来的扩展和维护有章可循。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e考题格式的统一化：动态表单背后的“考题”定义被统一为结构化的规范。无论是选择题、填空题还是代码题，都遵循同一套描述语言。这为批量管理考题、未来开发可视化编辑器、甚至支持更多题型打下了坚实的基础。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e用户进度存储的可靠性升级：通关状态是用户体验的核心。我优化了存储策略，结合本地缓存与轻量服务端校验，确保用户的闯关记录更安全、更可靠，减少了因设备或缓存问题导致进度丢失的风险。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"三代码优化删繁就简追求稳健\"\u003e三、代码优化：删繁就简，追求稳健\u003c/h2\u003e\n\u003cp\u003e最后，也是所有优化的基础，是一次代码层面的“大扫除”。\u003c/p\u003e\n\u003cp\u003e“删减逻辑”：我回顾并简化了部分在快速迭代中产生的冗余或过度设计的逻辑。删除了不再使用的历史代码，合并了功能相似的函数，让代码的执行路径更加清晰。\u003c/p\u003e\n\u003cp\u003e增强健壮性：对关键流程增加了更完善的错误处理与边界情况检查。让整个小程序在面对异常数据或网络波动时，行为更加可预测，从“能运行”走向“运行得稳健”。\u003c/p\u003e\n\u003cp\u003e第六版的“豆子碎片”，没有改变第五版那颗“知识闯关游戏”的有趣灵魂。它所做的，是为这个灵魂打造了一副更结实、更得体、更耐用的躯壳。\u003c/p\u003e\n\u003cp\u003e通过更清晰的界面（列表）、更稳固的基础（数据结构）、更可靠的体验（进度存储）、以及更简洁的代码，它完成了一次从“创意原型”到“可维护产品”的成熟蜕变。\u003c/p\u003e\n\u003cp\u003e这标志着这个个人项目的重心，已经从“追逐新奇想法”的探索期，进入了“打磨用户体验与工程质量”的深耕期。它依然是一个充满个人色彩的工具，但已悄然具备了长期生命力的稳健形态。\u003c/p\u003e","title":"豆子碎片小程序项目第六版"},{"content":"缘起：在完美的架构上，面对沉寂 我的小程序“豆子碎片”，在它的第四段生命中达到了一个技术上的稳定态：一台自主的Nginx代理服务器，像一座坚实的数据枢纽，保障了内容的稳定与可控；静默更新机制让它能自我焕新；反馈入口也让它不再是一座孤岛。\n然而，一个最本质的产品困境也随之浮现：它的浏览量越来越低，时常连续数日不见一个用户。​ 这很矛盾——我似乎建好了一座坚固、自动化的图书馆，却发现读者不再上门。是内容不合时宜，还是形式本身已失去了吸引力？\n我不愿看到它在精密的架构上“沉睡”。我需要一场根本性的变革，不是为了追逐数据，而是为了重新点燃创造与分享的火种。这一次，改变必须触及灵魂。\n破局：从“陈列知识”到“设计挑战” 我做出了一个大胆的决定：彻底抛弃沿用了四个版本的“文章列表”模式。​ 我不想它再是一个被动的知识仓库。我的新答案是：将它变成一个“知识闯关游戏”。\n这个转变，是产品哲学的重构：\n解构与升华：我不再直接展示文章。而是将每篇文章的核心知识点进行深度抽象，提炼成一个具体、明确的问题或挑战。例如，从《CSS Flex布局详解》一文，抽象出 “如何让元素在容器中水平垂直居中？”​ 这样一个关卡。\n重塑信息架构：首页不再是文章列表，而是一张 “知识探索地图”——一个九宫格式的关卡网格。每个格子是一个关卡，清晰标有挑战标题和用户的通关状态（锁形/对勾）。知识体系从此可视化、可追踪。\n重新定义“文章”：完整的文章并未消失，它们被移到了每个关卡的 “帮助”​ 按钮背后。文章从被阅读的终点，变成了用户闯关途中，可以主动查阅的“攻略”或“参考答案”。学习从被动接收，变成了“遇到问题 -\u0026gt; 主动求助 -\u0026gt; 解决问题”的主动探索循环。\n核心玩法：动态表单的解题仪式，用户点击一个关卡网格后，进入的不再是文章页，而是一个动态表单页。\n这里就是闯关的“考场”：\na. 形式：表单会根据当前关卡的问题，动态生成输入框、选择题、代码片段填写区等。它可能要求用户填写一个关键函数名，选择正确的执行顺序，或补全一段核心代码。\nb. 交互：用户需要运用自己的知识（或从“帮助”中寻找线索）来填写或选择答案。提交后，系统会进行验证。\nc. 结果：验证通过，关卡状态同步更新为“已通关”，九宫格地图上点亮一块拼图。验证失败，则停留在当前页面，用户可以选择再次尝试。\n精妙的融合：激励广告成为游戏机制，这次改版，我还无缝融入了一个关键设计：小程序的激励式视频广告。它的触发场景被精心设计：当用户多次尝试仍无法通过某个关卡时，系统会：“是否观看一段短视频广告，直接跳过此关卡？”\n这一设计一举三得：\n用户体验的减压阀：它提供了体面的“跳过”选择，将挫败感转化为自主权，防止用户流失。\n商业化的优雅植入：广告从强制阅读的干扰，变成了用户主动发起的“跳过”或“求助”工具，与游戏进程深度契合，接受度更高。\n项目可持续的微光：它为可能零星但长期的用户流量，提供了一个自然、低侵害的微变现可能，让项目有了覆盖基础运营成本的希望。\n它的技术实现：极简的三页架构\n理念虽宏大，实现却力求极简。整个小程序仅由三个核心页面构成：\n关卡地图页：承载九宫格网格，是游戏的主界面和进度总览。\n动态表单页：关卡的核心互动场，根据配置渲染不同的题目与输入组件，处理答案的提交与验证。\n帮助详情页：本质就是第四版及之前的文章详情页，无缝复用原有的Markdown解析与渲染能力，只是现在它作为“攻略”被调用。\n后端依然是那座稳定的Nginx代理服务器，继续可靠地提供着关卡配置、题目数据和文章内容。技术栈几乎未变，只是用全新的产品逻辑重组了信息的流动与交互。\n“豆子碎片”的第五次生命，是一场从“图书馆”到“冒险游戏”的迁徙。它把知识藏进了挑战里，把阅读变成了探索，把挫败感转化为可选择的捷径。\n这并非对过去的否定，而是一次螺旋式的上升。它没有抛弃“分享技术知识点”的初心，只是换了一种更具吸引力、也更符合学习本质的方式——在解决问题的过程中获得知识。那座由Nginx服务器构成的坚固堡垒依然矗立，但里面装载的不再是静态的书架，而是一张等待被点亮的、充满惊喜的知识寻宝图。\n这一次，它不再仅仅是我个人的数字笔记，而是一个向所有好奇者发出的、温和而有趣的挑战邀请。\n","permalink":"https://blog.91demo.top/wiki/v5.html","summary":"\u003ch2 id=\"缘起在完美的架构上面对沉寂\"\u003e缘起：在完美的架构上，面对沉寂\u003c/h2\u003e\n\u003cp\u003e我的小程序“豆子碎片”，在它的第四段生命中达到了一个技术上的稳定态：一台自主的Nginx代理服务器，像一座坚实的数据枢纽，保障了内容的稳定与可控；静默更新机制让它能自我焕新；反馈入口也让它不再是一座孤岛。\u003c/p\u003e\n\u003cp\u003e然而，一个最本质的产品困境也随之浮现：它的浏览量越来越低，时常连续数日不见一个用户。​ 这很矛盾——我似乎建好了一座坚固、自动化的图书馆，却发现读者不再上门。是内容不合时宜，还是形式本身已失去了吸引力？\u003c/p\u003e\n\u003cp\u003e我不愿看到它在精密的架构上“沉睡”。我需要一场根本性的变革，不是为了追逐数据，而是为了重新点燃创造与分享的火种。这一次，改变必须触及灵魂。\u003c/p\u003e\n\u003ch2 id=\"破局从陈列知识到设计挑战\"\u003e破局：从“陈列知识”到“设计挑战”\u003c/h2\u003e\n\u003cp\u003e我做出了一个大胆的决定：彻底抛弃沿用了四个版本的“文章列表”模式。​ 我不想它再是一个被动的知识仓库。我的新答案是：将它变成一个“知识闯关游戏”。\u003c/p\u003e\n\u003cp\u003e这个转变，是产品哲学的重构：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e解构与升华：我不再直接展示文章。而是将每篇文章的核心知识点进行深度抽象，提炼成一个具体、明确的问题或挑战。例如，从《CSS Flex布局详解》一文，抽象出 “如何让元素在容器中水平垂直居中？”​ 这样一个关卡。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e重塑信息架构：首页不再是文章列表，而是一张 “知识探索地图”——一个九宫格式的关卡网格。每个格子是一个关卡，清晰标有挑战标题和用户的通关状态（锁形/对勾）。知识体系从此可视化、可追踪。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e重新定义“文章”：完整的文章并未消失，它们被移到了每个关卡的 “帮助”​ 按钮背后。文章从被阅读的终点，变成了用户闯关途中，可以主动查阅的“攻略”或“参考答案”。学习从被动接收，变成了“遇到问题 -\u0026gt; 主动求助 -\u0026gt; 解决问题”的主动探索循环。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e核心玩法：动态表单的解题仪式，用户点击一个关卡网格后，进入的不再是文章页，而是一个动态表单页。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这里就是闯关的“考场”：\u003c/p\u003e\n\u003cp\u003ea. 形式：表单会根据当前关卡的问题，动态生成输入框、选择题、代码片段填写区等。它可能要求用户填写一个关键函数名，选择正确的执行顺序，或补全一段核心代码。\u003c/p\u003e\n\u003cp\u003eb. 交互：用户需要运用自己的知识（或从“帮助”中寻找线索）来填写或选择答案。提交后，系统会进行验证。\u003c/p\u003e\n\u003cp\u003ec. 结果：验证通过，关卡状态同步更新为“已通关”，九宫格地图上点亮一块拼图。验证失败，则停留在当前页面，用户可以选择再次尝试。\u003c/p\u003e\n\u003cp\u003e精妙的融合：激励广告成为游戏机制，这次改版，我还无缝融入了一个关键设计：小程序的激励式视频广告。它的触发场景被精心设计：当用户多次尝试仍无法通过某个关卡时，系统会：“是否观看一段短视频广告，直接跳过此关卡？”\u003c/p\u003e\n\u003cp\u003e这一设计一举三得：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e用户体验的减压阀：它提供了体面的“跳过”选择，将挫败感转化为自主权，防止用户流失。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e商业化的优雅植入：广告从强制阅读的干扰，变成了用户主动发起的“跳过”或“求助”工具，与游戏进程深度契合，接受度更高。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e项目可持续的微光：它为可能零星但长期的用户流量，提供了一个自然、低侵害的微变现可能，让项目有了覆盖基础运营成本的希望。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e它的技术实现：极简的三页架构\u003c/p\u003e\n\u003cp\u003e理念虽宏大，实现却力求极简。整个小程序仅由三个核心页面构成：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e关卡地图页：承载九宫格网格，是游戏的主界面和进度总览。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e动态表单页：关卡的核心互动场，根据配置渲染不同的题目与输入组件，处理答案的提交与验证。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e帮助详情页：本质就是第四版及之前的文章详情页，无缝复用原有的Markdown解析与渲染能力，只是现在它作为“攻略”被调用。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e后端依然是那座稳定的Nginx代理服务器，继续可靠地提供着关卡配置、题目数据和文章内容。技术栈几乎未变，只是用全新的产品逻辑重组了信息的流动与交互。\u003c/p\u003e\n\u003cp\u003e“豆子碎片”的第五次生命，是一场从“图书馆”到“冒险游戏”的迁徙。它把知识藏进了挑战里，把阅读变成了探索，把挫败感转化为可选择的捷径。\u003c/p\u003e\n\u003cp\u003e这并非对过去的否定，而是一次螺旋式的上升。它没有抛弃“分享技术知识点”的初心，只是换了一种更具吸引力、也更符合学习本质的方式——在解决问题的过程中获得知识。那座由Nginx服务器构成的坚固堡垒依然矗立，但里面装载的不再是静态的书架，而是一张等待被点亮的、充满惊喜的知识寻宝图。\u003c/p\u003e\n\u003cp\u003e这一次，它不再仅仅是我个人的数字笔记，而是一个向所有好奇者发出的、温和而有趣的挑战邀请。\u003c/p\u003e","title":"豆子碎片小程序项目第五版"},{"content":"我的小程序“豆子碎片”，在经历了平台梦的破碎和服务器到期的沉寂后，凭借一个巧妙的构思迎来了第三次生命：完全舍弃后端，将文章以Markdown文件托管在Git仓库，让小程序直接拉取、渲染。它成了一个零成本、静态化的数字笔记本，我以为这便是它最优雅、永恒的形态了。\n然而，免费的午餐总有代价。\n一、预警：来自免费依赖的背刺 在平稳运行一段时间后，危机不期而至。某一天，用户反馈和后台数据同时告诉我：小程序一整天都无法加载任何文章。排查之后，根源令我心中一沉：我所依赖的第三方Git服务，对非自身域名的高频次外链访问进行了拦截。紧接着，另一个隐患浮出水面：我存放在那里的文章，同样受制于平台的内容审核策略，随时有“被过滤”的风险。\n这次宕机像一记警钟。我终于清醒地认识到，我并没有解决核心的依赖问题——我只是将风险从需要付费的云服务器，转移到了一个免费但并不可控的第三方服务上。我的“永续运行”，建立在别人随时可能改变规则的沙地上。\n二、破局：构筑自主的数据枢纽 要真正解决问题，我必须为“豆子碎片”建立一个稳定且自主的数据枢纽。于是，我做了一个与“零服务器”理念看似相悖、实则更为务实的决定：重新购买一台轻量级服务器。因为我不再需要做内容平台，所以这台服务器配置可以很小。这一次，它的使命极为纯粹，不做复杂的业务逻辑，不跑庞大的数据库。我只用它做一件事：运行一个由Nginx搭建的、高性能的静态文件代理与缓存服务。\n架构的升级清晰而有力：\n内容源不变：我依然在本地用Markdown写作，用Git进行版本管理，并推送到远程仓库。这保留了我最舒适、最高效的创作工作流。\n链路自主化：小程序前端，所有指向第三方Git Raw链接的请求被全部替换。新的请求目标，指向我自己的这台服务器的文件服务。\n服务器作为网关：这台Nginx服务器扮演了智能网关的角色。它接收小程序的请求，然后返回对应的 index.json索引文件或 .md文章文件。获取成功后，它会在本地进行缓存，并返回给小程序。\n这一改变，一举解决了所有核心痛点：\n稳定性：我的服务器不会拦截和过滤文章内容，是合法、常规的访问模式，彻底解决了Git因客户端特征触发的风控拦截。\n自主性：文章内容经由我的服务器返回，Git平台不再提供静态文件服务，仅作为仓库，内容审核策略不再能影响我。我对最终交付的内容，拥有了完整的控制权。\n性能与成本可控：缓存机制降低了服务的压力，也提升了小程序的加载速度。这台轻量服务器的成本，远低于维护一个完整的应用后端。\n核心的东西从未改变——我依然在用Git管理内容，小程序依然在展示Markdown渲染的文章。但一切又都不同了：我用一台极简的Nginx服务器，替换了不可控的第三方依赖，为自己赢得了一片稳定的技术腹地。\n三、进化：从“能活”到“好活” 夺回了架构的控制权，就像为房子打下了坚实的地基。接下来，我开始考虑如何让住在里面的人更舒适。基于新的稳定架构，我实现了两次关键进化：\n技术体验：实现静默更新 在以前的纯静态模式下，每次我发布新文章，都必须更新小程序版本才能让用户看到，体验极差。我引入了 “版本号”机制。小程序启动时会向服务器查询一个简单的版本标识，如果发现线上有更新，便自动拉取最新的文章索引。从此，内容更新与小程序发版彻底解耦，用户获得了无缝的静默更新体验。\n产品形态：建立连接与生态 当技术不再成为掣肘，产品思维便有了空间。我不再仅仅满足于“展示”。\n借助服务器，我部署了自己的服务，我加入了 “反馈与联系”​ 的入口，让这个工具从单向输出，变为能聆听用户声音的窗口。\n我尝试在应用中，以不打扰的方式，与我开发的其他工具小程序进行相互引荐。我希望它们能彼此照亮，形成一个微小的、有共同气质的产品矩阵雏形。\n“豆子碎片”的第四次生命，始于一次外部依赖导致的崩溃，成于一个务实的架构决策。从完全依赖第三方，到引入自主的Nginx代理层，这一步看似是技术的“后退”（重新引入了服务器），实则是产品主权和稳定性的“大踏步前进”。\n它没有变得功能庞杂，反而在核心体验上更加健壮和流畅。它不再是一个脆弱的实验品，而成了一个拥有自主数据通道、可持续更新、并能与用户及其他产品产生连接的、真正意义上的完整产品。\n这一次，我守护住的，不仅是那个“展示技术思考”的简单初心，更是让这个初心得以在复杂多变的技术环境中，长期、稳定、自主存在的能力。这，便是第四段生命赋予它的全部意义。\n","permalink":"https://blog.91demo.top/wiki/v4.html","summary":"\u003cp\u003e我的小程序“豆子碎片”，在经历了平台梦的破碎和服务器到期的沉寂后，凭借一个巧妙的构思迎来了第三次生命：完全舍弃后端，将文章以Markdown文件托管在Git仓库，让小程序直接拉取、渲染。它成了一个零成本、静态化的数字笔记本，我以为这便是它最优雅、永恒的形态了。\u003c/p\u003e\n\u003cp\u003e然而，免费的午餐总有代价。\u003c/p\u003e\n\u003ch2 id=\"一预警来自免费依赖的背刺\"\u003e一、预警：来自免费依赖的背刺\u003c/h2\u003e\n\u003cp\u003e在平稳运行一段时间后，危机不期而至。某一天，用户反馈和后台数据同时告诉我：小程序一整天都无法加载任何文章。排查之后，根源令我心中一沉：我所依赖的第三方Git服务，对非自身域名的高频次外链访问进行了拦截。紧接着，另一个隐患浮出水面：我存放在那里的文章，同样受制于平台的内容审核策略，随时有“被过滤”的风险。\u003c/p\u003e\n\u003cp\u003e这次宕机像一记警钟。我终于清醒地认识到，我并没有解决核心的依赖问题——我只是将风险从需要付费的云服务器，转移到了一个免费但并不可控的第三方服务上。我的“永续运行”，建立在别人随时可能改变规则的沙地上。\u003c/p\u003e\n\u003ch2 id=\"二破局构筑自主的数据枢纽\"\u003e二、破局：构筑自主的数据枢纽\u003c/h2\u003e\n\u003cp\u003e要真正解决问题，我必须为“豆子碎片”建立一个稳定且自主的数据枢纽。于是，我做了一个与“零服务器”理念看似相悖、实则更为务实的决定：重新购买一台轻量级服务器。因为我不再需要做内容平台，所以这台服务器配置可以很小。这一次，它的使命极为纯粹，不做复杂的业务逻辑，不跑庞大的数据库。我只用它做一件事：运行一个由Nginx搭建的、高性能的静态文件代理与缓存服务。\u003c/p\u003e\n\u003cp\u003e架构的升级清晰而有力：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e内容源不变：我依然在本地用Markdown写作，用Git进行版本管理，并推送到远程仓库。这保留了我最舒适、最高效的创作工作流。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e链路自主化：小程序前端，所有指向第三方Git Raw链接的请求被全部替换。新的请求目标，指向我自己的这台服务器的文件服务。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e服务器作为网关：这台Nginx服务器扮演了智能网关的角色。它接收小程序的请求，然后返回对应的 index.json索引文件或 .md文章文件。获取成功后，它会在本地进行缓存，并返回给小程序。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这一改变，一举解决了所有核心痛点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e稳定性：我的服务器不会拦截和过滤文章内容，是合法、常规的访问模式，彻底解决了Git因客户端特征触发的风控拦截。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自主性：文章内容经由我的服务器返回，Git平台不再提供静态文件服务，仅作为仓库，内容审核策略不再能影响我。我对最终交付的内容，拥有了完整的控制权。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e性能与成本可控：缓存机制降低了服务的压力，也提升了小程序的加载速度。这台轻量服务器的成本，远低于维护一个完整的应用后端。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e核心的东西从未改变——我依然在用Git管理内容，小程序依然在展示Markdown渲染的文章。但一切又都不同了：我用一台极简的Nginx服务器，替换了不可控的第三方依赖，为自己赢得了一片稳定的技术腹地。\u003c/p\u003e\n\u003ch2 id=\"三进化从能活到好活\"\u003e三、进化：从“能活”到“好活”\u003c/h2\u003e\n\u003cp\u003e夺回了架构的控制权，就像为房子打下了坚实的地基。接下来，我开始考虑如何让住在里面的人更舒适。基于新的稳定架构，我实现了两次关键进化：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e技术体验：实现静默更新\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e在以前的纯静态模式下，每次我发布新文章，都必须更新小程序版本才能让用户看到，体验极差。我引入了 “版本号”机制。小程序启动时会向服务器查询一个简单的版本标识，如果发现线上有更新，便自动拉取最新的文章索引。从此，内容更新与小程序发版彻底解耦，用户获得了无缝的静默更新体验。\u003c/p\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e产品形态：建立连接与生态\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e当技术不再成为掣肘，产品思维便有了空间。我不再仅仅满足于“展示”。\u003c/p\u003e\n\u003cp\u003e借助服务器，我部署了自己的服务，我加入了 “反馈与联系”​ 的入口，让这个工具从单向输出，变为能聆听用户声音的窗口。\u003c/p\u003e\n\u003cp\u003e我尝试在应用中，以不打扰的方式，与我开发的其他工具小程序进行相互引荐。我希望它们能彼此照亮，形成一个微小的、有共同气质的产品矩阵雏形。\u003c/p\u003e\n\u003cp\u003e“豆子碎片”的第四次生命，始于一次外部依赖导致的崩溃，成于一个务实的架构决策。从完全依赖第三方，到引入自主的Nginx代理层，这一步看似是技术的“后退”（重新引入了服务器），实则是产品主权和稳定性的“大踏步前进”。\u003c/p\u003e\n\u003cp\u003e它没有变得功能庞杂，反而在核心体验上更加健壮和流畅。它不再是一个脆弱的实验品，而成了一个拥有自主数据通道、可持续更新、并能与用户及其他产品产生连接的、真正意义上的完整产品。\u003c/p\u003e\n\u003cp\u003e这一次，我守护住的，不仅是那个“展示技术思考”的简单初心，更是让这个初心得以在复杂多变的技术环境中，长期、稳定、自主存在的能力。这，便是第四段生命赋予它的全部意义。\u003c/p\u003e","title":"豆子碎片小程序项目第四版"},{"content":"我的小程序“豆子碎片”，迎来了它的第三次生命。\n这一次，它是在服务器到期、上一个雄心勃勃的版本彻底停摆后，从废墟里重新长出来的。\n它的第二段生命，曾是一个充满激情的平台梦。我为此开发了完整的创作者后台、公开的API、实时的MQTT通知，并设计了一套用“豆子点数”兑换人民币的激励循环。我想把它做成一个微型的、技术人共创的内容生态。但现实是骨感的：小程序广告收益极低且不透明，难以维系激励；用户与创作者的动力都不足；个人开发者权限也处处受限。这个复杂的循环，运行不久就悄然崩塌了。\n几个月后，服务器到期了。我没有续费。那个承载着API和数据库的“平台”，在物理上画上了句号。\n但我不想让它就这样消失。这个从我个人技术笔记成长起来的小程序，我投入了大量的时间和心血，像我的一个数字孩子，我舍不得。我必须在新的约束下——近乎零成本、零维护的约束下——为它找到一条新的活路。\n我的第一个决定，是做最彻底的产品减法。\n我砍掉了所有平台化的幻想。没有创作者上传，没有审核，没有激励系统。它回归了最初的模样：一个只属于我个人的、记录技术知识碎片的数字笔记本。原因很现实：一是我已没有精力维护复杂后台，二是小程序政策对个人开发者的内容上传限制越来越严。它不再是一个“平台”，重新变回一个单纯的“作品”。\n产品形态回归简单，但最核心的技术问题来了：内容存哪？怎么更新？\n以前，文章存在数据库，有个后台来维护。现在服务器都没了，这条路断了。直到一次偶然的灵感，让我找到了破局点：Git服务器（比如Gitee），不就是个现成的、免费的、极其稳定的文件服务器吗？\n我立刻动手改造。整个技术栈被彻底重构：\n创作回归本质：我不再需要后台。打开任何编辑器，用 Markdown 格式​写好文章，这本身就是最舒适的写作。\n存储拥抱版本控制：文章就是普通的 .md文件。我像提交代码一样，将它们 push到 Git 仓库的指定目录。这个仓库，成了我免费、永不过期、还带完整版本历史的“云端数据库”和“后台”。当然，为了能够访问，我也把代码公开，同时也把这种方案分享给了我的公众号读者。并获得了不小的流量。有很多读者与我同感。\n小程序变身静态渲染器：小程序端，我删光了所有调用自家后端的代码。新的数据流清晰极了：\na. 先拉目录：我在仓库里放了一个 index.json​ 文件，它就是全站的文章索引，里面用结构化的数据记录了所有文章的标题、摘要、时间和对应的文件路径。小程序启动后，第一件事就是用 downloadFile下载这个索引。\nb. 再找内容：所有文章列表的展示、以及前端搜索，都变成了在本地对这个 index.json文件进行查询和过滤。\nc. 最后渲染：用户点击某篇文章，小程序就根据路径，去下载那个具体的 .md文件，然后用我早已写好的 Markdown 解析器，将文本渲染成漂亮的页面。\n于是，“豆子碎片”的第三个版本，演化成了一个完全由 Git 驱动的静态小程序。数据在云端（Git仓库），逻辑在本地（小程序）。虽然我为此砍掉了所有动态、交互、平台化的功能，但最核心的东西从未改变：它依然是一个能优雅、可靠地展示我技术文章的地方。\n看似是战略撤退，实则是架构新生。它失去了平台的复杂性，却换来了前所未有的健壮性。只要我的 Git 仓库还在，只要小程序的基础框架还能运行，它就能一直活着，安静地待在那里，承载我的思考。\n这第三次生命，不再关于扩张的野心，而是关于专注的可持续。它不再试图成为一个生态，而是安心做好一个纯粹的工具——一个完全属于我个人，零成本运行，并且会一直伴随我成长的技术笔记。这次重构，让我更深刻地理解了“少即是多”的力量，以及如何在技术的约束下，找到最本真、最持久的价值。\n","permalink":"https://blog.91demo.top/wiki/v3.html","summary":"\u003cp\u003e我的小程序“豆子碎片”，迎来了它的第三次生命。\u003c/p\u003e\n\u003cp\u003e这一次，它是在服务器到期、上一个雄心勃勃的版本彻底停摆后，从废墟里重新长出来的。\u003c/p\u003e\n\u003cp\u003e它的第二段生命，曾是一个充满激情的平台梦。我为此开发了完整的创作者后台、公开的API、实时的MQTT通知，并设计了一套用“豆子点数”兑换人民币的激励循环。我想把它做成一个微型的、技术人共创的内容生态。但现实是骨感的：小程序广告收益极低且不透明，难以维系激励；用户与创作者的动力都不足；个人开发者权限也处处受限。这个复杂的循环，运行不久就悄然崩塌了。\u003c/p\u003e\n\u003cp\u003e几个月后，服务器到期了。我没有续费。那个承载着API和数据库的“平台”，在物理上画上了句号。\u003c/p\u003e\n\u003cp\u003e但我不想让它就这样消失。这个从我个人技术笔记成长起来的小程序，我投入了大量的时间和心血，像我的一个数字孩子，我舍不得。我必须在新的约束下——近乎零成本、零维护的约束下——为它找到一条新的活路。\u003c/p\u003e\n\u003cp\u003e我的第一个决定，是做最彻底的产品减法。\u003c/p\u003e\n\u003cp\u003e我砍掉了所有平台化的幻想。没有创作者上传，没有审核，没有激励系统。它回归了最初的模样：一个只属于我个人的、记录技术知识碎片的数字笔记本。原因很现实：一是我已没有精力维护复杂后台，二是小程序政策对个人开发者的内容上传限制越来越严。它不再是一个“平台”，重新变回一个单纯的“作品”。\u003c/p\u003e\n\u003cp\u003e产品形态回归简单，但最核心的技术问题来了：内容存哪？怎么更新？\u003c/p\u003e\n\u003cp\u003e以前，文章存在数据库，有个后台来维护。现在服务器都没了，这条路断了。直到一次偶然的灵感，让我找到了破局点：Git服务器（比如Gitee），不就是个现成的、免费的、极其稳定的文件服务器吗？\u003c/p\u003e\n\u003cp\u003e我立刻动手改造。整个技术栈被彻底重构：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e创作回归本质：我不再需要后台。打开任何编辑器，用 Markdown 格式​写好文章，这本身就是最舒适的写作。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e存储拥抱版本控制：文章就是普通的 .md文件。我像提交代码一样，将它们 push到 Git 仓库的指定目录。这个仓库，成了我免费、永不过期、还带完整版本历史的“云端数据库”和“后台”。当然，为了能够访问，我也把代码公开，同时也把这种方案分享给了我的公众号读者。并获得了不小的流量。有很多读者与我同感。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e小程序变身静态渲染器：小程序端，我删光了所有调用自家后端的代码。新的数据流清晰极了：\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003ea. 先拉目录：我在仓库里放了一个 index.json​ 文件，它就是全站的文章索引，里面用结构化的数据记录了所有文章的标题、摘要、时间和对应的文件路径。小程序启动后，第一件事就是用 downloadFile下载这个索引。\u003c/p\u003e\n\u003cp\u003eb. 再找内容：所有文章列表的展示、以及前端搜索，都变成了在本地对这个 index.json文件进行查询和过滤。\u003c/p\u003e\n\u003cp\u003ec. 最后渲染：用户点击某篇文章，小程序就根据路径，去下载那个具体的 .md文件，然后用我早已写好的 Markdown 解析器，将文本渲染成漂亮的页面。\u003c/p\u003e\n\u003cp\u003e于是，“豆子碎片”的第三个版本，演化成了一个完全由 Git 驱动的静态小程序。数据在云端（Git仓库），逻辑在本地（小程序）。虽然我为此砍掉了所有动态、交互、平台化的功能，但最核心的东西从未改变：它依然是一个能优雅、可靠地展示我技术文章的地方。\u003c/p\u003e\n\u003cp\u003e看似是战略撤退，实则是架构新生。它失去了平台的复杂性，却换来了前所未有的健壮性。只要我的 Git 仓库还在，只要小程序的基础框架还能运行，它就能一直活着，安静地待在那里，承载我的思考。\u003c/p\u003e\n\u003cp\u003e这第三次生命，不再关于扩张的野心，而是关于专注的可持续。它不再试图成为一个生态，而是安心做好一个纯粹的工具——一个完全属于我个人，零成本运行，并且会一直伴随我成长的技术笔记。这次重构，让我更深刻地理解了“少即是多”的力量，以及如何在技术的约束下，找到最本真、最持久的价值。\u003c/p\u003e","title":"豆子碎片小程序项目第三版"},{"content":"不想使用账号和密码登录，害怕被攻击，也不想做注册等功能；不想使用手机号和验证码登录，没有钱也没有资质去做这些。我想到了二维码，通过二维码进行登录。这里有一个核心的问题还是如何鉴权？\n在了解小程序后，我就决定使用小程序扫描二维码登录，使用小程序自带的微信账户体系完成鉴权。\n我们要知道，二维码（QR Code）是连接物理世界与数字世界的“虫洞”。在登录系统中，它承载着一个临时的身份信标。\n在实现这个Web登录功能的过程中，我们需要：\n1. 生成有效二维码 这个二维码需要显示在网页上，供用户扫码登录，登录二维码通常包含一个加密的 URL 或一个唯一的 UUID（通用唯一识别码）。\n这个唯一识别码需要具备的特性：\n唯一性： 每一对扫描动作都必须对应一个独一无二的 ID。 时效性： 二维码必须配合 Redis 设置过期时间（如 2 分钟），逾期自动失效。 在本项目中，我们将用户的微信 OpenID 作为 Key，生成的验证码作为 Value，使用Redis进行存储：\n// 存储验证码，并设置 5 分钟过期 err := rdb.Set(ctx, \u0026#34;user:123:code\u0026#34;, \u0026#34;8888\u0026#34;, 5*time.Minute).Err() // 读取验证码 val, err := rdb.Get(ctx, \u0026#34;user:123:code\u0026#34;).Result() 当有了二维码内容后，我们需要工具来生成二维码，在 Go 生态中，我们使用 skip2/go-qrcode 库来完成像素的绘制：\n// 生成二维码字节数组 var png []byte png, _ = qrcode.Encode(\u0026#34;91demo.top\u0026#34;+sessionID, qrcode.Medium, 256) 为了防止用户伪造扫码请求，二维码里的内容通常是加密的或者是不可预测的长随机数（UUID）。只有真实存在的 ID 才能通过后端的 Redis 校验。\n2. 传输二维码 当在服务端生成二维码后，还需要传递给浏览器，让浏览器进行显示，用户才可以扫码。这里有两个方法：1，生成图片文件，然后浏览器下载后显示。2，将图片内容转为Base64字符串传递给浏览器。这里我们选择了后者，我们不希望在用户硬盘上产生大量的临时 .png 文件。\n在Go中可以这样操作：\n// 转换为前端可直接识别的 Data URL base64Img := \u0026#34;data:image/png;base64,\u0026#34; + base64.StdEncoding.EncodeToString(png) 为了让前端不至于崩溃，后端必须返回统一的格式。\nfunc HandleLogin(c *gin.Context) { // 逻辑处理... c.JSON(200, gin.H{ \u0026#34;code\u0026#34;: 1, \u0026#34;content\u0026#34;: base64Img, }) } 在前端，我们不再需要引入沉重的第三方库来做简单的请求。浏览器原生提供的 Fetch API 简洁且基于 Promise。\n// 向 豆子实验室发起请求 fetch(\u0026#34;api.91demo.top\u0026#34;) .then((response) =\u0026gt; response.json()) .then((data) =\u0026gt; console.log(data)) .catch((error) =\u0026gt; console.error(\u0026#34;探索失败:\u0026#34;, error)); 需要注意的是，当尝试从 91demo.top 请求二维码的数据时，浏览器会为了安全触发跨域资源共享 (CORS) 检查。如果我们使用了不同的域名，需要后端（ 豆子实验室的服务器）在响应头中明确标记Access-Control-Allow-Origin。\n拿到的原始数据通常是 JSON 格式。我们需要将其解析为 JavaScript 对象，并利用模板引擎或 DOM 操作将其渲染到HTML页面上。这里使用HTML的img标签，二维码就可以正常显示在网页上了。\n3. 读取二维码 扫码动作本质上是将桌面端的 SessionID 传递给移动端。日常生活中，我们常见使用APP进行扫码，但是我没有精力去开发APP，所以这里使用了微信小程序的扫码功能。微信小程序提供了极其简便的接口来调用摄像头。它不仅能识别标准的二维码（QR Code），还能识别条形码。当然，这里我们仅仅使用二维码功能。\n// 小程序端：发起扫码 wx.scanCode({ onlyFromCamera: true, // 只允许相机扫码，不允许从相册选择 success: (res) =\u0026gt; { // res.result 包含了二维码中的内容，例如：sid=52025 const sid = parseQuery(res.result).sid; this.confirmLogin(sid); }, }); 当 wx.scanCode 成功后，小程序拿到了二维码里的“信标”（SessionID）。\n此时，我们需要在小程序中发起网络请求，告诉后端谁扫描了二维码，这里使用 wx.request 向后端发送：“我是用户 A，我刚扫到了 ID 为 52025 的设备，请记录。”\n这里使用了小程序提供的 API 用于与开发者服务器通信。与浏览器不同，它没有跨域限制，但要求通信必须使用安全的 HTTPS 协议。\n// 小程序端：确认网站登录 wx.request({ url: \u0026#34;api.91demo.top\u0026#34;, method: \u0026#34;POST\u0026#34;, data: { sessionId: \u0026#34;xxx\u0026#34;, // 扫码拿到的标识 code: \u0026#34;用来在服务端获取openid的code\u0026#34;, }, success: (res) =\u0026gt; { console.log(\u0026#34;扫码成功\u0026#34;); }, }); 此时，网页就通过二维码有了身份。\n4. 后端验证 我们先来介绍一下小程序身份，小程序自带微信账号体系。当小程序传入sessionID时，也携带了用户的小程序身份code，我们需要将这个code从临时凭证转到永久 openid。\n微信小程序的登录流程是典型的 三方安全鉴权 模型（小程序客户端、你的服务器、微信服务器）。\n首先是临时凭证：Code 的诞生，在小程序前端，我们调用 wx.login()。这个函数会返回一个名为 code 的临时凭证。\n时效性： code 只有 5 分钟有效期。 唯一性： 每次调用生成的 code 都不相同，且只能使用一次。 然后，当我们的服务器拿到 code 后，需要配合 AppID 和 AppSecret，从后端向微信接口发起请求：\n// Go 伪代码：请求微信 code2Session 接口 resp, err := http.Get(\u0026#34;code2Session接口地址\u0026#34; + code + \u0026#34;\u0026amp;grant_type=authorization_code\u0026#34;) 如果 code 有效，微信服务器会返回：\nOpenID： 用户的唯一标识（针对当前小程序）。 SessionKey： 会话密钥，用于后续对加密数据（如手机号、运动步数）的解密。 这里的OpenID就是我们想要的值，它标识了用户在小程序中的身份。\n设计这么复杂是为了防止 AppSecret 泄露。AppSecret 永远只留在我们的服务器上，而 code 在公网传输即使被截获，没有密钥也无法换取用户信息。\n好了，有了小程序身份之后，我们需要验证二维码中的sessionID。我们是在生成二维码时将网站生成的 SessionID 存入了 Redis。\n当后端接收到小程序的 wx.request 后，会执行以下逻辑：\n提取： 获取小程序传来的 SessionID。\n校验： 在 Redis 中查询该 ID 是否存在。\n标记： 将该 ID 的状态改为“已授权”，并写入用户信息。\n通知： 触发 WebSocket 或轮询，告诉网站：“用户已点击确认，准许进入”。\n好了，到了这一步，二维码内容中的sessionID已经有了身份，我们来看最后一步，Web端如何知道扫码已经成功。\n5. Web端获取登录状态 我们需要知道的是，当二维码显示在界面后，浏览器需要不断询问后端：“有人扫我了吗？”\n这里有两个实现方案：\n方案 A： 简单轮询（每 2 秒发一次 HTTP 请求）。 方案 B： 使用 WebSocket，当扫码成功时，后端主动推送到桌面端。 方案A的核心在于启动一个定时器，然后不断轮询后台，当登录成功后，浏览器自动调整到授权的面板页面。\n方案B使用了WebSocket，我们来简单介绍一下：\nHTTP 协议是“一问一答”的，这种模式在实时性要求高的场景下显得非常笨重。WebSocket 的出现彻底改变了这一点。\n在 JavaScript 中，建立WebSocket连接非常简单：\nconst socket = new WebSocket(\u0026#34;wss://api.dou-dou.top/ws\u0026#34;); // 监听连接开启 socket.onopen = () =\u0026gt; console.log(\u0026#34;心跳建立成功\u0026#34;); // 接收来自 豆子实验室的推送 socket.onmessage = (event) =\u0026gt; { const data = JSON.parse(event.data); console.log(\u0026#34;收到实时数据:\u0026#34;, data); }; 网络环境是复杂的，连接可能会因为路由超时或断网而静默中断。\n在实际应用中，为了维持连接，我们需要实现 心跳检测 (Heartbeat)。就像人类的脉搏一样，客户端每隔一段时间发送一个特定的微型包（Ping），服务器回复一个（Pong），确保通道始终畅通。\n同时为了具备“韧性”，这就需要重连机制。如果 WebSocket 意外关闭，前端需要有一套重连算法（通常采用指数退避策略），在不压垮服务器的前提下，尝试自动找回连接。\n无论使用哪种方案，都是将后端的身份验证状态告知前端。我们使用cookie来存储token，token中包含用户身份。当浏览器再次请求时，由于携带了cookie，服务端就知道谁在访问？同时也知道了它是否有权限访问。\n至此，我们就完成了二维码登录。在我的网站运行一段时间后，我发现还有更好的办法来登录。这就是小程序码登录，这个登录体验更流程，更便捷。\n6. 什么是小程序码？ 小程序码也称为太阳码，是微信独有的视觉交互方案，它比普通二维码更安全，因为其解析过程完全在微信内部完成。\n登录流程和逻辑大同小异，我们仅仅需要替换二维码即可。\n同样的道理，我们需要先生成小程序码，在请求微信生成小程序码之前，你的 Go 后端必须先向微信换取一个“通行证”—— access_token， 它是你调用微信服务器 API 的唯一身份证明。由于它有 2 小时的有效期，建议使用 Redis 进行缓存，避免频繁调用被限流。\n微信提供了几个接口来生成小程序码，最常用的是 getUnlimited，因为这个接口生成的码数量不受限制，非常适合大规模的登录业务。它允许你传递一个 scene 参数（场景值），scene有长度限制，不能超过32个字符，我们将生成的 SessionID 放入 scene 中。\n// Go 伪代码：请求微信生成小程序码 reqBody := map[string]interface{}{ \u0026#34;scene\u0026#34;: \u0026#34;sid=52025\u0026#34;, \u0026#34;page\u0026#34;: \u0026#34;pages/index/index\u0026#34;, \u0026#34;width\u0026#34;: 430, } // 微信会直接返回图片的二进制流 (Binary Stream) 与普通 API 返回 JSON 不同，微信这个接口返回的是图片的 二进制数据。\n在 Go 后端，我们需要读取响应体的 Body，并将其再次通过 Base64 编码，发送给前端显示。\n7. 小程序码登录 当我们打开微信扫描小程序码时，微信会将参数放入 scene 字段。在小程序的 onLoad 函数中，我们可以捕获到该参数值：\nonLoad(options) { if (options.scene) { // 微信会将 scene 进行 URL 编码，需要解码 const scene = decodeURIComponent(options.scene); // 提取我们在后端埋下的 sid=52025 this.setData({ sessionId: getQueryString(scene, \u0026#39;sid\u0026#39;) }); } } 后面的逻辑和二维码就一样了。我个人觉得它的最大便利就是扫码即完成登录。因为它不需要在小程序页面中再提供一个按钮去调用scanCode扫描二维码了。\n想尝试吗？可以点击登录体验一下。\n","permalink":"https://blog.91demo.top/web/qrcode_login.html","summary":"\u003cp\u003e不想使用账号和密码登录，害怕被攻击，也不想做注册等功能；不想使用手机号和验证码登录，没有钱也没有资质去做这些。我想到了二维码，通过二维码进行登录。这里有一个核心的问题还是如何鉴权？\u003c/p\u003e\n\u003cp\u003e在了解小程序后，我就决定使用小程序扫描二维码登录，使用小程序自带的微信账户体系完成鉴权。\u003c/p\u003e\n\u003cp\u003e我们要知道，二维码（QR Code）是连接物理世界与数字世界的“虫洞”。在登录系统中，它承载着一个临时的\u003cstrong\u003e身份信标\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e在实现这个Web登录功能的过程中，我们需要：\u003c/p\u003e\n\u003ch3 id=\"1-生成有效二维码\"\u003e1. 生成有效二维码\u003c/h3\u003e\n\u003cp\u003e这个二维码需要显示在网页上，供用户扫码登录，登录二维码通常包含一个加密的 URL 或一个唯一的 \u003cstrong\u003eUUID\u003c/strong\u003e（通用唯一识别码）。\u003c/p\u003e\n\u003cp\u003e这个唯一识别码需要具备的特性：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e唯一性：\u003c/strong\u003e 每一对扫描动作都必须对应一个独一无二的 ID。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e时效性：\u003c/strong\u003e 二维码必须配合 Redis 设置过期时间（如 2 分钟），逾期自动失效。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在本项目中，我们将用户的微信 OpenID 作为 Key，生成的验证码作为 Value，使用Redis进行存储：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 存储验证码，并设置 5 分钟过期\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003erdb\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eSet\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ectx\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;user:123:code\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;8888\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e5\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003etime\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eMinute\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eErr\u003c/span\u003e()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 读取验证码\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eval\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eerr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003erdb\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eGet\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ectx\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;user:123:code\u0026#34;\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eResult\u003c/span\u003e()\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e当有了二维码内容后，我们需要工具来生成二维码，在 Go 生态中，我们使用 \u003ccode\u003eskip2/go-qrcode\u003c/code\u003e 库来完成像素的绘制：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 生成二维码字节数组\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003epng\u003c/span\u003e []\u003cspan style=\"color:#66d9ef\"\u003ebyte\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003epng\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003e_\u003c/span\u003e = \u003cspan style=\"color:#a6e22e\"\u003eqrcode\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eEncode\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;91demo.top\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003esessionID\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eqrcode\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eMedium\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e256\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e为了防止用户伪造扫码请求，二维码里的内容通常是加密的或者是不可预测的长随机数（UUID）。只有真实存在的 ID 才能通过后端的 Redis 校验。\u003c/p\u003e\n\u003ch3 id=\"2-传输二维码\"\u003e2. 传输二维码\u003c/h3\u003e\n\u003cp\u003e当在服务端生成二维码后，还需要传递给浏览器，让浏览器进行显示，用户才可以扫码。这里有两个方法：1，生成图片文件，然后浏览器下载后显示。2，将图片内容转为Base64字符串传递给浏览器。这里我们选择了后者，我们不希望在用户硬盘上产生大量的临时 .png 文件。\u003c/p\u003e\n\u003cp\u003e在Go中可以这样操作：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 转换为前端可直接识别的 Data URL\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003ebase64Img\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;data:image/png;base64,\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ebase64\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eStdEncoding\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eEncodeToString\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003epng\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e为了让前端不至于崩溃，后端必须返回统一的格式。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eHandleLogin\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ec\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003egin\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eContext\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 逻辑处理...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ec\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eJSON\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e200\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003egin\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eH\u003c/span\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;code\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;content\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#a6e22e\"\u003ebase64Img\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    })\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在前端，我们不再需要引入沉重的第三方库来做简单的请求。浏览器原生提供的 \u003cstrong\u003eFetch\u003c/strong\u003e API 简洁且基于 Promise。\u003c/p\u003e","title":"基于 Go + 小程序实现网页端“扫码登录”实战"},{"content":"当我使用小程序码登录网站时，我发现了一个问题，在手机端不方便登录。我们都知道手机号验证码可以登录网站，但是我没有资源去实现手机号验证码功能，我使用一个变通的方案，在手机端不使用手机号验证码也能登录。\n小程序实现验证码登录 它的核心还是鉴权，我在小程序端制作了一个获取验证码界面，它可以生成模拟编号和验证码。当用户点击获取验证码时，会向后台请求返回编号和验证码，后台这个时候会记录为哪个用户openid生成了哪个验证码。那为什么不单单使用验证码呢？还要再添加一个编号，不麻烦吗？因为单单使用验证码会发生撞车的可能。要知道，验证码一般4位或者6位，很容易被暴力攻击的。\n当用户在网站端输入编号和验证码时，后端会校验是否存在这对编号和验证码，如果校验正确，将取出openid并绑定到sessionID上，然后返回给前端，存入cookie中。可以看看前面的文章，扫描二维码登录，同样的道理。\n除了在小程序生成验证码外，我们还可以在公众号中发送消息获取验证码。它其实也是使用了微信的账号体系。\n公众号实现验证码登录 要知道公众号不仅是内容分发平台，更是一个强大的身份认证中间件。我们使用发送消息来获取验证码信息。\n1. 握手与回调 (Webhook) 当你在公众号消息发送验证码三个字时，微信会推送事件到你的服务器。我使用go对接了微信公众号消息。\n要在 Go 中接收微信消息，你必须先在微信公众平台配置一个 服务器地址 (URL)。** 微信会向你的 URL 发送一个 GET 请求，包含签名、随机数等。你必须按照规定的算法计算并返回正确的 echostr，这被称为“服务器验证”。 验证通过后，每当用户发送消息，微信服务器就会以 POST 方式将消息体推送给你。\n2. 解析 XML 数据 与现代 API 不同，微信公众号的推送采用的是 XML 格式。Go 的标准库 encoding/xml 提供了强大的解析能力：\ntype WxMsg struct { ToUserName string `xml:\u0026#34;ToUserName\u0026#34;` FromUserName string `xml:\u0026#34;FromUserName\u0026#34;` // 这就是用户的 OpenID Content string `xml:\u0026#34;Content\u0026#34;` // 用户发来的文字 } 3. 验证码生存逻辑 当用户发送“验证码”关键字时，我们的 Go 后端会生成一个 4-6 位的随机数和一个编号。并将消息中的 OpenID 与 编号和随机数 存入 Redis，并设置 TTL（如 5 分钟）。然后通过 XML 响应将验证码发回给用户。\n4. 身份绑定，完成登录 我们在Web HTML页面提供了登录表单，提供编号和验证码输入框。用户输入编号和验证码后提交到后端，后端从 Redis 中根据编号和验证码反查 OpenID，若存在且有效，则代表身份验证成功，返回包含身份的TOKEN。\n在考虑手机验证码的时候，我正在研究Asterisk，顺便实现了一个语音验证码。这个语音验证码没有对接电信系统，所以体验上差点意思。但这不妨碍我们研究其原理。\n我实现语音验证码的思路如下：\n1，制作了一个小程序页面，上面有一个按钮就叫语音验证码。\n2，当点击获取语音验证码按钮时，会请求服务端，获取该用户的openid，然后查找其绑定的VOIP账号。\n3，当找到账号后，会生成编号和验证码，主键是编号和验证码，值为openid。\n4，后端go服务会调用ARI，传递编号和验证码，呼叫openid对应的VOIP账号。\n5，如果此时它的客户端在线，就会振铃，接听后会播报语音验证码和编号。\n6，用户在Web页面填入听到的验证码和编号，点击登录。\n7，后端会根据它查找对应的openid，生成token，返回给浏览器。\n8，后续的浏览器请求会携带这个token，我们就知道是谁？这就完成了登录。\n在实现的过程中，我们发现发送语音请求通常需要 1-3 秒才能得到Asterisk的响应。在 Go 中，我们绝不能让主处理函数在那里“傻等”。\n通过使用 Goroutine，我们可以瞬间返回“已发起呼叫”的信号给小程序前端，而真实的 API 请求在后台悄悄进行。\n// 异步发起语音呼叫 go func(phone string, code string) { err := smsService.Call(phone, code) if err != nil { log.Println(\u0026#34;语音播报失败:\u0026#34;, err) } }(userPhone, verifyCode) 我没有对接电信系统，如果对接了电信系统，这个是需要收费的。语音呼叫成本较高且容易被攻击骚扰用户。在逻辑中，我们必须通过 Redis 实施严格的限制：\n同一手机号 60 秒内只能获取一次。 同一 IP 每天限制获取 5 次。 如果你喜欢这项技术，可以点击登录体验一下。\n","permalink":"https://blog.91demo.top/web/vcode_login.html","summary":"\u003cp\u003e当我使用小程序码登录网站时，我发现了一个问题，在手机端不方便登录。我们都知道手机号验证码可以登录网站，但是我没有资源去实现手机号验证码功能，我使用一个变通的方案，在手机端不使用手机号验证码也能登录。\u003c/p\u003e\n\u003ch3 id=\"小程序实现验证码登录\"\u003e小程序实现验证码登录\u003c/h3\u003e\n\u003cp\u003e它的核心还是鉴权，我在小程序端制作了一个获取验证码界面，它可以生成模拟编号和验证码。当用户点击获取验证码时，会向后台请求返回编号和验证码，后台这个时候会记录为哪个用户openid生成了哪个验证码。那为什么不单单使用验证码呢？还要再添加一个编号，不麻烦吗？因为单单使用验证码会发生撞车的可能。要知道，验证码一般4位或者6位，很容易被暴力攻击的。\u003c/p\u003e\n\u003cp\u003e当用户在网站端输入编号和验证码时，后端会校验是否存在这对编号和验证码，如果校验正确，将取出openid并绑定到sessionID上，然后返回给前端，存入cookie中。可以看看前面的文章，扫描二维码登录，同样的道理。\u003c/p\u003e\n\u003cp\u003e除了在小程序生成验证码外，我们还可以在公众号中发送消息获取验证码。它其实也是使用了微信的账号体系。\u003c/p\u003e\n\u003ch3 id=\"公众号实现验证码登录\"\u003e公众号实现验证码登录\u003c/h3\u003e\n\u003cp\u003e要知道公众号不仅是内容分发平台，更是一个强大的\u003cstrong\u003e身份认证中间件\u003c/strong\u003e。我们使用发送消息来获取验证码信息。\u003c/p\u003e\n\u003ch4 id=\"1-握手与回调-webhook\"\u003e1. 握手与回调 (Webhook)\u003c/h4\u003e\n\u003cp\u003e当你在公众号消息发送验证码三个字时，微信会推送事件到你的服务器。我使用go对接了微信公众号消息。\u003c/p\u003e\n\u003cp\u003e要在 Go 中接收微信消息，你必须先在微信公众平台配置一个 \u003cstrong\u003e服务器地址 (URL)\u003c/strong\u003e。** 微信会向你的 URL 发送一个 GET 请求，包含签名、随机数等。你必须按照规定的算法计算并返回正确的 \u003ccode\u003eechostr\u003c/code\u003e，这被称为“服务器验证”。 验证通过后，每当用户发送消息，微信服务器就会以 POST 方式将消息体推送给你。\u003c/p\u003e\n\u003ch4 id=\"2-解析-xml-数据\"\u003e2. 解析 XML 数据\u003c/h4\u003e\n\u003cp\u003e与现代 API 不同，微信公众号的推送采用的是 \u003cstrong\u003eXML\u003c/strong\u003e 格式。Go 的标准库 \u003ccode\u003eencoding/xml\u003c/code\u003e 提供了强大的解析能力：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003etype\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eWxMsg\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eToUserName\u003c/span\u003e   \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`xml:\u0026#34;ToUserName\u0026#34;`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eFromUserName\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`xml:\u0026#34;FromUserName\u0026#34;`\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e// 这就是用户的 OpenID\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003eContent\u003c/span\u003e      \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`xml:\u0026#34;Content\u0026#34;`\u003c/span\u003e      \u003cspan style=\"color:#75715e\"\u003e// 用户发来的文字\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"3-验证码生存逻辑\"\u003e3. 验证码生存逻辑\u003c/h4\u003e\n\u003cp\u003e当用户发送“验证码”关键字时，我们的 Go 后端会生成一个 4-6 位的随机数和一个编号。并将消息中的 OpenID 与 编号和随机数 存入 Redis，并设置 TTL（如 5 分钟）。然后通过 XML 响应将验证码发回给用户。\u003c/p\u003e\n\u003ch4 id=\"4-身份绑定完成登录\"\u003e4. 身份绑定，完成登录\u003c/h4\u003e\n\u003cp\u003e我们在Web HTML页面提供了登录表单，提供编号和验证码输入框。用户输入编号和验证码后提交到后端，后端从 Redis 中根据编号和验证码反查 OpenID，若存在且有效，则代表身份验证成功，返回包含身份的TOKEN。\u003c/p\u003e","title":"构建自定义证码登录系统及研究语音验证码实战"},{"content":"所有的宏大工程都始于最微小的表达。这一节，我们聊聊那个只有 index.html 的纯真时代。本文将带你了解数字世界背后的故事。\n1. 域名的“翻译”艺术 服务器在网络中是以 IP 地址（如 123.123.123.123）存在的。为了让人类能够记忆，我们引入了域名系统（DNS）。\n解析逻辑： 通过配置一条 A 记录，我们将 91demo.top 映射到特定的数字 IP 上。从此，枯燥的数字拥有了可读的身份。 2. 安全的阶梯：从 HTTP 到 HTTPS 一个现代化的网站不应“裸奔”。\n使用 Let\u0026rsquo;s Encrypt 提供的免费证书，配合 Nginx 的反向代理配置，我们为网站穿上了 TLS 加密的铠甲。\n成就达成： 浏览器地址栏那个小锁头的出现，标志着你已初步掌握了网络协议的基础。 3. 守门人：Nginx 的静默力量 在本项目中，没有繁琐的数据库和 API 逻辑。Nginx 扮演了一个纯粹的“守门人”角色。\n它安静地运行在后台，监听着 80 与 443 端口，随时准备将那一个笨拙却真诚的 index.html 展示给世界。\n“虽然 CSS 是 AI 生成的，但我终于学会了如何让代码在服务器上跳舞。”\n4. 归于简单：单页的本质 现在的 Web 世界如迷宫般复杂，但回看那个单页网站，那不仅是 HTML，更是开发者第一次触碰数字创造的本质：用基础的材料，构建被看见的天地。\n","permalink":"https://blog.91demo.top/wiki/bean_web.html","summary":"\u003cp\u003e所有的宏大工程都始于最微小的表达。这一节，我们聊聊那个只有 \u003ccode\u003eindex.html\u003c/code\u003e 的纯真时代。本文将带你了解数字世界背后的故事。\u003c/p\u003e\n\u003ch3 id=\"1-域名的翻译艺术\"\u003e1. 域名的“翻译”艺术\u003c/h3\u003e\n\u003cp\u003e服务器在网络中是以 IP 地址（如 \u003ccode\u003e123.123.123.123\u003c/code\u003e）存在的。为了让人类能够记忆，我们引入了域名系统（DNS）。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e解析逻辑：\u003c/strong\u003e 通过配置一条 \u003cstrong\u003eA 记录\u003c/strong\u003e，我们将 \u003ccode\u003e91demo.top\u003c/code\u003e 映射到特定的数字 IP 上。从此，枯燥的数字拥有了可读的身份。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-安全的阶梯从-http-到-https\"\u003e2. 安全的阶梯：从 HTTP 到 HTTPS\u003c/h3\u003e\n\u003cp\u003e一个现代化的网站不应“裸奔”。\u003cbr\u003e\n使用 \u003cstrong\u003eLet\u0026rsquo;s Encrypt\u003c/strong\u003e 提供的免费证书，配合 Nginx 的反向代理配置，我们为网站穿上了 TLS 加密的铠甲。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e成就达成：\u003c/strong\u003e 浏览器地址栏那个小锁头的出现，标志着你已初步掌握了网络协议的基础。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-守门人nginx-的静默力量\"\u003e3. 守门人：Nginx 的静默力量\u003c/h3\u003e\n\u003cp\u003e在本项目中，没有繁琐的数据库和 API 逻辑。\u003cstrong\u003eNginx\u003c/strong\u003e 扮演了一个纯粹的“守门人”角色。\u003cbr\u003e\n它安静地运行在后台，监听着 80 与 443 端口，随时准备将那一个笨拙却真诚的 \u003ccode\u003eindex.html\u003c/code\u003e 展示给世界。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“虽然 CSS 是 AI 生成的，但我终于学会了如何让代码在服务器上跳舞。”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"4-归于简单单页的本质\"\u003e4. 归于简单：单页的本质\u003c/h3\u003e\n\u003cp\u003e现在的 Web 世界如迷宫般复杂，但回看那个单页网站，那不仅是 HTML，更是开发者第一次触碰数字创造的本质：\u003cstrong\u003e用基础的材料，构建被看见的天地。\u003c/strong\u003e\u003c/p\u003e","title":"从单页网站开启数字创造"},{"content":"小程序使用相册 微信小程序提供了丰富的相册操作 API，允许开发者访问用户的相册、拍照、选择图片、保存图片等功能。这些功能主要通过 wx.chooseMedia、wx.saveImageToPhotosAlbum 等 API 实现。\n下面是使用的一些示例：\n1，定义变量\ndata: { // 选择的图片列表 imageList: [], // 当前选中的图片索引 currentIndex: 0, // 是否显示预览 showPreview: false, // 加载状态 loading: false, // 保存结果提示 saveResult: \u0026#39;\u0026#39; }, onLoad() { console.log(\u0026#39;相册页面加载\u0026#39;); this.checkAlbumPermission(); }, /** * 检查相册权限 */ checkAlbumPermission() { wx.getSetting({ success: (res) =\u0026gt; { if (!res.authSetting[\u0026#39;scope.writePhotosAlbum\u0026#39;]) { // 如果没有相册权限，提示用户授权 this.requestAlbumPermission(); } } }); }, /** * 请求相册权限 */ requestAlbumPermission() { wx.authorize({ scope: \u0026#39;scope.writePhotosAlbum\u0026#39;, success: () =\u0026gt; { console.log(\u0026#39;相册权限授权成功\u0026#39;); }, fail: () =\u0026gt; { console.log(\u0026#39;相册权限授权失败\u0026#39;); this.showPermissionGuide(); } }); }, /** * 显示权限引导 */ showPermissionGuide() { wx.showModal({ title: \u0026#39;权限提示\u0026#39;, content: \u0026#39;需要相册权限才能保存图片，请在设置中开启权限\u0026#39;, confirmText: \u0026#39;去设置\u0026#39;, success: (res) =\u0026gt; { if (res.confirm) { wx.openSetting({ success: (res) =\u0026gt; { console.log(\u0026#39;打开设置成功\u0026#39;, res); } }); } } }); }, /** * 选择相册图片 */ chooseFromAlbum() { wx.chooseMedia({ count: 9, // 最多选择9张 mediaType: [\u0026#39;image\u0026#39;], // 只选择图片 sourceType: [\u0026#39;album\u0026#39;], // 从相册选择 maxDuration: 30, camera: \u0026#39;back\u0026#39;, success: (res) =\u0026gt; { console.log(\u0026#39;选择图片成功\u0026#39;, res); const tempFiles = res.tempFiles; const newImageList = tempFiles.map((file, index) =\u0026gt; ({ 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: \u0026#39;\u0026#39; }); wx.showToast({ title: `成功选择${newImageList.length}张图片`, icon: \u0026#39;success\u0026#39;, duration: 2000 }); }, fail: (err) =\u0026gt; { console.error(\u0026#39;选择图片失败\u0026#39;, err); this.handleChooseError(err); } }); }, /** * 拍照并选择 */ takePhoto() { wx.chooseMedia({ count: 1, mediaType: [\u0026#39;image\u0026#39;], sourceType: [\u0026#39;camera\u0026#39;], // 从相机拍照 camera: \u0026#39;back\u0026#39;, success: (res) =\u0026gt; { console.log(\u0026#39;拍照成功\u0026#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: \u0026#39;\u0026#39; }); wx.showToast({ title: \u0026#39;拍照成功\u0026#39;, icon: \u0026#39;success\u0026#39;, duration: 2000 }); }, fail: (err) =\u0026gt; { console.error(\u0026#39;拍照失败\u0026#39;, err); this.handleChooseError(err); } }); }, /** * 处理选择错误 */ handleChooseError(err) { let errorMsg = \u0026#39;选择图片失败\u0026#39;; switch (err.errMsg) { case \u0026#39;chooseMedia:fail auth deny\u0026#39;: errorMsg = \u0026#39;没有相册访问权限\u0026#39;; break; case \u0026#39;chooseMedia:fail cancel\u0026#39;: errorMsg = \u0026#39;用户取消了选择\u0026#39;; break; case \u0026#39;chooseMedia:fail no media\u0026#39;: errorMsg = \u0026#39;没有找到图片\u0026#39;; break; default: errorMsg = err.errMsg || errorMsg; } wx.showToast({ title: errorMsg, icon: \u0026#39;none\u0026#39;, duration: 2000 }); }, /** * 预览图片 */ previewImage(e) { const index = e.currentTarget.dataset.index; const urls = this.data.imageList.map(item =\u0026gt; item.tempFilePath); wx.previewImage({ current: urls[index], urls: urls, success: () =\u0026gt; { console.log(\u0026#39;预览图片成功\u0026#39;); }, fail: (err) =\u0026gt; { console.error(\u0026#39;预览图片失败\u0026#39;, err); wx.showToast({ title: \u0026#39;预览失败\u0026#39;, icon: \u0026#39;none\u0026#39; }); } }); }, /** * 选择/取消选择图片 */ toggleSelect(e) { const index = e.currentTarget.dataset.index; const imageList = this.data.imageList.map((item, i) =\u0026gt; { if (i === index) { return { ...item, selected: !item.selected }; } return item; }); this.setData({ imageList }); }, /** * 全选/取消全选 */ toggleSelectAll() { const allSelected = this.data.imageList.every(item =\u0026gt; item.selected); const imageList = this.data.imageList.map(item =\u0026gt; ({ ...item, selected: !allSelected })); this.setData({ imageList }); }, /** * 获取选中的图片 */ getSelectedImages() { return this.data.imageList.filter(item =\u0026gt; item.selected); }, /** * 保存选中图片到相册 */ saveSelectedToAlbum() { const selectedImages = this.getSelectedImages(); if (selectedImages.length === 0) { wx.showToast({ title: \u0026#39;请先选择图片\u0026#39;, icon: \u0026#39;none\u0026#39;, duration: 2000 }); return; } this.setData({ loading: true, saveResult: \u0026#39;\u0026#39; }); // 批量保存 this.batchSaveImages(selectedImages); }, /** * 批量保存图片 */ async batchSaveImages(images) { let successCount = 0; let failCount = 0; const results = []; for (let i = 0; i \u0026lt; 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) =\u0026gt; { wx.saveImageToPhotosAlbum({ filePath: image.tempFilePath, success: () =\u0026gt; { console.log(\u0026#39;保存图片成功\u0026#39;, image.tempFilePath); resolve(); }, fail: (err) =\u0026gt; { console.error(\u0026#39;保存图片失败\u0026#39;, err); reject(new Error(this.getSaveErrorMsg(err))); } }); }); }, /** * 获取保存错误信息 */ getSaveErrorMsg(err) { switch (err.errMsg) { case \u0026#39;saveImageToPhotosAlbum:fail auth deny\u0026#39;: return \u0026#39;没有相册写入权限\u0026#39;; case \u0026#39;saveImageToPhotosAlbum:fail cancel\u0026#39;: return \u0026#39;用户取消了保存\u0026#39;; case \u0026#39;saveImageToPhotosAlbum:fail no image\u0026#39;: return \u0026#39;图片文件不存在\u0026#39;; default: return err.errMsg || \u0026#39;保存失败\u0026#39;; } }, /** * 显示保存结果 */ showSaveResult(successCount, failCount, results) { let title = \u0026#39;\u0026#39;; if (failCount === 0) { title = `成功保存${successCount}张图片`; } else if (successCount === 0) { title = `保存失败，${failCount}张图片未保存`; } else { title = `成功${successCount}张，失败${failCount}张`; } this.setData({ saveResult: title }); wx.showModal({ title: \u0026#39;保存结果\u0026#39;, content: title, showCancel: false, success: () =\u0026gt; { // 清空选择状态 const imageList = this.data.imageList.map(item =\u0026gt; ({ ...item, selected: false })); this.setData({ imageList }); } }); // 记录详细结果 console.log(\u0026#39;保存详细结果:\u0026#39;, results); }, /** * 压缩图片 */ compressImage(imagePath) { return new Promise((resolve, reject) =\u0026gt; { wx.compressImage({ src: imagePath, quality: 80, // 压缩质量 0-100 success: (res) =\u0026gt; { console.log(\u0026#39;图片压缩成功\u0026#39;, res); resolve(res.tempFilePath); }, fail: (err) =\u0026gt; { console.error(\u0026#39;图片压缩失败\u0026#39;, err); reject(err); } }); }); }, /** * 获取图片信息 */ getImageInfo(imagePath) { return new Promise((resolve, reject) =\u0026gt; { wx.getImageInfo({ src: imagePath, success: (res) =\u0026gt; { console.log(\u0026#39;图片信息获取成功\u0026#39;, res); resolve(res); }, fail: (err) =\u0026gt; { console.error(\u0026#39;图片信息获取失败\u0026#39;, err); reject(err); } }); }); }, /** * 批量压缩并保存 */ async compressAndSaveSelected() { const selectedImages = this.getSelectedImages(); if (selectedImages.length === 0) { wx.showToast({ title: \u0026#39;请先选择图片\u0026#39;, icon: \u0026#39;none\u0026#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 \u0026lt; 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: \u0026#39;压缩保存失败\u0026#39;, icon: \u0026#39;none\u0026#39; }); } }, /** * 删除选中图片 */ deleteSelected() { const selectedImages = this.getSelectedImages(); if (selectedImages.length === 0) { wx.showToast({ title: \u0026#39;请先选择要删除的图片\u0026#39;, icon: \u0026#39;none\u0026#39; }); return; } wx.showModal({ title: \u0026#39;确认删除\u0026#39;, content: `确定要删除选中的${selectedImages.length}张图片吗？`, success: (res) =\u0026gt; { if (res.confirm) { const remainingImages = this.data.imageList.filter(item =\u0026gt; !item.selected); this.setData({ imageList: remainingImages, saveResult: `已删除${selectedImages.length}张图片` }); wx.showToast({ title: \u0026#39;删除成功\u0026#39;, icon: \u0026#39;success\u0026#39; }); } } }); }, /** * 清空所有图片 */ clearAllImages() { if (this.data.imageList.length === 0) { wx.showToast({ title: \u0026#39;没有图片可清空\u0026#39;, icon: \u0026#39;none\u0026#39; }); return; } wx.showModal({ title: \u0026#39;确认清空\u0026#39;, content: \u0026#39;确定要清空所有图片吗？\u0026#39;, success: (res) =\u0026gt; { if (res.confirm) { this.setData({ imageList: [], saveResult: \u0026#39;已清空所有图片\u0026#39; }); wx.showToast({ title: \u0026#39;清空成功\u0026#39;, icon: \u0026#39;success\u0026#39; }); } } }); }, /** * 分享图片 */ onShareAppMessage() { const selectedImages = this.getSelectedImages(); if (selectedImages.length \u0026gt; 0) { return { title: `分享${selectedImages.length}张图片`, path: \u0026#39;/pages/album/album\u0026#39;, imageUrl: selectedImages[0].thumbTempFilePath }; } return { title: \u0026#39;分享我的相册\u0026#39;, path: \u0026#39;/pages/album/album\u0026#39; }; }, /** * 下拉刷新 */ onPullDownRefresh() { console.log(\u0026#39;下拉刷新\u0026#39;); // 模拟刷新操作 setTimeout(() =\u0026gt; { this.setData({ saveResult: \u0026#39;刷新完成\u0026#39; }); wx.stopPullDownRefresh(); wx.showToast({ title: \u0026#39;刷新成功\u0026#39;, icon: \u0026#39;success\u0026#39; }); }, 1000); }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onReachBottom() { console.log(\u0026#39;上拉加载更多\u0026#39;); // 可以在这里实现加载更多图片的逻辑 }, /** * 用户点击右上角分享 */ onShareTimeline() { return { title: \u0026#39;我的小程序相册\u0026#39;, imageUrl: \u0026#39;/images/share.jpg\u0026#39; }; }, /** * 错误处理统一函数 */ handleError(error, operation = \u0026#39;操作\u0026#39;) { console.error(`${operation}失败:`, error); wx.showToast({ title: `${operation}失败`, icon: \u0026#39;none\u0026#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}加载失败`); // 可以在这里设置默认图片或重试逻辑 } }); 这是一个 WXML 文件：\n\u0026lt;!-- pages/album/album.wxml --\u0026gt; \u0026lt;view class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;!-- 操作按钮区域 --\u0026gt; \u0026lt;view class=\u0026#34;action-buttons\u0026#34;\u0026gt; \u0026lt;button bindtap=\u0026#34;chooseFromAlbum\u0026#34;\u0026gt;选择相册\u0026lt;/button\u0026gt; \u0026lt;button bindtap=\u0026#34;takePhoto\u0026#34;\u0026gt;拍照\u0026lt;/button\u0026gt; \u0026lt;button bindtap=\u0026#34;toggleSelectAll\u0026#34;\u0026gt;全选/取消\u0026lt;/button\u0026gt; \u0026lt;button bindtap=\u0026#34;saveSelectedToAlbum\u0026#34;\u0026gt;保存选中\u0026lt;/button\u0026gt; \u0026lt;button bindtap=\u0026#34;compressAndSaveSelected\u0026#34;\u0026gt;压缩保存\u0026lt;/button\u0026gt; \u0026lt;button bindtap=\u0026#34;deleteSelected\u0026#34;\u0026gt;删除选中\u0026lt;/button\u0026gt; \u0026lt;button bindtap=\u0026#34;clearAllImages\u0026#34;\u0026gt;清空全部\u0026lt;/button\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 状态显示 --\u0026gt; \u0026lt;view class=\u0026#34;status-info\u0026#34;\u0026gt; \u0026lt;text\u0026gt;已选择: {{imageList.length}} 张图片\u0026lt;/text\u0026gt; \u0026lt;text\u0026gt;选中: {{getSelectedImages().length}} 张\u0026lt;/text\u0026gt; \u0026lt;text\u0026gt;{{saveResult}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 图片列表 --\u0026gt; \u0026lt;view class=\u0026#34;image-grid\u0026#34;\u0026gt; \u0026lt;view wx:for=\u0026#34;{{imageList}}\u0026#34; wx:key=\u0026#34;id\u0026#34; class=\u0026#34;image-item {{item.selected ? \u0026#39;selected\u0026#39; : \u0026#39;\u0026#39;}}\u0026#34; bindtap=\u0026#34;toggleSelect\u0026#34; data-index=\u0026#34;{{index}}\u0026#34; \u0026gt; \u0026lt;image src=\u0026#34;{{item.thumbTempFilePath}}\u0026#34; mode=\u0026#34;aspectFill\u0026#34; bindload=\u0026#34;onImageLoad\u0026#34; binderror=\u0026#34;onImageError\u0026#34; data-index=\u0026#34;{{index}}\u0026#34; bindtap=\u0026#34;previewImage\u0026#34; /\u0026gt; \u0026lt;view class=\u0026#34;image-mask\u0026#34; wx:if=\u0026#34;{{item.selected}}\u0026#34;\u0026gt;✓\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 加载指示器 --\u0026gt; \u0026lt;view wx:if=\u0026#34;{{loading}}\u0026#34; class=\u0026#34;loading\u0026#34;\u0026gt; \u0026lt;text\u0026gt;处理中...\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 主要功能说明\n​ 图片选择功能 ​ chooseFromAlbum(): 从相册选择图片\ntakePhoto(): 拍照获取图片\n支持多选（最多 9 张）\n错误处理和权限检查\n​ 图片管理功能 ​ toggleSelect(): 选择/取消选择单张图片\ntoggleSelectAll(): 全选/取消全选\ndeleteSelected(): 删除选中图片\nclearAllImages(): 清空所有图片\n​ 图片保存功能 ​ saveSelectedToAlbum(): 保存选中图片到相册\ncompressAndSaveSelected(): 压缩后保存\n批量保存，支持进度显示\n详细的错误处理和结果反馈\n​ 图片处理功能 ​ previewImage(): 预览大图\ncompressImage(): 图片压缩\ngetImageInfo(): 获取图片信息\n​ 权限管理 ​ 自动检查相册权限\n引导用户授权\n友好的错误提示\n​ 用户体验优化 ​ 加载状态显示\n操作结果反馈\n错误处理机制\n性能优化（懒加载等）\n这个示例提供了完整的相册操作功能，可以根据实际需求进行裁剪和扩展。\n通关密语：相册\n","permalink":"https://blog.91demo.top/wiki/mp-image.html","summary":"\u003ch1 id=\"小程序使用相册\"\u003e小程序使用相册\u003c/h1\u003e\n\u003cp\u003e微信小程序提供了丰富的相册操作 API，允许开发者访问用户的相册、拍照、选择图片、保存图片等功能。这些功能主要通过 wx.chooseMedia、wx.saveImageToPhotosAlbum 等 API 实现。\u003c/p\u003e\n\u003cp\u003e下面是使用的一些示例：\u003c/p\u003e\n\u003cp\u003e1，定义变量\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  data: {\n    // 选择的图片列表\n    imageList: [],\n    // 当前选中的图片索引\n    currentIndex: 0,\n    // 是否显示预览\n    showPreview: false,\n    // 加载状态\n    loading: false,\n    // 保存结果提示\n    saveResult: \u0026#39;\u0026#39;\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  onLoad() {\n    console.log(\u0026#39;相册页面加载\u0026#39;);\n    this.checkAlbumPermission();\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 检查相册权限\n   */\n  checkAlbumPermission() {\n    wx.getSetting({\n      success: (res) =\u0026gt; {\n        if (!res.authSetting[\u0026#39;scope.writePhotosAlbum\u0026#39;]) {\n          // 如果没有相册权限，提示用户授权\n          this.requestAlbumPermission();\n        }\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 请求相册权限\n   */\n  requestAlbumPermission() {\n    wx.authorize({\n      scope: \u0026#39;scope.writePhotosAlbum\u0026#39;,\n      success: () =\u0026gt; {\n        console.log(\u0026#39;相册权限授权成功\u0026#39;);\n      },\n      fail: () =\u0026gt; {\n        console.log(\u0026#39;相册权限授权失败\u0026#39;);\n        this.showPermissionGuide();\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 显示权限引导\n   */\n  showPermissionGuide() {\n    wx.showModal({\n      title: \u0026#39;权限提示\u0026#39;,\n      content: \u0026#39;需要相册权限才能保存图片，请在设置中开启权限\u0026#39;,\n      confirmText: \u0026#39;去设置\u0026#39;,\n      success: (res) =\u0026gt; {\n        if (res.confirm) {\n          wx.openSetting({\n            success: (res) =\u0026gt; {\n              console.log(\u0026#39;打开设置成功\u0026#39;, res);\n            }\n          });\n        }\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 选择相册图片\n   */\n  chooseFromAlbum() {\n    wx.chooseMedia({\n      count: 9, // 最多选择9张\n      mediaType: [\u0026#39;image\u0026#39;], // 只选择图片\n      sourceType: [\u0026#39;album\u0026#39;], // 从相册选择\n      maxDuration: 30,\n      camera: \u0026#39;back\u0026#39;,\n      success: (res) =\u0026gt; {\n        console.log(\u0026#39;选择图片成功\u0026#39;, res);\n\n        const tempFiles = res.tempFiles;\n        const newImageList = tempFiles.map((file, index) =\u0026gt; ({\n          tempFilePath: file.tempFilePath,\n          size: file.size,\n          width: file.width,\n          height: file.height,\n          thumbTempFilePath: file.thumbTempFilePath,\n          id: Date.now() + index, // 生成唯一ID\n          selected: false\n        }));\n\n        // 合并到现有列表\n        this.setData({\n          imageList: [...this.data.imageList, ...newImageList],\n          saveResult: \u0026#39;\u0026#39;\n        });\n\n        wx.showToast({\n          title: `成功选择${newImageList.length}张图片`,\n          icon: \u0026#39;success\u0026#39;,\n          duration: 2000\n        });\n      },\n      fail: (err) =\u0026gt; {\n        console.error(\u0026#39;选择图片失败\u0026#39;, err);\n        this.handleChooseError(err);\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 拍照并选择\n   */\n  takePhoto() {\n    wx.chooseMedia({\n      count: 1,\n      mediaType: [\u0026#39;image\u0026#39;],\n      sourceType: [\u0026#39;camera\u0026#39;], // 从相机拍照\n      camera: \u0026#39;back\u0026#39;,\n      success: (res) =\u0026gt; {\n        console.log(\u0026#39;拍照成功\u0026#39;, res);\n\n        const file = res.tempFiles[0];\n        const newImage = {\n          tempFilePath: file.tempFilePath,\n          size: file.size,\n          width: file.width,\n          height: file.height,\n          thumbTempFilePath: file.thumbTempFilePath,\n          id: Date.now(),\n          selected: false,\n          isFromCamera: true // 标记来自相机\n        };\n\n        this.setData({\n          imageList: [newImage, ...this.data.imageList],\n          saveResult: \u0026#39;\u0026#39;\n        });\n\n        wx.showToast({\n          title: \u0026#39;拍照成功\u0026#39;,\n          icon: \u0026#39;success\u0026#39;,\n          duration: 2000\n        });\n      },\n      fail: (err) =\u0026gt; {\n        console.error(\u0026#39;拍照失败\u0026#39;, err);\n        this.handleChooseError(err);\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 处理选择错误\n   */\n  handleChooseError(err) {\n    let errorMsg = \u0026#39;选择图片失败\u0026#39;;\n\n    switch (err.errMsg) {\n      case \u0026#39;chooseMedia:fail auth deny\u0026#39;:\n        errorMsg = \u0026#39;没有相册访问权限\u0026#39;;\n        break;\n      case \u0026#39;chooseMedia:fail cancel\u0026#39;:\n        errorMsg = \u0026#39;用户取消了选择\u0026#39;;\n        break;\n      case \u0026#39;chooseMedia:fail no media\u0026#39;:\n        errorMsg = \u0026#39;没有找到图片\u0026#39;;\n        break;\n      default:\n        errorMsg = err.errMsg || errorMsg;\n    }\n\n    wx.showToast({\n      title: errorMsg,\n      icon: \u0026#39;none\u0026#39;,\n      duration: 2000\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 预览图片\n   */\n  previewImage(e) {\n    const index = e.currentTarget.dataset.index;\n    const urls = this.data.imageList.map(item =\u0026gt; item.tempFilePath);\n\n    wx.previewImage({\n      current: urls[index],\n      urls: urls,\n      success: () =\u0026gt; {\n        console.log(\u0026#39;预览图片成功\u0026#39;);\n      },\n      fail: (err) =\u0026gt; {\n        console.error(\u0026#39;预览图片失败\u0026#39;, err);\n        wx.showToast({\n          title: \u0026#39;预览失败\u0026#39;,\n          icon: \u0026#39;none\u0026#39;\n        });\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 选择/取消选择图片\n   */\n  toggleSelect(e) {\n    const index = e.currentTarget.dataset.index;\n    const imageList = this.data.imageList.map((item, i) =\u0026gt; {\n      if (i === index) {\n        return { ...item, selected: !item.selected };\n      }\n      return item;\n    });\n\n    this.setData({ imageList });\n  },\n\n  /**\n   * 全选/取消全选\n   */\n  toggleSelectAll() {\n    const allSelected = this.data.imageList.every(item =\u0026gt; item.selected);\n    const imageList = this.data.imageList.map(item =\u0026gt; ({\n      ...item,\n      selected: !allSelected\n    }));\n\n    this.setData({ imageList });\n  },\n\n  /**\n   * 获取选中的图片\n   */\n  getSelectedImages() {\n    return this.data.imageList.filter(item =\u0026gt; item.selected);\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 保存选中图片到相册\n   */\n  saveSelectedToAlbum() {\n    const selectedImages = this.getSelectedImages();\n\n    if (selectedImages.length === 0) {\n      wx.showToast({\n        title: \u0026#39;请先选择图片\u0026#39;,\n        icon: \u0026#39;none\u0026#39;,\n        duration: 2000\n      });\n      return;\n    }\n\n    this.setData({ loading: true, saveResult: \u0026#39;\u0026#39; });\n\n    // 批量保存\n    this.batchSaveImages(selectedImages);\n  },\n\n  /**\n   * 批量保存图片\n   */\n  async batchSaveImages(images) {\n    let successCount = 0;\n    let failCount = 0;\n    const results = [];\n\n    for (let i = 0; i \u0026lt; images.length; i++) {\n      try {\n        await this.saveSingleImage(images[i]);\n        successCount++;\n        results.push({ index: i, success: true });\n      } catch (error) {\n        failCount++;\n        results.push({ index: i, success: false, error: error.message });\n      }\n\n      // 更新进度\n      this.setData({\n        saveResult: `保存进度: ${i + 1}/${images.length}`\n      });\n    }\n\n    this.setData({ loading: false });\n    this.showSaveResult(successCount, failCount, results);\n  },\n\n  /**\n   * 保存单张图片\n   */\n  saveSingleImage(image) {\n    return new Promise((resolve, reject) =\u0026gt; {\n      wx.saveImageToPhotosAlbum({\n        filePath: image.tempFilePath,\n        success: () =\u0026gt; {\n          console.log(\u0026#39;保存图片成功\u0026#39;, image.tempFilePath);\n          resolve();\n        },\n        fail: (err) =\u0026gt; {\n          console.error(\u0026#39;保存图片失败\u0026#39;, err);\n          reject(new Error(this.getSaveErrorMsg(err)));\n        }\n      });\n    });\n  },\n\n  /**\n   * 获取保存错误信息\n   */\n  getSaveErrorMsg(err) {\n    switch (err.errMsg) {\n      case \u0026#39;saveImageToPhotosAlbum:fail auth deny\u0026#39;:\n        return \u0026#39;没有相册写入权限\u0026#39;;\n      case \u0026#39;saveImageToPhotosAlbum:fail cancel\u0026#39;:\n        return \u0026#39;用户取消了保存\u0026#39;;\n      case \u0026#39;saveImageToPhotosAlbum:fail no image\u0026#39;:\n        return \u0026#39;图片文件不存在\u0026#39;;\n      default:\n        return err.errMsg || \u0026#39;保存失败\u0026#39;;\n    }\n  },\n\n  /**\n   * 显示保存结果\n   */\n  showSaveResult(successCount, failCount, results) {\n    let title = \u0026#39;\u0026#39;;\n    if (failCount === 0) {\n      title = `成功保存${successCount}张图片`;\n    } else if (successCount === 0) {\n      title = `保存失败，${failCount}张图片未保存`;\n    } else {\n      title = `成功${successCount}张，失败${failCount}张`;\n    }\n\n    this.setData({\n      saveResult: title\n    });\n\n    wx.showModal({\n      title: \u0026#39;保存结果\u0026#39;,\n      content: title,\n      showCancel: false,\n      success: () =\u0026gt; {\n        // 清空选择状态\n        const imageList = this.data.imageList.map(item =\u0026gt; ({\n          ...item,\n          selected: false\n        }));\n        this.setData({ imageList });\n      }\n    });\n\n    // 记录详细结果\n    console.log(\u0026#39;保存详细结果:\u0026#39;, results);\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 压缩图片\n   */\n  compressImage(imagePath) {\n    return new Promise((resolve, reject) =\u0026gt; {\n      wx.compressImage({\n        src: imagePath,\n        quality: 80, // 压缩质量 0-100\n        success: (res) =\u0026gt; {\n          console.log(\u0026#39;图片压缩成功\u0026#39;, res);\n          resolve(res.tempFilePath);\n        },\n        fail: (err) =\u0026gt; {\n          console.error(\u0026#39;图片压缩失败\u0026#39;, err);\n          reject(err);\n        }\n      });\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\n  /**\n   * 获取图片信息\n   */\n  getImageInfo(imagePath) {\n    return new Promise((resolve, reject) =\u0026gt; {\n      wx.getImageInfo({\n        src: imagePath,\n        success: (res) =\u0026gt; {\n          console.log(\u0026#39;图片信息获取成功\u0026#39;, res);\n          resolve(res);\n        },\n        fail: (err) =\u0026gt; {\n          console.error(\u0026#39;图片信息获取失败\u0026#39;, err);\n          reject(err);\n        }\n      });\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 批量压缩并保存\n   */\n  async compressAndSaveSelected() {\n    const selectedImages = this.getSelectedImages();\n\n    if (selectedImages.length === 0) {\n      wx.showToast({\n        title: \u0026#39;请先选择图片\u0026#39;,\n        icon: \u0026#39;none\u0026#39;\n      });\n      return;\n    }\n\n    this.setData({ loading: true });\n\n    try {\n      // 先压缩所有图片\n      const compressedPaths = [];\n      for (const image of selectedImages) {\n        const compressedPath = await this.compressImage(image.tempFilePath);\n        compressedPaths.push(compressedPath);\n      }\n\n      // 保存压缩后的图片\n      for (let i = 0; i \u0026lt; compressedPaths.length; i++) {\n        await this.saveSingleImage({ tempFilePath: compressedPaths[i] });\n\n        this.setData({\n          saveResult: `压缩保存进度: ${i + 1}/${compressedPaths.length}`\n        });\n      }\n\n      this.setData({ loading: false });\n      this.showSaveResult(compressedPaths.length, 0, []);\n\n    } catch (error) {\n      this.setData({ loading: false });\n      wx.showToast({\n        title: \u0026#39;压缩保存失败\u0026#39;,\n        icon: \u0026#39;none\u0026#39;\n      });\n    }\n  },\n\n  /**\n   * 删除选中图片\n   */\n  deleteSelected() {\n    const selectedImages = this.getSelectedImages();\n\n    if (selectedImages.length === 0) {\n      wx.showToast({\n        title: \u0026#39;请先选择要删除的图片\u0026#39;,\n        icon: \u0026#39;none\u0026#39;\n      });\n      return;\n    }\n\n    wx.showModal({\n      title: \u0026#39;确认删除\u0026#39;,\n      content: `确定要删除选中的${selectedImages.length}张图片吗？`,\n      success: (res) =\u0026gt; {\n        if (res.confirm) {\n          const remainingImages = this.data.imageList.filter(item =\u0026gt; !item.selected);\n          this.setData({\n            imageList: remainingImages,\n            saveResult: `已删除${selectedImages.length}张图片`\n          });\n\n          wx.showToast({\n            title: \u0026#39;删除成功\u0026#39;,\n            icon: \u0026#39;success\u0026#39;\n          });\n        }\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\n  /**\n   * 清空所有图片\n   */\n  clearAllImages() {\n    if (this.data.imageList.length === 0) {\n      wx.showToast({\n        title: \u0026#39;没有图片可清空\u0026#39;,\n        icon: \u0026#39;none\u0026#39;\n      });\n      return;\n    }\n\n    wx.showModal({\n      title: \u0026#39;确认清空\u0026#39;,\n      content: \u0026#39;确定要清空所有图片吗？\u0026#39;,\n      success: (res) =\u0026gt; {\n        if (res.confirm) {\n          this.setData({\n            imageList: [],\n            saveResult: \u0026#39;已清空所有图片\u0026#39;\n          });\n\n          wx.showToast({\n            title: \u0026#39;清空成功\u0026#39;,\n            icon: \u0026#39;success\u0026#39;\n          });\n        }\n      }\n    });\n  },\n\u003c/code\u003e\u003c/pre\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e  /**\n   * 分享图片\n   */\n  onShareAppMessage() {\n    const selectedImages = this.getSelectedImages();\n\n    if (selectedImages.length \u0026gt; 0) {\n      return {\n        title: `分享${selectedImages.length}张图片`,\n        path: \u0026#39;/pages/album/album\u0026#39;,\n        imageUrl: selectedImages[0].thumbTempFilePath\n      };\n    }\n\n    return {\n      title: \u0026#39;分享我的相册\u0026#39;,\n      path: \u0026#39;/pages/album/album\u0026#39;\n    };\n  },\n\n  /**\n   * 下拉刷新\n   */\n  onPullDownRefresh() {\n    console.log(\u0026#39;下拉刷新\u0026#39;);\n\n    // 模拟刷新操作\n    setTimeout(() =\u0026gt; {\n      this.setData({\n        saveResult: \u0026#39;刷新完成\u0026#39;\n      });\n      wx.stopPullDownRefresh();\n\n      wx.showToast({\n        title: \u0026#39;刷新成功\u0026#39;,\n        icon: \u0026#39;success\u0026#39;\n      });\n    }, 1000);\n  },\n\n  /**\n   * 页面相关事件处理函数--监听用户下拉动作\n   */\n  onReachBottom() {\n    console.log(\u0026#39;上拉加载更多\u0026#39;);\n    // 可以在这里实现加载更多图片的逻辑\n  },\n\n  /**\n   * 用户点击右上角分享\n   */\n  onShareTimeline() {\n    return {\n      title: \u0026#39;我的小程序相册\u0026#39;,\n      imageUrl: \u0026#39;/images/share.jpg\u0026#39;\n    };\n  },\n\n  /**\n   * 错误处理统一函数\n   */\n  handleError(error, operation = \u0026#39;操作\u0026#39;) {\n    console.error(`${operation}失败:`, error);\n\n    wx.showToast({\n      title: `${operation}失败`,\n      icon: \u0026#39;none\u0026#39;,\n      duration: 2000\n    });\n  },\n\n  /**\n   * 性能优化：图片懒加载\n   */\n  onImageLoad(e) {\n    const index = e.currentTarget.dataset.index;\n    console.log(`图片${index}加载完成`);\n\n    // 可以在这里添加图片加载完成的处理逻辑\n  },\n\n  /**\n   * 性能优化：图片加载失败\n   */\n  onImageError(e) {\n    const index = e.currentTarget.dataset.index;\n    console.error(`图片${index}加载失败`);\n\n    // 可以在这里设置默认图片或重试逻辑\n  }\n});\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这是一个 WXML 文件：\u003c/p\u003e","title":"在小程序中使用相册"},{"content":"在处理小程序文章索引 JSON 文件时，发现了这个好工具 yq，特分享给大家。\nyq 是一个轻量级、命令行驱动的文件处理工具，专门用于处理 YAML、JSON、XML 等结构化数据格式。它类似于著名的 jq（专门处理 JSON 的工具），但扩展了对 YAML 的原生支持，并能够处理多种文件格式之间的转换。\nyq 采用 Go 语言编写，具有跨平台特性，可以在 Linux、macOS 和 Windows 系统上运行。它的核心功能包括查询、过滤、修改和转换结构化数据文件。\nyq 在大多数 Linux 发行版可以通过包管理器安装：\n# Ubuntu/Debian sudo apt install yq # CentOS/RHEL sudo yum install yq # macOS (使用 Homebrew) brew install yq 或者从 Github 发布页面下载预编译的二进制文件。\nyq 常用于：\n配置文件处理，例如在 DevOps 和云原生环境中处理 YAML 等配置文件。 数据转换，在不同格式（YAML/JSON/XML/CSV）之间进行转换 数据提取，从复杂数据结构中提取特定字段或值 批量修改，对多个文件中的特定字段进行统一修改 自动化脚本，在 shell 脚本中处理结构化数据 数据验证，检查配置文件是否符合特定结构或值要求 下面是一些使用示例：\n1，查询 YAML 文件\n假设我们有一个 config.yaml 文件：\nserver: port: 8080 host: \u0026#34;localhost\u0026#34; database: name: \u0026#34;test_db\u0026#34; user: \u0026#34;admin\u0026#34; 我们要获取服务器端口：\nyq \u0026#39;.server.port\u0026#39; config.yaml # 输出: 8080 2，格式转换\n将 YAML 转换为 JSON：\nyq -o=json config.yaml 输出： { \u0026#34;server\u0026#34;: { \u0026#34;port\u0026#34;: 8080, \u0026#34;host\u0026#34;: \u0026#34;localhost\u0026#34; }, \u0026#34;database\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;test_db\u0026#34;, \u0026#34;user\u0026#34;: \u0026#34;admin\u0026#34; } } 3，修改文件内容\n将端口修改为 9090：\nyq -i \u0026#39;.server.port = 9090\u0026#39; config.yaml 4，复杂查询与过滤\n我们有一个更复杂的文件 services.yaml：\nservices: - name: \u0026#34;web\u0026#34; replicas: 3 env: \u0026#34;production\u0026#34; - name: \u0026#34;db\u0026#34; replicas: 1 env: \u0026#34;production\u0026#34; - name: \u0026#34;cache\u0026#34; replicas: 2 env: \u0026#34;staging\u0026#34; 获取所有生产环境的服务名称：\nyq \u0026#39;.services[] | select(.env == \u0026#34;production\u0026#34;) | .name\u0026#39; services.yaml # 输出: # web # db 5，多文件操作\n批量修改多个 YAML 文件中的镜像标签：\nfor file in *.yaml; do yq -i \u0026#39;.image.tag = \u0026#34;v1.2.0\u0026#34;\u0026#39; \u0026#34;$file\u0026#34; done 6，合并 YAML 文件\n将两个 YAML 文件合并：\nyq eval-all \u0026#39;select(fileIndex == 0) * select(fileIndex == 1)\u0026#39; file1.yaml file2.yaml \u0026gt; merged.yaml 7，查询 JSON 文件\n假设有一个 user.json 文件：\n{ \u0026#34;id\u0026#34;: 12345, \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;alice@example.com\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;street\u0026#34;: \u0026#34;123 Main St\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;New York\u0026#34;, \u0026#34;zip\u0026#34;: \u0026#34;10001\u0026#34; }, \u0026#34;hobbies\u0026#34;: [\u0026#34;reading\u0026#34;, \u0026#34;hiking\u0026#34;, \u0026#34;photography\u0026#34;] } 我们可以这样查询：\n# 获取用户姓名 yq \u0026#39;.name\u0026#39; user.json # 输出: Alice # 获取城市信息 yq \u0026#39;.address.city\u0026#39; user.json # 输出: New York # 获取第一个爱好 yq \u0026#39;.hobbies[0]\u0026#39; user.json # 输出: reading # 获取所有爱好 yq \u0026#39;.hobbies[]\u0026#39; user.json # 输出: # reading # hiking # photography # 查询某个爱好，使用管道过滤 yq \u0026#39;.hobbies[] | select(. == \u0026#34;hiking\u0026#34;)\u0026#39; user.json # 输出: hiking 8，修改 JSON 文件\n# 修改电子邮件地址 yq -i \u0026#39;.email = \u0026#34;new_email@example.com\u0026#34;\u0026#39; user.json # 添加新爱好 yq -i \u0026#39;.hobbies += [\u0026#34;swimming\u0026#34;]\u0026#39; user.json # 删除字段 yq -i \u0026#39;del(.address.zip)\u0026#39; user.json 9，JSON 格式转换\nJSON 转 YAML\nyq -P user.json \u0026gt; user.yaml # 输出user.yaml id: 12345 name: Alice email: alice@example.com address: street: 123 Main St city: New York hobbies: - reading - hiking - photography 10，处理 JSON 数组\n假设有一个 users.json 文件：\n[ { \u0026#34;id\u0026#34;: 1, \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;active\u0026#34;: true }, { \u0026#34;id\u0026#34;: 2, \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;, \u0026#34;active\u0026#34;: false }, { \u0026#34;id\u0026#34;: 3, \u0026#34;name\u0026#34;: \u0026#34;Charlie\u0026#34;, \u0026#34;active\u0026#34;: true } ] 我们可以这样操作：\n# 获取所有活跃用户的名字 yq \u0026#39;.[] | select(.active == true) | .name\u0026#39; users.json # 输出: # Alice # Charlie # 获取ID 为2的用户 yq \u0026#39;.[] | select(.id == 2)\u0026#39; users.json # 输出: # { # \u0026#34;id\u0026#34;: 2, # \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;, # \u0026#34;active\u0026#34;: false # } 11，处理 JSON 文件生成新文件\n# 美化压缩的JSON yq -P compressed.json \u0026gt; pretty.json # 提取特定字段创建新的JSON yq \u0026#39;{name: .name, email: .email}\u0026#39; user.json \u0026gt; contact_info.json # 从JSON数组创建CSV yq -o=csv \u0026#39;.[] | [.id, .name, .active]\u0026#39; users.json # 输出: # 1,Alice,true # 2,Bob,false # 3,Charlie,true yq 还有更多的其它用法，请详细的内容请查看 Github 地址：\nhttps://github.com/mikefarah/yq\nyq 是现代开发者和系统管理员工具箱中不可或缺的工具，特别是在云原生和 DevOps 环境中。它简化了配置文件处理，使得在自动化脚本中操作结构化数据变得轻而易举。\n","permalink":"https://blog.91demo.top/wiki/yqtool.html","summary":"\u003cp\u003e在处理小程序文章索引 JSON 文件时，发现了这个好工具 yq，特分享给大家。\u003c/p\u003e\n\u003cp\u003eyq 是一个轻量级、命令行驱动的文件处理工具，专门用于处理 YAML、JSON、XML 等结构化数据格式。它类似于著名的 jq（专门处理 JSON 的工具），但扩展了对 YAML 的原生支持，并能够处理多种文件格式之间的转换。\u003c/p\u003e\n\u003cp\u003eyq 采用 Go 语言编写，具有跨平台特性，可以在 Linux、macOS 和 Windows 系统上运行。它的核心功能包括查询、过滤、修改和转换结构化数据文件。\u003c/p\u003e\n\u003cp\u003eyq 在大多数 Linux 发行版可以通过包管理器安装：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# Ubuntu/Debian\nsudo apt install yq\n\n# CentOS/RHEL\nsudo yum install yq\n\n# macOS (使用 Homebrew)\nbrew install yq\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e或者从 Github 发布页面下载预编译的二进制文件。\u003c/p\u003e\n\u003cp\u003eyq 常用于：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e配置文件处理，例如在 DevOps 和云原生环境中处理 YAML 等配置文件。\u003c/li\u003e\n\u003cli\u003e数据转换，在不同格式（YAML/JSON/XML/CSV）之间进行转换\u003c/li\u003e\n\u003cli\u003e数据提取，从复杂数据结构中提取特定字段或值\u003c/li\u003e\n\u003cli\u003e批量修改，对多个文件中的特定字段进行统一修改\u003c/li\u003e\n\u003cli\u003e自动化脚本，在 shell 脚本中处理结构化数据\u003c/li\u003e\n\u003cli\u003e数据验证，检查配置文件是否符合特定结构或值要求\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e下面是一些使用示例：\u003c/p\u003e\n\u003cp\u003e1，查询 YAML 文件\u003c/p\u003e\n\u003cp\u003e假设我们有一个 config.yaml 文件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eserver:\n  port: 8080\n  host: \u0026#34;localhost\u0026#34;\ndatabase:\n  name: \u0026#34;test_db\u0026#34;\n  user: \u0026#34;admin\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e我们要获取服务器端口：\u003c/p\u003e","title":"高效文件处理工具 yq"},{"content":"起点：一个技术人的展示橱窗 在我学习了小程序之后，作为技术人，我想把自己的知识使用小程序来实现，这一切始于一个朴素的想法：我需要一个地方，来系统性地展示和沉淀我的技术知识点。\n于是，“豆子小豆子碎片”小程序第一个版本诞生了。它是一个纯粹的技术“展示橱窗”，记录着我的学习与思考。\n然而，我很快发现，一个单向输出的“橱窗”，即使内容再好，也缺乏持续的活力与互动。用户来了又走，我作为唯一的创作者，也感到动力有限。我意识到，如果知识只是被陈列，而无法流动、无法创造连接、无法衡量价值，它的影响力终究是有限的。\n进化：引入“豆子点数”，点燃价值引擎 我想到了激励，为了自己，也方便他人。为了让知识流动起来，我构思了系统的核心：“豆子点数”激励系统。这不再是一个静态的展示柜，而是一个动态的价值交换市场。\n我的目标，是将每一次阅读、每一次创作、每一次互动，都转化为可积累、可量化的数字资产——“豆子”。而“豆子”的终极锚点，是能兑换为人民币，这是我最初的想法。那我的收益又从哪里来呢？广告收入，用户需要查看文章，需要消耗豆子点数，当点数不足时，需要观看激励广告。\n这个链路就是，创作者上传文章-\u0026gt;用户看广告获取豆子点数-\u0026gt;平台通过广告获取收益-\u0026gt;用户浏览文章消耗豆子点数，创作者获取豆子点数-\u0026gt;创作者兑换豆子点数。\n升华：从个人项目到创作者平台 真正的蜕变，在于我决定将这个系统平台化、开放化。我不再是唯一的创作者，而是平台的搭建者和规则制定者。\n平台的三角模型：\n我（平台方）：开发了小程序和后台API，文章上传客户端，负责维护系统、制定规则、对接广告，确保整个经济体稳定运行。\n创作者：任何技术爱好者都可以通过API或客户端，向平台上传自己的技术文章，成为内容供应者。\n读者：所有用户，来此免费消费这些技术内容。\n它的核心运转机制：基于广告分成的价值循环。这是整个构想最精妙的部分，它形成了一个完美的闭环：\n创作激励：读者阅读文章时，需要观看一则广告。这则广告产生的收益，将被平台折算成 “豆子点数”​ ，奖励给该文章的上传者（创作者）。创作者贡献内容，平台通过广告将读者的注意力变现，并回馈给创作者。\n内容分层与溢价机制：创作者如果对自己的文章质量有足够信心，可以为其 “加锁”​ 。一旦文章被“加锁”，读者阅读时需要观看的广告（或广告价值）会更高，随之产生、并奖励给创作者的“豆子点数”也会更多。\n这形成了强大的自我筛选和品质激励：优质内容会主动标示自己，并寻求更高回报；读者为高质量内容付出稍多的注意力（看更长或更多的广告），也觉合理；平台则通过优质内容吸引更多流量，形成良性循环。\n互动增强回路：除了阅读广告产生的点数，“点赞”等互动行为也可以被设计为额外的奖励来源（例如，点赞能为创作者带来少量点数）。这鼓励创作者不仅追求流量，更追求内容带来的认同与共鸣，促进社区氛围。\n完整的经济闭环：\n创作者​为获取“豆子”（可换钱），积极生产内容，优质内容会“加锁”以求更高收益。\n读者​为获取免费知识，付出注意力观看广告。\n广告商​的投放费用流入平台。\n平台​将广告收益的大部分，以“豆子点数”形式分配给创作者（特别是“加锁”的优质内容获得更高权重）。\n创作者​积累“豆子”，最终从平台兑换人民币，实现知识变现。\n小程序本身，则纯粹作为这个生态的“展示窗口”和“交互界面”，负责优雅、流畅地向终端用户呈现这一切。\n愿景：一个由技术人共建、共享的良性生态 这个构想的终极目标，不再是展示我个人的技术，而是搭建一个基于价值认同的、可持续的开发者内容生态。对创作者而言，这是一个轻量级、可直接获得正反馈和物质激励的技术博客平台。对读者而言，这是一个能免费（以注意力为代价）接触到众多同行一线思考和经验的宝库。对整个社区而言，“豆子点数”如同血液，驱动着优质内容的生产、筛选与流通。\n这看起来非常美好，仿佛是一个微小的公众号生态缩影。然而，当我真正试运行之后，才发现这个循环其实非常脆弱，很容易就会崩塌。\n原因主要有这么几点：\n第一，小程序的广告收益实在太低，我根本无法维持当初设想的“1000豆子兑换1元人民币”的固定汇率。\n第二，小程序广告的收益算法并不透明，我很难设计出公平又可持续的豆子折算比例。\n第三，用户对文章内容并不那么感兴趣，阅读量和互动始终起不来。\n第四，创作者那边也缺乏持续上传文章的动力，早期激励难以覆盖他们的创作投入。\n第五，从长远来看，小程序平台本身也在政策上存在不确定性，后续版本很可能会限制甚至禁止用户自定义内容的上传。\n为了把这个想法落地，我在技术实现上是下了功夫的。我专门开发了一个内容上传平台，它拥有清晰的公开API和创作者使用的客户端界面。当创作者提交文章后，会进入审核流程。为了给创作者即时的反馈，我还额外集成了MQTT消息服务——一旦文章审核通过（或退回），系统就会通过MQTT通道向创作者客户端推送实时通知，让整个流程更顺畅、更透明。\n就这样，尽管有这些技术上的准备和实现，这个想法在运行一段时间后，还是慢慢沉寂了。\n虽然它没有成功，但却给我留下了一笔很大的财富——我意识到，我过去所做的，从技术实现上看是完整的，但从模式上看，更多是一种对现有逻辑的复制，而非真正的创新。\n但这依然是一次宝贵的尝试。它让我完整地走完了一个“想法-开发-运营-验证”的闭环，其中的技术架构与产品反思，是单纯复制项目所无法获得的。\n最后介绍一下识别码。文章上传和维护，我们是通过命令行工具来完成的。没有集成到小程序中，一是因为个人小程序不容易过审，二是在小程序中编制 Markdown 文档非常麻烦，因为没有提供 API 选择 MARKDOWN 文档，因此上传 Markdown 文档也不能实现。我自己实现了命令行工具可以管理文章。也提供开放接口 API 供用户自己实现工具上传。为了保护开放接口，我们需要用户认证。在小程序中实现了获取用户识别码的功能。可以获取到用户识别码，调用开放接口。\n用户识别码 我们的小程序只实现了文章的浏览和搜索。为了将整个应用完善，我们还需要文章上传，文章删除等功能。上一节，我们已经讲了一种解决方案。就是使用开放接口 API。\n开放接口因为是面向互联网的，所以任何人都能访问，为了保护我们的开放接口不让任意调用，我们需要添加认证。我们通过小程序的微信生态进行认证。只有用户能使用小程序，并能获取用户的 OPENID，我们才给用户提供识别码。\n用户识别码相当于用户的账号和密码，我们基于用户的 OPENID 生成识别码。识别码是项目中是唯一的，可识别的。识别码密钥是随机字符串。通过识别码和识别码密钥，我们可以用来识别用户，所以，请妥善保管你的识别码和识别码密钥。防止他人滥用。你还可以重置识别码密钥，让以前的识别码密钥失效。类似于用户账户中的修改密码。\n根据小程序的特性，我们还集成了广告，结合广告，用户识别码更加的重要。我们使用豆子点数机制，来实现广告和用户识别码的结合。当用户观看广告时，获取豆子点数，当用户调用开放接口时，消耗豆子点数。用户的公开加锁文章被其他用户浏览时，可以获得豆子点数，并且提供文章的浏览量，提高文章的排序。\n增加一个 Tab 项，叫我的页面。我的页面只有两个功能，观看广告获取豆子点数，获取我的识别码。现在看看如何实现我的识别码。\n\u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form__bd\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form__text-area\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;weui-form__title\u0026#34;\u0026gt;我的识别码\u0026lt;/h2\u0026gt; \u0026lt;view class=\u0026#34;weui-form__desc\u0026#34;\u0026gt;识别码用于开放接口认证使用。请妥善保管识别码和密钥，不要将密钥告诉他人。\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-form__control-area\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cells__group weui-cells__group_form\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cells\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cell\u0026#34;\u0026gt; \u0026lt;text\u0026gt;我的识别码：{{icode}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-cell\u0026#34;\u0026gt; \u0026lt;text\u0026gt;我的密钥：{{isecret}}\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-form__ft\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-form__opr-area\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-btn weui-btn_primary\u0026#34; aria-role=\u0026#34;button\u0026#34; bind:tap=\u0026#34;getMyAppIcode\u0026#34;\u0026gt;获取识别码\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-btn weui-btn_warn\u0026#34; aria-role=\u0026#34;button\u0026#34; bind:tap=\u0026#34;resetAppIcode\u0026#34;\u0026gt;重置识别码密钥\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-btn weui-btn_default\u0026#34; aria-role=\u0026#34;button\u0026#34; bind:tap=\u0026#34;doCopy\u0026#34;\u0026gt;复制到剪切板\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 获取识别码和重置识别码的整体思路和逻辑与前面的文章类似，没有新的技术点。这里添加了剪切板功能，方便用户复制识别码。\n为了激励用户，我们把加锁的文章权重提高，当用户浏览加锁文章时，浏览量是普通文章的两倍。这样在排序时，会排到前面。\n集成广告 当小程序的用户量达到一定数量时，就可以开通流量主。集成小程序广告，就可以获得收益。\n为了增加一点收益，用于承担一些我的服务器费用支出。根据我的小程序特点，我集成了 3 类类型的广告。如果你喜欢我的文章，欢迎你浏览小程序观看广告。\n封面广告，这个没有技术含量，在小程序后台开通即可。\nBanner 广告，这个也没有技术含量，在界面中设置时注意和界面的其它元素不冲突，排版协调即可。\n激励视频广告，这个有一点技术含量。官方文档中说在 onLoad 方法中加载广告的初始化。我发现这样设置的话，界面会不流畅。特别是多次返回再进入，页面迟滞现象严重。\n为了优化，我也想了很多办法。最后的解决方法如下：\nloadAd() { const that = this; vAd = wx.createRewardedVideoAd({ adUnitId: \u0026#39;adunit-2ce6db3cb1e45a86\u0026#39;, }) vAd.onLoad(() =\u0026gt; { hasLoadAd = true }), vAd.onError((err) =\u0026gt; { console.error(\u0026#39;激励视频广告加载失败,\u0026#39;, err) }), vAd.onClose((res) =\u0026gt; { if (res \u0026amp;\u0026amp; res.isEnded) { that.doAdProfit(); } else { wx.showToast({ title: \u0026#39;没有获得点数哟！\u0026#39;, }) } }) }, playAd() { const that = this; wx.showLoading({ title: \u0026#39;加载广告中\u0026#39;, }) if (!hasLoadAd) { that.loadAd(); } // 用户触发广告后，显示激励视频广告 if (vAd) { vAd.show().then(() =\u0026gt; [ wx.hideLoading() ]).catch(() =\u0026gt; { // 失败重试 vAd.load() .then(() =\u0026gt; { wx.hideLoading() vAd.show() }) .catch(err =\u0026gt; { wx.hideLoading() wx.showToast({ title: \u0026#39;请重试一次\u0026#39;, }) console.error(\u0026#39;激励视频 广告显示失败\u0026#39;, err) }) }) } else { wx.hideLoading() wx.showToast({ title: \u0026#39;请稍后重试\u0026#39;, }) } }, 进入页面后，不加载广告，只有当用户主动点击广告按钮时，才加载广告。在加载广告的时候，给用户显示 Loading，增加用户的体验。如果已经加载了广告，就直接播放。如果加载广告失败，再重新拉取一次。还报错，就只能显示给用户错误了。\n通过上面的方法改进后，发现多次进入页面不会卡顿，发生页面迟滞现象。\n小程序开发部分基本就完成了，后续有新的功能或者优化再开新的文章。\n","permalink":"https://blog.91demo.top/wiki/v2.html","summary":"\u003ch2 id=\"起点一个技术人的展示橱窗\"\u003e起点：一个技术人的展示橱窗\u003c/h2\u003e\n\u003cp\u003e在我学习了小程序之后，作为技术人，我想把自己的知识使用小程序来实现，这一切始于一个朴素的想法：我需要一个地方，来系统性地展示和沉淀我的技术知识点。\u003c/p\u003e\n\u003cp\u003e于是，“豆子小豆子碎片”小程序第一个版本诞生了。它是一个纯粹的技术“展示橱窗”，记录着我的学习与思考。\u003c/p\u003e\n\u003cp\u003e然而，我很快发现，一个单向输出的“橱窗”，即使内容再好，也缺乏持续的活力与互动。用户来了又走，我作为唯一的创作者，也感到动力有限。我意识到，如果知识只是被陈列，而无法流动、无法创造连接、无法衡量价值，它的影响力终究是有限的。\u003c/p\u003e\n\u003ch2 id=\"进化引入豆子点数点燃价值引擎\"\u003e进化：引入“豆子点数”，点燃价值引擎\u003c/h2\u003e\n\u003cp\u003e我想到了激励，为了自己，也方便他人。为了让知识流动起来，我构思了系统的核心：“豆子点数”激励系统。这不再是一个静态的展示柜，而是一个动态的价值交换市场。\u003c/p\u003e\n\u003cp\u003e我的目标，是将每一次阅读、每一次创作、每一次互动，都转化为可积累、可量化的数字资产——“豆子”。而“豆子”的终极锚点，是能兑换为人民币，这是我最初的想法。那我的收益又从哪里来呢？广告收入，用户需要查看文章，需要消耗豆子点数，当点数不足时，需要观看激励广告。\u003c/p\u003e\n\u003cp\u003e这个链路就是，创作者上传文章-\u0026gt;用户看广告获取豆子点数-\u0026gt;平台通过广告获取收益-\u0026gt;用户浏览文章消耗豆子点数，创作者获取豆子点数-\u0026gt;创作者兑换豆子点数。\u003c/p\u003e\n\u003ch2 id=\"升华从个人项目到创作者平台\"\u003e升华：从个人项目到创作者平台\u003c/h2\u003e\n\u003cp\u003e真正的蜕变，在于我决定将这个系统平台化、开放化。我不再是唯一的创作者，而是平台的搭建者和规则制定者。\u003c/p\u003e\n\u003cp\u003e平台的三角模型：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e我（平台方）：开发了小程序和后台API，文章上传客户端，负责维护系统、制定规则、对接广告，确保整个经济体稳定运行。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e创作者：任何技术爱好者都可以通过API或客户端，向平台上传自己的技术文章，成为内容供应者。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e读者：所有用户，来此免费消费这些技术内容。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e它的核心运转机制：基于广告分成的价值循环。这是整个构想最精妙的部分，它形成了一个完美的闭环：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e创作激励：读者阅读文章时，需要观看一则广告。这则广告产生的收益，将被平台折算成 “豆子点数”​ ，奖励给该文章的上传者（创作者）。创作者贡献内容，平台通过广告将读者的注意力变现，并回馈给创作者。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e内容分层与溢价机制：创作者如果对自己的文章质量有足够信心，可以为其 “加锁”​ 。一旦文章被“加锁”，读者阅读时需要观看的广告（或广告价值）会更高，随之产生、并奖励给创作者的“豆子点数”也会更多。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e这形成了强大的自我筛选和品质激励：优质内容会主动标示自己，并寻求更高回报；读者为高质量内容付出稍多的注意力（看更长或更多的广告），也觉合理；平台则通过优质内容吸引更多流量，形成良性循环。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e互动增强回路：除了阅读广告产生的点数，“点赞”等互动行为也可以被设计为额外的奖励来源（例如，点赞能为创作者带来少量点数）。这鼓励创作者不仅追求流量，更追求内容带来的认同与共鸣，促进社区氛围。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e完整的经济闭环：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e创作者​为获取“豆子”（可换钱），积极生产内容，优质内容会“加锁”以求更高收益。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e读者​为获取免费知识，付出注意力观看广告。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e广告商​的投放费用流入平台。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e平台​将广告收益的大部分，以“豆子点数”形式分配给创作者（特别是“加锁”的优质内容获得更高权重）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e创作者​积累“豆子”，最终从平台兑换人民币，实现知识变现。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e小程序本身，则纯粹作为这个生态的“展示窗口”和“交互界面”，负责优雅、流畅地向终端用户呈现这一切。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"愿景一个由技术人共建共享的良性生态\"\u003e愿景：一个由技术人共建、共享的良性生态\u003c/h2\u003e\n\u003cp\u003e这个构想的终极目标，不再是展示我个人的技术，而是搭建一个基于价值认同的、可持续的开发者内容生态。对创作者而言，这是一个轻量级、可直接获得正反馈和物质激励的技术博客平台。对读者而言，这是一个能免费（以注意力为代价）接触到众多同行一线思考和经验的宝库。对整个社区而言，“豆子点数”如同血液，驱动着优质内容的生产、筛选与流通。\u003c/p\u003e\n\u003cp\u003e这看起来非常美好，仿佛是一个微小的公众号生态缩影。然而，当我真正试运行之后，才发现这个循环其实非常脆弱，很容易就会崩塌。\u003c/p\u003e\n\u003cp\u003e原因主要有这么几点：\u003c/p\u003e\n\u003cp\u003e第一，小程序的广告收益实在太低，我根本无法维持当初设想的“1000豆子兑换1元人民币”的固定汇率。\u003c/p\u003e\n\u003cp\u003e第二，小程序广告的收益算法并不透明，我很难设计出公平又可持续的豆子折算比例。\u003c/p\u003e\n\u003cp\u003e第三，用户对文章内容并不那么感兴趣，阅读量和互动始终起不来。\u003c/p\u003e\n\u003cp\u003e第四，创作者那边也缺乏持续上传文章的动力，早期激励难以覆盖他们的创作投入。\u003c/p\u003e\n\u003cp\u003e第五，从长远来看，小程序平台本身也在政策上存在不确定性，后续版本很可能会限制甚至禁止用户自定义内容的上传。\u003c/p\u003e\n\u003cp\u003e为了把这个想法落地，我在技术实现上是下了功夫的。我专门开发了一个内容上传平台，它拥有清晰的公开API和创作者使用的客户端界面。当创作者提交文章后，会进入审核流程。为了给创作者即时的反馈，我还额外集成了MQTT消息服务——一旦文章审核通过（或退回），系统就会通过MQTT通道向创作者客户端推送实时通知，让整个流程更顺畅、更透明。\u003c/p\u003e\n\u003cp\u003e就这样，尽管有这些技术上的准备和实现，这个想法在运行一段时间后，还是慢慢沉寂了。\u003c/p\u003e\n\u003cp\u003e虽然它没有成功，但却给我留下了一笔很大的财富——我意识到，我过去所做的，从技术实现上看是完整的，但从模式上看，更多是一种对现有逻辑的复制，而非真正的创新。\u003c/p\u003e\n\u003cp\u003e但这依然是一次宝贵的尝试。它让我完整地走完了一个“想法-开发-运营-验证”的闭环，其中的技术架构与产品反思，是单纯复制项目所无法获得的。\u003c/p\u003e\n\u003cp\u003e最后介绍一下识别码。文章上传和维护，我们是通过命令行工具来完成的。没有集成到小程序中，一是因为个人小程序不容易过审，二是在小程序中编制 Markdown 文档非常麻烦，因为没有提供 API 选择 MARKDOWN 文档，因此上传 Markdown 文档也不能实现。我自己实现了命令行工具可以管理文章。也提供开放接口 API 供用户自己实现工具上传。为了保护开放接口，我们需要用户认证。在小程序中实现了获取用户识别码的功能。可以获取到用户识别码，调用开放接口。\u003c/p\u003e\n\u003ch2 id=\"用户识别码\"\u003e用户识别码\u003c/h2\u003e\n\u003cp\u003e我们的小程序只实现了文章的浏览和搜索。为了将整个应用完善，我们还需要文章上传，文章删除等功能。上一节，我们已经讲了一种解决方案。就是使用开放接口 API。\u003c/p\u003e\n\u003cp\u003e开放接口因为是面向互联网的，所以任何人都能访问，为了保护我们的开放接口不让任意调用，我们需要添加认证。我们通过小程序的微信生态进行认证。只有用户能使用小程序，并能获取用户的 OPENID，我们才给用户提供识别码。\u003c/p\u003e\n\u003cp\u003e用户识别码相当于用户的账号和密码，我们基于用户的 OPENID 生成识别码。识别码是项目中是唯一的，可识别的。识别码密钥是随机字符串。通过识别码和识别码密钥，我们可以用来识别用户，所以，请妥善保管你的识别码和识别码密钥。防止他人滥用。你还可以重置识别码密钥，让以前的识别码密钥失效。类似于用户账户中的修改密码。\u003c/p\u003e\n\u003cp\u003e根据小程序的特性，我们还集成了广告，结合广告，用户识别码更加的重要。我们使用豆子点数机制，来实现广告和用户识别码的结合。当用户观看广告时，获取豆子点数，当用户调用开放接口时，消耗豆子点数。用户的公开加锁文章被其他用户浏览时，可以获得豆子点数，并且提供文章的浏览量，提高文章的排序。\u003c/p\u003e\n\u003cp\u003e增加一个 Tab 项，叫我的页面。我的页面只有两个功能，观看广告获取豆子点数，获取我的识别码。现在看看如何实现我的识别码。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt;\n  \u0026lt;view class=\u0026#34;weui-form\u0026#34;\u0026gt;\n    \u0026lt;view class=\u0026#34;weui-form__bd\u0026#34;\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-form__text-area\u0026#34;\u0026gt;\n        \u0026lt;h2 class=\u0026#34;weui-form__title\u0026#34;\u0026gt;我的识别码\u0026lt;/h2\u0026gt;\n        \u0026lt;view class=\u0026#34;weui-form__desc\u0026#34;\u0026gt;识别码用于开放接口认证使用。请妥善保管识别码和密钥，不要将密钥告诉他人。\u0026lt;/view\u0026gt;\n      \u0026lt;/view\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-form__control-area\u0026#34;\u0026gt;\n        \u0026lt;view class=\u0026#34;weui-cells__group weui-cells__group_form\u0026#34;\u0026gt;\n          \u0026lt;view class=\u0026#34;weui-cells\u0026#34;\u0026gt;\n            \u0026lt;view class=\u0026#34;weui-cell\u0026#34;\u0026gt;\n            \u0026lt;text\u0026gt;我的识别码：{{icode}}\u0026lt;/text\u0026gt;\n            \u0026lt;/view\u0026gt;\n            \u0026lt;view class=\u0026#34;weui-cell\u0026#34;\u0026gt;\n            \u0026lt;text\u0026gt;我的密钥：{{isecret}}\u0026lt;/text\u0026gt;\n            \u0026lt;/view\u0026gt;\n          \u0026lt;/view\u0026gt;\n        \u0026lt;/view\u0026gt;\n      \u0026lt;/view\u0026gt;\n    \u0026lt;/view\u0026gt;\n    \u0026lt;view class=\u0026#34;weui-form__ft\u0026#34;\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-form__opr-area\u0026#34;\u0026gt;\n        \u0026lt;view class=\u0026#34;weui-btn weui-btn_primary\u0026#34; aria-role=\u0026#34;button\u0026#34; bind:tap=\u0026#34;getMyAppIcode\u0026#34;\u0026gt;获取识别码\u0026lt;/view\u0026gt;\n        \u0026lt;view class=\u0026#34;weui-btn weui-btn_warn\u0026#34; aria-role=\u0026#34;button\u0026#34; bind:tap=\u0026#34;resetAppIcode\u0026#34;\u0026gt;重置识别码密钥\u0026lt;/view\u0026gt;\n        \u0026lt;view class=\u0026#34;weui-btn weui-btn_default\u0026#34; aria-role=\u0026#34;button\u0026#34; bind:tap=\u0026#34;doCopy\u0026#34;\u0026gt;复制到剪切板\u0026lt;/view\u0026gt;\n      \u0026lt;/view\u0026gt;\n    \u0026lt;/view\u0026gt;\n  \u0026lt;/view\u0026gt;\n\u0026lt;/view\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e获取识别码和重置识别码的整体思路和逻辑与前面的文章类似，没有新的技术点。这里添加了剪切板功能，方便用户复制识别码。\u003c/p\u003e","title":"豆子碎片小程序项目第二版"},{"content":"背景介绍 在我将ESP8266采集代码灌入之后，它已经可以实现自动采集按钮信号了。核心逻辑实现之后，我需要能够连接网络，因为网络是连接MQTT的前置条件。\n在连接之前，我需要将路由器的WIFI密码告知ESP8266芯片，这不同于Debug的时候，在代码内固定填写WIFI账号密码。我需要动态的获取并写入到ESP8266芯片。\n网络配置有很多种方式，我选择了SoftAP配网，虽然配置有点繁琐，但这是一种很可靠的配网方案，兼容性极高，不依赖手机硬件的特殊协议。\n准确来说，这不应该算是一个项目，但因为它具有通用性和一点实用性。我决定把它记录下来，可以方便地应用到我的其它项目中。\nSoft配网原理 初始化模式：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客户端的连接。\n以下代码实现了一个非常简单的Web服务，它监听/config接口并能够接收WIFI信息，以及接收之后可以本地存储，实现动态配置的功能。\n注意：当存储在本地之后，下次启动可以直接使用。而不需要重新配置。\n#include \u0026lt;ESP8266WiFi.h\u0026gt; #include \u0026lt;ESP8266WebServer.h\u0026gt; // 定义AP热点的名称 const char* ap_ssid = \u0026#34;ESP8266_Config_Device\u0026#34;; ESP8266WebServer server(80); void handleConfig() { if (server.hasArg(\u0026#34;ssid\u0026#34;) \u0026amp;\u0026amp; server.hasArg(\u0026#34;pass\u0026#34;)) { String target_ssid = server.arg(\u0026#34;ssid\u0026#34;); String target_pass = server.arg(\u0026#34;pass\u0026#34;); // 确保这里是 pass // 1. 先回复小程序，否则一旦开始连WiFi，AP就会失效，小程序收不到回复会报错 server.sendHeader(\u0026#34;Access-Control-Allow-Origin\u0026#34;, \u0026#34;*\u0026#34;); server.send(200, \u0026#34;text/plain\u0026#34;, \u0026#34;SUCCESS\u0026#34;); Serial.println(\u0026#34;配置信息已收到，准备连接...\u0026#34;); // 2. 延迟一下再连接，给 HTTP 响应留出传输时间 delay(1000); WiFi.begin(target_ssid.c_str(), target_pass.c_str()); int counter = 0; while (WiFi.status() != WL_CONNECTED \u0026amp;\u0026amp; counter \u0026lt; 20) { // 等待10秒 delay(500); Serial.print(\u0026#34;.\u0026#34;); counter++; } if (WiFi.status() == WL_CONNECTED) { Serial.println(\u0026#34;\\nWiFi连接成功!\u0026#34;); WiFi.mode(WIFI_STA); // 关闭AP模式，切换为STA模式 WiFi.setAutoConnect(true); WiFi.persist(true); // 将账号密码永久保存到Flash } else { Serial.println(\u0026#34;\\n连接失败，请检查账号密码\u0026#34;); // 保持AP模式，等待下次尝试 } } else { server.send(400, \u0026#34;text/plain\u0026#34;, \u0026#34;FAIL: Missing Params\u0026#34;); } } void setup() { Serial.begin(115200); // 设置为AP+STA模式 WiFi.mode(WIFI_AP_STA); WiFi.softAP(ap_ssid); Serial.println(\u0026#34;AP热点已启动，IP地址: \u0026#34; + WiFi.softAPIP().toString()); // 注册接口 server.on(\u0026#34;/config\u0026#34;, HTTP_POST, handleConfig); server.begin(); Serial.println(\u0026#34;HTTP服务器已启动\u0026#34;); } void loop() { server.handleClient(); // 如果连接成功，可以根据需要在这里处理业务逻辑 if (WiFi.status() == WL_CONNECTED) { static bool connected_msg = false; if (!connected_msg) { Serial.println(\u0026#34;设备已联网，IP: \u0026#34; + WiFi.localIP().toString()); connected_msg = true; } } } 参考上面的原理，设备上电之后，启动AP模式，接收配置，这个时候可以通过指示灯来显示状态。接收完毕后，可以正常连接到WIFI，则将WIFI配置信息保存到本地并进入核心业务逻辑。\n小程序端核心代码 小程序充当了HTTP客户端，它用来作为输入WIFI信息的终端。这个界面非常简单，一个WiFi信息提交表单，包含了SSID信息和密码信息，以及一个提交按钮。\n下面是小程序端提交WiFi配置信息的核心代码，需要注意的是，在操作的时候，需要将手机先连接到ESP8266热点，然后让用户填写的WIFI账号和密码，并点击提交按钮，调用JS通过HTTP接口传送给ESP8266 Web服务。\nsendConfig(e) { const that = this; let obj = e.detail.value; if (utils.isEmpty(obj.ssid)) { wx.showToast({ title: \u0026#39;WiFi名称为空\u0026#39;, icon: \u0026#39;none\u0026#39; }); return; } wx.showLoading({ title: \u0026#39;配置中...\u0026#39;, mask: true }); const url = \u0026#34;http://192.168.4.1/config\u0026#34;; const requestTask = wx.request({ url: url, method: \u0026#34;POST\u0026#34;, timeout: 10000, // 10秒超时 header: { \u0026#39;content-type\u0026#39;: \u0026#39;application/x-www-form-urlencoded\u0026#39;, }, data: { ssid: obj.ssid, pass: obj.pass, }, success: (res) =\u0026gt; { wx.hideLoading(); // 兼容处理：只要服务器返回了200，基本代表ESP8266已收到指令 if (res.statusCode === 200) { that.setData({ status: \u0026#34;指令下发成功，观察设备状态\u0026#34; }); wx.showModal({ title: \u0026#39;配置已发送\u0026#39;, content: \u0026#39;设备正在尝试连接路由器，请观察设备指示灯或稍后切换网络查看。\u0026#39;, showCancel: false }); } }, fail: (err) =\u0026gt; { wx.hideLoading(); // 在SoftAP模式下，ESP8266切换网络会导致热点消失，常触发 errno: 600001 (连接超时/断开) if (err.errMsg.indexOf(\u0026#39;timeout\u0026#39;) !== -1) { that.setData({ status: \u0026#34;连接超时，请检查热点连接\u0026#34; }); } else { // 关键逻辑：如果是由于热点消失导致的断开，其实很可能是已经成功去连接WiFi了 that.setData({ status: \u0026#34;已下发配网，请查看设备状态\u0026#34; }); } } }); }, 开发建议 小程序端建议：小程序在发送 POST 请求时，URL 应设为 192.168.4.1（这是 ESP8266 默认的 AP 地址），如果需要其它地址，需要ESP8266芯片代码和小程序端代码同时修改。 持久化：代码中的 WiFi.persist(true) 会将最后一次成功的配置写入 Flash。下次开机时，在 setup() 中调用 WiFi.begin() 且不传参数，它会自动尝试连接上次的 WiFi。这块逻辑需要根据自身情况去完善。 异常处理：示例代码非常简单，在实际实现时，需要在小程序端加入超时逻辑。如果 10 秒内设备热点没消失（说明没切到 STA 模式），则提示用户“密码可能输入错误”。 ","permalink":"https://blog.91demo.top/embedded/esp-conf.html","summary":"\u003ch2 id=\"背景介绍\"\u003e背景介绍\u003c/h2\u003e\n\u003cp\u003e在我将ESP8266采集代码灌入之后，它已经可以实现自动采集按钮信号了。核心逻辑实现之后，我需要能够连接网络，因为网络是连接MQTT的前置条件。\u003c/p\u003e\n\u003cp\u003e在连接之前，我需要将路由器的WIFI密码告知ESP8266芯片，这不同于Debug的时候，在代码内固定填写WIFI账号密码。我需要动态的获取并写入到ESP8266芯片。\u003c/p\u003e\n\u003cp\u003e网络配置有很多种方式，我选择了\u003ccode\u003eSoftAP配网\u003c/code\u003e，虽然配置有点繁琐，但这是一种很可靠的配网方案，兼容性极高，不依赖手机硬件的特殊协议。\u003c/p\u003e\n\u003cp\u003e准确来说，这不应该算是一个项目，但因为它具有通用性和一点实用性。我决定把它记录下来，可以方便地应用到我的其它项目中。\u003c/p\u003e\n\u003ch2 id=\"soft配网原理\"\u003eSoft配网原理\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e初始化模式：ESP8266 启动后进入 WIFI_AP_STA 模式。它会开启一个无密码（或已知密码）的热点（AP），并运行一个轻量级的 Web 服务器（HTTP Server）。\u003c/li\u003e\n\u003cli\u003e通道建立：手机通过小程序或系统设置连接到该热点。此时手机与 ESP8266 处于同一个局域网内。\u003c/li\u003e\n\u003cli\u003e数据交互：小程序通过 HTTP Post 请求将目标 WiFi 的 SSID 和 Password 发送给 ESP8266 的固定接口（如 /config）。\u003c/li\u003e\n\u003cli\u003e校验与切换：ESP8266 收到参数后，尝试作为客户端（STA）连接路由器。如果连接成功，则关闭 AP 热点，保存参数到 Flash；如果失败，则返回错误信息并维持 AP 状态。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"esp8266端核心代码arduino-ide\"\u003eESP8266端核心代码（Arduino IDE）\u003c/h2\u003e\n\u003cp\u003e在Arduino IDE打开项目后，需要安装ESP8266 WebServer库。这个库提供了Web服务，可以接收其它HTTP客户端的连接。\u003c/p\u003e\n\u003cp\u003e以下代码实现了一个非常简单的Web服务，它监听\u003ccode\u003e/config\u003c/code\u003e接口并能够接收WIFI信息，以及接收之后可以本地存储，实现动态配置的功能。\u003c/p\u003e\n\u003cp\u003e注意：当存储在本地之后，下次启动可以直接使用。而不需要重新配置。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#include \u0026lt;ESP8266WiFi.h\u0026gt;\n#include \u0026lt;ESP8266WebServer.h\u0026gt;\n\n// 定义AP热点的名称\nconst char* ap_ssid = \u0026#34;ESP8266_Config_Device\u0026#34;;\nESP8266WebServer server(80);\n\nvoid handleConfig() {\n  if (server.hasArg(\u0026#34;ssid\u0026#34;) \u0026amp;\u0026amp; server.hasArg(\u0026#34;pass\u0026#34;)) {\n    String target_ssid = server.arg(\u0026#34;ssid\u0026#34;);\n    String target_pass = server.arg(\u0026#34;pass\u0026#34;); // 确保这里是 pass\n\n    // 1. 先回复小程序，否则一旦开始连WiFi，AP就会失效，小程序收不到回复会报错\n    server.sendHeader(\u0026#34;Access-Control-Allow-Origin\u0026#34;, \u0026#34;*\u0026#34;);\n    server.send(200, \u0026#34;text/plain\u0026#34;, \u0026#34;SUCCESS\u0026#34;);\n  \n    Serial.println(\u0026#34;配置信息已收到，准备连接...\u0026#34;);\n  \n    // 2. 延迟一下再连接，给 HTTP 响应留出传输时间\n    delay(1000); \n  \n    WiFi.begin(target_ssid.c_str(), target_pass.c_str());\n    \n    int counter = 0;\n    while (WiFi.status() != WL_CONNECTED \u0026amp;\u0026amp; counter \u0026lt; 20) { // 等待10秒\n      delay(500);\n      Serial.print(\u0026#34;.\u0026#34;);\n      counter++;\n    }\n\n    if (WiFi.status() == WL_CONNECTED) {\n      Serial.println(\u0026#34;\\nWiFi连接成功!\u0026#34;);\n      WiFi.mode(WIFI_STA); // 关闭AP模式，切换为STA模式\n      WiFi.setAutoConnect(true);\n      WiFi.persist(true);   // 将账号密码永久保存到Flash\n    } else {\n      Serial.println(\u0026#34;\\n连接失败，请检查账号密码\u0026#34;);\n      // 保持AP模式，等待下次尝试\n    }\n  } else {\n    server.send(400, \u0026#34;text/plain\u0026#34;, \u0026#34;FAIL: Missing Params\u0026#34;);\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  \n  // 设置为AP+STA模式\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.softAP(ap_ssid);\n  \n  Serial.println(\u0026#34;AP热点已启动，IP地址: \u0026#34; + WiFi.softAPIP().toString());\n\n  // 注册接口\n  server.on(\u0026#34;/config\u0026#34;, HTTP_POST, handleConfig);\n  server.begin();\n  Serial.println(\u0026#34;HTTP服务器已启动\u0026#34;);\n}\n\nvoid loop() {\n  server.handleClient();\n  \n  // 如果连接成功，可以根据需要在这里处理业务逻辑\n  if (WiFi.status() == WL_CONNECTED) {\n    static bool connected_msg = false;\n    if (!connected_msg) {\n      Serial.println(\u0026#34;设备已联网，IP: \u0026#34; + WiFi.localIP().toString());\n      connected_msg = true;\n    }\n  }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e参考上面的原理，设备上电之后，启动AP模式，接收配置，这个时候可以通过指示灯来显示状态。接收完毕后，可以正常连接到WIFI，则将WIFI配置信息保存到本地并进入核心业务逻辑。\u003c/p\u003e","title":"ESP8266 工业级配网实战：基于 SoftAP 的可靠 WIFI 动态配置方案"},{"content":"查询本机 IP 连接\nnetstat -an 查看网络 TCP 连接状态数量\nnetstat -n | awk \u0026#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}\u0026#39; 查看 8080 端口有多少个 TCP 连接\nnetstat -ant |grep 8080|wc -l 查看当前 TCP 连接状态为 ESTABLISHED 的数量\nnetstat -ant|grep 8080|grep ESTABLISHED|wc -l 查看 TCP 连接\nnetstat -ant 过滤某个 TCP 端口\nnetstat -ant |grep 端口号 查看 UDP 连接\nnetstat -anu 过滤某个 UDP 端口\nnetstat -anu |grep 端口号 实时监控 UDP 连接信息，每隔 2 秒自动更新一次\nwatch -n 2 \u0026#39;netstat -anu\u0026#39; 使用 ss 或 netstat 关联进程和端口\n# 显示所有 TCP/UDP 连接及关联的进程（需要 root 权限） sudo ss -tunap | grep \u0026lt;目标IP或端口\u0026gt; sudo netstat -tunap | grep \u0026lt;目标IP或端口\u0026gt; 使用 lsof 按端口或 IP 过滤进程\n# 列出所有使用 TCP 的进程 sudo lsof -iTCP -sTCP:ESTABLISHED # 按端口过滤（如 443） sudo lsof -i :443 # 按远程 IP 过滤（如 203.0.113.5） sudo lsof -i @203.0.113.5 抓包分析明文内容\n# 抓取 eth0 网卡上目标端口 80 的流量，并显示 ASCII 内容 sudo tcpdump -i eth0 port 80 -A # 抓取特定 IP 的流量（如 203.0.113.5） sudo tcpdump -i eth0 host 203.0.113.5 -A 使用 nethogs 实时监控流量（需 root）\nsudo nethogs -d 2 # 每 2 秒刷新一次 检查进程可信性\n# 通过 PID 查进程路径 sudo ls -l /proc/\u0026lt;PID\u0026gt;/exe # 查进程启动参数 sudo ps -p \u0026lt;PID\u0026gt; -f 阻断可疑连接\n# 用 iptables 临时封禁 IP sudo iptables -A INPUT -s \u0026lt;可疑IP\u0026gt; -j DROP 检查外部 IP\n# 查询 IP 归属地 whois \u0026lt;可疑IP\u0026gt; curl ipinfo.io/\u0026lt;可疑IP\u0026gt; 假设发现一个到 203.0.113.5:443 的未知 TCP 连接：\n定位进程： sudo ss -tunap | grep 203.0.113.5:443 # 输出：PID=1234 (curl) 查看进程详情： ps -p 1234 -f # 输出：/usr/bin/curl https://example.com 抓包验证： sudo tcpdump -i eth0 host 203.0.113.5 -A # 输出：GET /api/data HTTP/1.1... 需要安装的工具：\n# Debian/Ubuntu sudo apt install net-tools lsof tcpdump tshark # CentOS/RHEL sudo yum install net-tools lsof tcpdump wireshark-cli ","permalink":"https://blog.91demo.top/wiki/netstat.html","summary":"\u003cp\u003e查询本机 IP 连接\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -an\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看网络 TCP 连接状态数量\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -n | awk \u0026#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}\u0026#39;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看 8080 端口有多少个 TCP 连接\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -ant |grep 8080|wc -l\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看当前 TCP 连接状态为 ESTABLISHED 的数量\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -ant|grep 8080|grep ESTABLISHED|wc -l\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看 TCP 连接\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -ant\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e过滤某个 TCP 端口\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -ant |grep 端口号\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看 UDP 连接\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -anu\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e过滤某个 UDP 端口\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -anu |grep 端口号\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e实时监控 UDP 连接信息，每隔 2 秒自动更新一次\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ewatch -n 2 \u0026#39;netstat -anu\u0026#39;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e使用 ss 或 netstat 关联进程和端口\u003c/p\u003e","title":"  Netstat 常用命令"},{"content":"Asterisk 是第一套以开放源代码软件实现的 用户交换机 (PBX) 系统。Asterisk 由 Digium 的创办人马克·史宾瑟（Mark Spencer）于 1999 年他还在奥本大学念书时所开发。与其他的用户交换机系统相同，Asterisk 同样支持电话拨打另一只分机，和拨打到公共交换电话网与 IP 电话系统。Asterisk 这个名称源自于星号 \u0026ldquo;*\u0026quot;。\n网址：https://www.asterisk.org/\nAsterisk 常见错误 403 登录失败\n解决：等待 1 分钟，等超时之后重新连接即可。一般是切换 IP 时或者换软电话登录会碰到。\n401 登录失败\n解决：检查账户和密码是否正确？检查连接地址是否正确？检查 Asterisk 是否启动？检查防火墙端口是否打开？\n408 连接超时\n解决：一般是 Asterisk 服务不通，检查 Asterisk 服务是否启动，检查防火墙是否打开？\n可以拨通，没有声音？\n一般是 NAT 造成，配置这三个参数：rtp_symmetric，force_rport，rewrite_contact。\nSIP 客户端没有自动挂机？\n一般是没有设置 stun 造成的，在 SIP 客户端，设置 stun 即可。如果没有 stun server，可以设置这个stun.l.google.com:19302\nAGI 脚本返回状态 4，正常应该为 0？\n查看网上资料，是 AGI 脚本中调用 Hangup 导致，将脚本中的 Hangup 去掉，放在拨号计划配置文件中执行 Hangup，可以解决这个问题。\nAGI Script agidemo completed, returning 0 解决 asterisk 没有声音的问题 在配置好 asterisk 之后，拨打 8000 没有听到声音。\n我的环境，asterisk 在云服务器，使用手机 zoiper 拨打。没有声音，经过一番调整后，使用如下配置可行。\nasterisk 的配置如下：\nrtp_symmetric = yes force_rport = yes rewrite_contact = no 手机 zoiper 的配置，ice 启用，其它全是默认配置。\n现在发现，WIFI 可以拨通，流量不可以。\n经过排查，发现 RTP 在 WIFI 情况下使用同一个端口，在流量情况下不是同一个端口。\n打开 rtp 调试信息，使用如下命令：\nrtp set debug on 查看 rtp 的配置，使用如下命令：\nrtp show setting 可以看到 rtp 信息。\n如果排查 PJSIP 信息，可以使用查看 pjsip 的日志命令：\npjsip set logger on 在配置好 asterisk 之后，拨打 8000 没有听到声音。\n我的环境，asterisk 在云服务器，使用手机 zoiper 拨打。没有声音，经过一番调整后，使用如下配置可行。\nasterisk 的配置如下：\nrtp_symmetric = yes force_rport = yes rewrite_contact = no 手机 zoiper 的配置，ice 启用，其它全是默认配置。\n现在发现，WIFI 可以拨通，流量不可以。\n经过排查，发现 RTP 在 WIFI 情况下使用同一个端口，在流量情况下不是同一个端口。\n打开 rtp 调试信息，使用如下命令：\nrtp set debug on 查看 rtp 的配置，使用如下命令：\nrtp show setting 可以看到 rtp 信息。\n如果排查 PJSIP 信息，可以使用查看 pjsip 的日志命令：\npjsip set logger on ","permalink":"https://blog.91demo.top/wiki/asterisk.html","summary":"\u003cp\u003eAsterisk 是第一套以开放源代码软件实现的 用户交换机 (PBX) 系统。Asterisk 由 Digium 的创办人马克·史宾瑟（Mark Spencer）于 1999 年他还在奥本大学念书时所开发。与其他的用户交换机系统相同，Asterisk 同样支持电话拨打另一只分机，和拨打到公共交换电话网与 IP 电话系统。Asterisk 这个名称源自于星号 \u0026ldquo;*\u0026quot;。\u003c/p\u003e\n\u003cp\u003e网址：https://www.asterisk.org/\u003c/p\u003e\n\u003ch2 id=\"asterisk-常见错误\"\u003eAsterisk 常见错误\u003c/h2\u003e\n\u003cp\u003e403 登录失败\u003c/p\u003e\n\u003cp\u003e解决：等待 1 分钟，等超时之后重新连接即可。一般是切换 IP 时或者换软电话登录会碰到。\u003c/p\u003e\n\u003cp\u003e401 登录失败\u003c/p\u003e\n\u003cp\u003e解决：检查账户和密码是否正确？检查连接地址是否正确？检查 Asterisk 是否启动？检查防火墙端口是否打开？\u003c/p\u003e\n\u003cp\u003e408 连接超时\u003c/p\u003e\n\u003cp\u003e解决：一般是 Asterisk 服务不通，检查 Asterisk 服务是否启动，检查防火墙是否打开？\u003c/p\u003e\n\u003cp\u003e可以拨通，没有声音？\u003c/p\u003e\n\u003cp\u003e一般是 NAT 造成，配置这三个参数：rtp_symmetric，force_rport，rewrite_contact。\u003c/p\u003e\n\u003cp\u003eSIP 客户端没有自动挂机？\u003c/p\u003e\n\u003cp\u003e一般是没有设置 stun 造成的，在 SIP 客户端，设置 stun 即可。如果没有 stun server，可以设置这个\u003ccode\u003estun.l.google.com:19302\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eAGI 脚本返回状态 4，正常应该为 0？\u003c/p\u003e\n\u003cp\u003e查看网上资料，是 AGI 脚本中调用 Hangup 导致，将脚本中的 Hangup 去掉，放在拨号计划配置文件中执行 Hangup，可以解决这个问题。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eAGI Script agidemo completed, returning 0\n\u003c/code\u003e\u003c/pre\u003e\u003ch1 id=\"解决-asterisk-没有声音的问题\"\u003e解决 asterisk 没有声音的问题\u003c/h1\u003e\n\u003cp\u003e在配置好 asterisk 之后，拨打 8000 没有听到声音。\u003c/p\u003e","title":" Asterisk 常见错误"},{"content":"查看内存占用\nps aux | grep soffice | grep -v grep | awk \u0026#39;{print $6/1024 \u0026#34; MB\u0026#34;}\u0026#39; 查看日志 查询 IP 信息，查找某条请求记录，然后打印出 IP 那一列，进行排序，再去重。可以得出请求的 IP 信息。\ncat golog/20240905_ad.log |grep \u0026#39;ad\u0026#39;|awk \u0026#39;{print $9}\u0026#39;|sort|uniq 从 IP 归属地库查询省份城市信息\ncat ip.merge.txt |grep \u0026#39;河南省\u0026#39;|awk -v FS=\u0026#34;|\u0026#34; \u0026#39;{print $5}\u0026#39;|sort|uniq 将行记录变成一行，并以逗号分割。\ncat ip.merge.txt |grep \u0026#39;河南省\u0026#39;|awk -v FS=\u0026#34;|\u0026#34; \u0026#39;{print $5}\u0026#39;|sort|uniq|paste -s -d \u0026#34;,\u0026#34; Awk awk 是一种处理文本文件的语言，是一个强大的文本分析工具。\n使用分隔符指定列：\nawk -F\u0026#39;,\u0026#39; \u0026#39;{print $1, $2}\u0026#39; file 打印满足条件的行数：\nawk \u0026#39;/pattern/ {print NR, $0}\u0026#39; file SCP 正则匹配复制文件 scp 可以使用正则复制文件，一次复制多个文件\nscp -P 2222 \\*.mp4 root@host:/data/upfile/ scp 还可以复制文件夹，使用-r 属性，递归复制，例如下面的格式：\nscp -P 2222 -r 2023/ root@host:/data/upfile/ Sed Linux sed 命令是利用脚本来处理文本文件。\n将文件中的 oo 改为 kk\nsed -i \u0026#39;s/oo/kk/g\u0026#39; testfile 将文件中的 ooo 去掉\nsed -i \u0026#39;s/oo//g\u0026#39; testfile 删除行首空格\nsed -i \u0026#39;s/^ //g\u0026#39; testfile 删除行尾空格\nsed -i \u0026#39;s/ $//g\u0026#39; testfile 将 IP 前面的部分予以删除：\n$ /sbin/ifconfig eth0 | grep \u0026#39;inet addr\u0026#39; | sed \u0026#39;s/^.*addr://g\u0026#39; 192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0 在文件最后添加一行内容：\nsed -i \u0026#39;$a # This is a test\u0026#39; regular_express.txt 开机自启服务 在 Windows10 上，使用 windows + r 键，调出运行，输入 shell:startup 进入开机启动项文件夹。新建文件：wsl.vbs，名字自定义，但必须使用 vbs 作为扩展名。\nSet ws = CreateObject(\u0026#34;Wscript.Shell\u0026#34;) ws.run \u0026#34;wsl -d Ubuntu -u root /etc/init.d/ssh start\u0026#34;, vbhide ws.run \u0026#34;wsl -d Ubuntu -u root /etc/init.d/cron start\u0026#34;,vbhide 查看默认的 WSL 名称 wsl -l Mapfile 从标准输入或文件描述符读取行并复制给数组。\n参数选项：\n-d delim 将delim设为行分隔符，代替默认的换行符。 -n count 从标准输入中获取最多count行，如果count为零那么获取全部。 -O origin 从数组下标为origin的位置开始赋值，默认的下标为0。 -s count 跳过对前count行的读取。 -t 读取时移除行分隔符delim（默认为换行符）。 -u fd 从文件描述符fd中读取。 -C callback 每当读取了quantum行时，调用callback语句。 -c quantum 设定读取的行数为quantum。 如果使用-C时没有同时使用-c指定quantum的值，那么quantum默认为5000。 当callback语句执行时，将数组下一个要赋值的下标以及读取的行作为额外的参数传递给callback语句。 如果使用-O时没有提供起始位置，那么mapfile会在实际赋值之前清空该数组。 array（可选）：用于输出的数组名称。如果没有指定数组名称，那么会默认写入到变量名为MAPFILE的数组中。 返回成功除非使用了非法选项、指定的数组是只读的、指定的数组不是下标数组。 下面是几个小示例：\n# 常见的读取形式。 mapfile \u0026lt; source_file target_array cat source_file |mapfile target_array mapfile -u fd target_array # 只读取前5行。 mapfile \u0026lt; source_file -n 5 target_array # 跳过前5行。 mapfile \u0026lt; source_file -s 5 target_array # 在数组指定的下标开始赋值。 # 请注意：这样做不会清空该数组。 mapfile \u0026lt; source_file -O 2 target_array # 读取时设定行分隔符为tab。 # 注意，第二行的tab在终端需要用ctrl+v tab输入； mapfile \u0026lt; source_file -d $\u0026#39;\\t\u0026#39; target_array mapfile \u0026lt; source_file -d \u0026#39;\t\u0026#39; target_array # 读取时移除行分隔符（tab）。 mapfile \u0026lt; source_file -d $\u0026#39;\\t\u0026#39; -t target_array # 读取时移除行分隔符（换行符）。 mapfile \u0026lt; source_file -t target_array # 每读取2行，执行一次语句（在这里是echo）。 mapfile \u0026lt; source_file -C \u0026#34;echo CALLBACK:\u0026#34; -c 2 target_array # 遍历下标，依次显示数组的元素。 for i in ${!target_array[@]}; do printf \u0026#34;%s\u0026#34; ${target_array[i]} done 进程替换 在 Linux 的 Bash 脚本中，\u0026lt; \u0026lt;(command)的组合是进程替换的意思，表示将 command 的输出通过临时文件描述符重定向到前一个命令的输入。\n例如：\ncat \u0026lt; \u0026lt;(echo \u0026#34;Hello World\u0026#34;) # 等价于：echo \u0026#34;Hello World\u0026#34; | cat 其中，** \u0026lt;(command) ** 会生成一个临时文件描述符（如/dev/fd/63），其中包含 command 的输出，然后** \u0026lt; ** 将这个文件描述符的内容作为输入传递给前面的命令。\n同理，\u0026gt; \u0026gt;(command) 可以将前面命令的输出通过管道传递给另一个命令：\n例如：\n# 将 ls 的输出传递给 grep ls \u0026gt; \u0026gt;(grep \u0026#34;txt\u0026#34;) 通过这种组合，你可以更灵活地在命令之间传递数据，避免管道 (|) 的某些限制（例如需要处理子 Shell 或临时文件）。\n","permalink":"https://blog.91demo.top/wiki/bash.html","summary":"\u003cp\u003e查看内存占用\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eps aux | grep soffice | grep -v grep | awk \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{print $6/1024 \u0026#34; MB\u0026#34;}\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"查看日志\"\u003e查看日志\u003c/h2\u003e\n\u003cp\u003e查询 IP 信息，查找某条请求记录，然后打印出 IP 那一列，进行排序，再去重。可以得出请求的 IP 信息。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecat golog/20240905_ad.log |grep \u0026#39;ad\u0026#39;|awk \u0026#39;{print $9}\u0026#39;|sort|uniq\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e从 IP 归属地库查询省份城市信息\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecat ip.merge.txt |grep \u0026#39;河南省\u0026#39;|awk -v FS=\u0026#34;|\u0026#34; \u0026#39;{print $5}\u0026#39;|sort|uniq\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e将行记录变成一行，并以逗号分割。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecat ip.merge.txt |grep \u0026#39;河南省\u0026#39;|awk -v FS=\u0026#34;|\u0026#34; \u0026#39;{print $5}\u0026#39;|sort|uniq|paste -s -d \u0026#34;,\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"awk\"\u003eAwk\u003c/h2\u003e\n\u003cp\u003eawk 是一种处理文本文件的语言，是一个强大的文本分析工具。\u003c/p\u003e\n\u003cp\u003e使用分隔符指定列：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eawk -F\u0026#39;,\u0026#39; \u0026#39;{print $1, $2}\u0026#39; file\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e打印满足条件的行数：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eawk \u0026#39;/pattern/ {print NR, $0}\u0026#39; file\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"scp-正则匹配复制文件\"\u003eSCP 正则匹配复制文件\u003c/h2\u003e\n\u003cp\u003escp 可以使用正则复制文件，一次复制多个文件\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003escp -P 2222 \\*.mp4 root@host:/data/upfile/\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003escp 还可以复制文件夹，使用-r 属性，递归复制，例如下面的格式：\u003c/p\u003e","title":" Bash 常用命令"},{"content":"cURL（Client URL）是一个功能强大的命令行工具，用于通过 URL 传输数据，支持多种协议（如 HTTP/HTTPS、FTP、SFTP、SCP 等）。它广泛用于 API 测试、文件传输、数据提交等场景。\n安装 Linux (Debian/Ubuntu) sudo apt-get install curl macOS brew install curl # 通过Homebrew安装/更新 Windows 从官网下载二进制文件 或使用 Chocolatey： choco install curl 基本用法 发起 GET 请求 curl https://example.com 保存响应到文件 curl -o custom_filename.html https://example.com # 自定义文件名 curl -O https://example.com/file.zip # 使用远程文件名 常用选项 选项 说明 -v 显示详细日志（请求头、响应头、SSL 信息） -L 自动跟随重定向 -s 静默模式（隐藏进度条和错误信息） -i 显示响应头 + 响应体 -I 仅显示响应头（发送 HEAD 请求） -k 跳过 SSL 证书验证（不安全，谨慎使用） -A 设置 User-Agent，如 -A \u0026quot;Mozilla/5.0\u0026quot; HTTP 方法 POST 请求（表单数据） curl -X POST https://api.example.com/data -d \u0026#34;name=John\u0026amp;age=30\u0026#34; POST 请求（JSON 数据） curl -X POST -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;name\u0026#34;:\u0026#34;John\u0026#34;, \u0026#34;age\u0026#34;:30}\u0026#39; https://api.example.com/data PUT/DELETE 请求 curl -X PUT https://api.example.com/item/1 -d \u0026#34;data=example\u0026#34; curl -X DELETE https://api.example.com/item/1 处理 HTTP 头 查看响应头 curl -I https://example.com 发送自定义头 curl -H \u0026#34;Authorization: Bearer token123\u0026#34; \\ -H \u0026#34;X-Custom-Header: value\u0026#34; https://api.example.com 文件传输 上传文件（表单） curl -F \u0026#34;file=@/path/to/file.txt\u0026#34; https://example.com/upload 上传文件（FTP） curl -T file.txt ftp://example.com/upload/ 断点续传 curl -C - -O https://example.com/large-file.zip 认证 基本认证（Basic Auth） curl -u username:password https://example.com Cookie 管理 curl -c cookies.txt https://example.com/login # 保存Cookie curl -b cookies.txt https://example.com/dashboard # 发送Cookie 高级功能 限速下载（100KB/s） curl --limit-rate 100K -O https://example.com/large-file.zip 使用代理 curl -x http://proxy-server:8080 https://example.com 调试请求 curl --trace-ascii debug.txt https://example.com 实际示例 下载 GitHub 仓库 curl -L -O https://github.com/user/repo/archive/master.zip 模拟浏览器访问 curl -A \u0026#34;Mozilla/5.0\u0026#34; -H \u0026#34;Accept-Language: en-US\u0026#34; https://example.com 调试 HTTPS 请求 curl -v -k https://example.com # 查看SSL握手过程（忽略证书错误） 注意事项 敏感数据：避免在命令行中直接暴露密码，推荐使用--netrc或环境变量。 HTTPS 安全：生产环境中尽量不使用-k（跳过证书验证）。 命令顺序：组合选项时注意顺序（如-L -O需先重定向再保存文件）。 速率限制：使用--limit-rate避免占用过多带宽。 ","permalink":"https://blog.91demo.top/wiki/curl.html","summary":"\u003cp\u003ecURL（Client URL）是一个功能强大的命令行工具，用于通过 URL 传输数据，支持多种协议（如 HTTP/HTTPS、FTP、SFTP、SCP 等）。它广泛用于 API 测试、文件传输、数据提交等场景。\u003c/p\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003ch3 id=\"linux-debianubuntu\"\u003eLinux (Debian/Ubuntu)\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo apt-get install curl\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"macos\"\u003emacOS\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ebrew install curl  \u003cspan style=\"color:#75715e\"\u003e# 通过Homebrew安装/更新\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"windows\"\u003eWindows\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e从\u003ca href=\"https://curl.se/windows/\"\u003e官网下载\u003c/a\u003e二进制文件\u003c/li\u003e\n\u003cli\u003e或使用 Chocolatey：\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003echoco install curl\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"基本用法\"\u003e基本用法\u003c/h2\u003e\n\u003ch3 id=\"发起-get-请求\"\u003e发起 GET 请求\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"保存响应到文件\"\u003e保存响应到文件\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -o custom_filename.html https://example.com  \u003cspan style=\"color:#75715e\"\u003e# 自定义文件名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -O https://example.com/file.zip              \u003cspan style=\"color:#75715e\"\u003e# 使用远程文件名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"常用选项\"\u003e常用选项\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e选项\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-v\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e显示详细日志（请求头、响应头、SSL 信息）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-L\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e自动跟随重定向\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-s\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e静默模式（隐藏进度条和错误信息）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-i\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e显示响应头 + 响应体\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-I\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e仅显示响应头（发送 HEAD 请求）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-k\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e跳过 SSL 证书验证（不安全，谨慎使用）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-A\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e设置 User-Agent，如 \u003ccode\u003e-A \u0026quot;Mozilla/5.0\u0026quot;\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"http-方法\"\u003eHTTP 方法\u003c/h2\u003e\n\u003ch3 id=\"post-请求表单数据\"\u003ePOST 请求（表单数据）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -X POST https://api.example.com/data -d \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;name=John\u0026amp;age=30\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"post-请求json-数据\"\u003ePOST 请求（JSON 数据）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -X POST -H \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Content-Type: application/json\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e     -d \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{\u0026#34;name\u0026#34;:\u0026#34;John\u0026#34;, \u0026#34;age\u0026#34;:30}\u0026#39;\u003c/span\u003e https://api.example.com/data\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"putdelete-请求\"\u003ePUT/DELETE 请求\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -X PUT https://api.example.com/item/1 -d \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;data=example\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -X DELETE https://api.example.com/item/1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"处理-http-头\"\u003e处理 HTTP 头\u003c/h2\u003e\n\u003ch3 id=\"查看响应头\"\u003e查看响应头\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -I https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"发送自定义头\"\u003e发送自定义头\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -H \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Authorization: Bearer token123\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e     -H \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;X-Custom-Header: value\u0026#34;\u003c/span\u003e https://api.example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"文件传输\"\u003e文件传输\u003c/h2\u003e\n\u003ch3 id=\"上传文件表单\"\u003e上传文件（表单）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -F \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;file=@/path/to/file.txt\u0026#34;\u003c/span\u003e https://example.com/upload\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"上传文件ftp\"\u003e上传文件（FTP）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -T file.txt ftp://example.com/upload/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"断点续传\"\u003e断点续传\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -C - -O https://example.com/large-file.zip\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"认证\"\u003e认证\u003c/h2\u003e\n\u003ch3 id=\"基本认证basic-auth\"\u003e基本认证（Basic Auth）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -u username:password https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"cookie-管理\"\u003eCookie 管理\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -c cookies.txt https://example.com/login     \u003cspan style=\"color:#75715e\"\u003e# 保存Cookie\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -b cookies.txt https://example.com/dashboard \u003cspan style=\"color:#75715e\"\u003e# 发送Cookie\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"高级功能\"\u003e高级功能\u003c/h2\u003e\n\u003ch3 id=\"限速下载100kbs\"\u003e限速下载（100KB/s）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl --limit-rate 100K -O https://example.com/large-file.zip\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"使用代理\"\u003e使用代理\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -x http://proxy-server:8080 https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"调试请求\"\u003e调试请求\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl --trace-ascii debug.txt https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"实际示例\"\u003e实际示例\u003c/h2\u003e\n\u003ch3 id=\"下载-github-仓库\"\u003e下载 GitHub 仓库\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -L -O https://github.com/user/repo/archive/master.zip\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"模拟浏览器访问\"\u003e模拟浏览器访问\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -A \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Mozilla/5.0\u0026#34;\u003c/span\u003e -H \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Accept-Language: en-US\u0026#34;\u003c/span\u003e https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"调试-https-请求\"\u003e调试 HTTPS 请求\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -v -k https://example.com  \u003cspan style=\"color:#75715e\"\u003e# 查看SSL握手过程（忽略证书错误）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"注意事项\"\u003e注意事项\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e敏感数据\u003c/strong\u003e：避免在命令行中直接暴露密码，推荐使用\u003ccode\u003e--netrc\u003c/code\u003e或环境变量。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHTTPS 安全\u003c/strong\u003e：生产环境中尽量不使用\u003ccode\u003e-k\u003c/code\u003e（跳过证书验证）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e命令顺序\u003c/strong\u003e：组合选项时注意顺序（如\u003ccode\u003e-L -O\u003c/code\u003e需先重定向再保存文件）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e速率限制\u003c/strong\u003e：使用\u003ccode\u003e--limit-rate\u003c/code\u003e避免占用过多带宽。\u003c/li\u003e\n\u003c/ol\u003e","title":" Curl 用法"},{"content":"DOCKER Docker 是一个开源的应用容器引擎，基于 Go 语言 并遵从 Apache2.0 协议开源。\nDocker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中，然后发布到任何流行的 Linux 机器上，也可以实现虚拟化。\n容器是完全使用沙箱机制，相互之间不会有任何接口（类似 iPhone 的 app）,更重要的是容器性能开销极低。\nDocker 网址：https://www.docker.com/\nDocker 常用命令： docker init # Creates Docker-related starter files docker build -t friendlyname . # Create image using this directory\u0026#39;s Dockerfile docker run -p 4000:80 friendlyname # Run \u0026#34;friendlyname\u0026#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 \u0026lt;hash\u0026gt; # Gracefully stop the specified container docker ps -a # See a list of all containers, even the ones not running docker kill \u0026lt;hash\u0026gt; # Force shutdown of the specified container docker rm \u0026lt;hash\u0026gt; # Remove the specified container from this machine docker rm -f \u0026lt;hash\u0026gt; # 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 \u0026lt;imagename\u0026gt; # Remove the specified image from this machine docker rmi $(docker images -q) # Remove all images from this machine docker logs \u0026lt;container-id\u0026gt; -f # Live tail a container\u0026#39;s logs docker login # Log in this CLI session using your Docker credentials docker tag \u0026lt;image\u0026gt; username/repository:tag # Tag \u0026lt;image\u0026gt; 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 DOCKER COMPOSE 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 \u0026lt;service_name\u0026gt;=\u0026lt;replica\u0026gt; # 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. DOCKER SERVICES docker service create \u0026lt;options\u0026gt; \u0026lt;image\u0026gt; \u0026lt;command\u0026gt; # Create new service docker service inspect --pretty \u0026lt;service_name\u0026gt; # Display detailed information Service(s) docker service ls # List Services docker service ps # List the tasks of Services docker service scale \u0026lt;service_name\u0026gt;=\u0026lt;replica\u0026gt; # Scale special service(s) docker service update \u0026lt;options\u0026gt; \u0026lt;service_name\u0026gt; # Update Service options DOCKER STACK docker stack ls # List all running applications on this Docker host docker stack deploy -c \u0026lt;composefile\u0026gt; \u0026lt;appname\u0026gt; # Run the specified Compose file docker stack services \u0026lt;appname\u0026gt; # List the services associated with an app docker stack ps \u0026lt;appname\u0026gt; # List the running containers associated with an app docker stack rm \u0026lt;appname\u0026gt; # Tear down an application DOCKER MACHINE docker-machine create --driver virtualbox myvm1 # Create a VM (Mac, Win7, Linux) docker-machine create -d hyperv --hyperv-virtual-switch \u0026#34;myswitch\u0026#34; myvm1 # Win10 docker-machine env myvm1 # View basic information about your node docker-machine ssh myvm1 \u0026#34;docker node ls\u0026#34; # List the nodes in your swarm docker-machine ssh myvm1 \u0026#34;docker node inspect \u0026lt;node ID\u0026gt;\u0026#34; # Inspect a node docker-machine ssh myvm1 \u0026#34;docker swarm join-token -q worker\u0026#34; # View join token docker-machine ssh myvm1 # Open an SSH session with the VM; type \u0026#34;exit\u0026#34; to end docker-machine ssh myvm2 \u0026#34;docker swarm leave\u0026#34; # Make the worker leave the swarm docker-machine ssh myvm1 \u0026#34;docker swarm leave -f\u0026#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\u0026#39;s home dir docker-machine ssh myvm1 \u0026#34;docker stack deploy -c \u0026lt;file\u0026gt; \u0026lt;app\u0026gt;\u0026#34; # Deploy an app ","permalink":"https://blog.91demo.top/wiki/docker.html","summary":"\u003ch1 id=\"docker\"\u003eDOCKER\u003c/h1\u003e\n\u003cp\u003eDocker 是一个开源的应用容器引擎，基于 Go 语言 并遵从 Apache2.0 协议开源。\u003c/p\u003e\n\u003cp\u003eDocker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中，然后发布到任何流行的 Linux 机器上，也可以实现虚拟化。\u003c/p\u003e\n\u003cp\u003e容器是完全使用沙箱机制，相互之间不会有任何接口（类似 iPhone 的 app）,更重要的是容器性能开销极低。\u003c/p\u003e\n\u003cp\u003eDocker 网址：https://www.docker.com/\u003c/p\u003e\n\u003ch2 id=\"docker-常用命令\"\u003eDocker 常用命令：\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edocker init # Creates Docker-related starter files\ndocker build -t friendlyname . # Create image using this directory\u0026#39;s Dockerfile\ndocker run -p 4000:80 friendlyname # Run \u0026#34;friendlyname\u0026#34; mapping port 4000 to 80\ndocker run -d -p 4000:80 friendlyname # Same thing, but in detached mode\ndocker exec -it [container-id] bash # Enter a running container\ndocker ps # See a list of all running containers\ndocker stop \u0026lt;hash\u0026gt; # Gracefully stop the specified container\ndocker ps -a # See a list of all containers, even the ones not running\ndocker kill \u0026lt;hash\u0026gt; # Force shutdown of the specified container\ndocker rm \u0026lt;hash\u0026gt; # Remove the specified container from this machine\ndocker rm -f \u0026lt;hash\u0026gt; # Remove force specified container from this machine\ndocker rm $(docker ps -a -q) # Remove all containers from this machine\ndocker images -a # Show all images on this machine\ndocker rmi \u0026lt;imagename\u0026gt; # Remove the specified image from this machine\ndocker rmi $(docker images -q) # Remove all images from this machine\ndocker logs \u0026lt;container-id\u0026gt; -f # Live tail a container\u0026#39;s logs\ndocker login # Log in this CLI session using your Docker credentials\ndocker tag \u0026lt;image\u0026gt; username/repository:tag # Tag \u0026lt;image\u0026gt; for upload to registry\ndocker push username/repository:tag # Upload tagged image to registry\ndocker run username/repository:tag # Run image from a registry\ndocker system prune # Remove all unused containers, networks, images (both dangling and unreferenced), and optionally, volumes. (Docker 17.06.1-ce and superior)\ndocker system prune -a # Remove all unused containers, networks, images not just dangling ones (Docker 17.06.1-ce and superior)\ndocker volume prune # Remove all unused local volumes\ndocker network prune # Remove all unused networks\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"docker-compose\"\u003eDOCKER COMPOSE\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edocker-compose up # Create and start containers\ndocker-compose up -d # Create and start containers in detached mode\ndocker-compose down # Stop and remove containers, networks, images, and volumes\ndocker-compose logs # View output from containers\ndocker-compose restart # Restart all service\ndocker-compose pull # Pull all image service\ndocker-compose build # Build all image service\ndocker-compose config # Validate and view the Compose file\ndocker-compose scale \u0026lt;service_name\u0026gt;=\u0026lt;replica\u0026gt; # Scale special service(s)\ndocker-compose top # Display the running processes\ndocker-compose run -rm -p 2022:22 web bash # Start web service and runs bash as its command, remove old container.\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"docker-services\"\u003eDOCKER SERVICES\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edocker service create \u0026lt;options\u0026gt; \u0026lt;image\u0026gt; \u0026lt;command\u0026gt; # Create new service\ndocker service inspect --pretty \u0026lt;service_name\u0026gt; # Display detailed information Service(s)\ndocker service ls # List Services\ndocker service ps # List the tasks of Services\ndocker service scale \u0026lt;service_name\u0026gt;=\u0026lt;replica\u0026gt; # Scale special service(s)\ndocker service update \u0026lt;options\u0026gt; \u0026lt;service_name\u0026gt; # Update Service options\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"docker-stack\"\u003eDOCKER STACK\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edocker stack ls # List all running applications on this Docker host\ndocker stack deploy -c \u0026lt;composefile\u0026gt; \u0026lt;appname\u0026gt; # Run the specified Compose file\ndocker stack services \u0026lt;appname\u0026gt; # List the services associated with an app\ndocker stack ps \u0026lt;appname\u0026gt; # List the running containers associated with an app\ndocker stack rm \u0026lt;appname\u0026gt; # Tear down an application\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"docker-machine\"\u003eDOCKER MACHINE\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edocker-machine create --driver virtualbox myvm1 # Create a VM (Mac, Win7, Linux)\ndocker-machine create -d hyperv --hyperv-virtual-switch \u0026#34;myswitch\u0026#34; myvm1 # Win10\ndocker-machine env myvm1 # View basic information about your node\ndocker-machine ssh myvm1 \u0026#34;docker node ls\u0026#34; # List the nodes in your swarm\ndocker-machine ssh myvm1 \u0026#34;docker node inspect \u0026lt;node ID\u0026gt;\u0026#34; # Inspect a node\ndocker-machine ssh myvm1 \u0026#34;docker swarm join-token -q worker\u0026#34; # View join token\ndocker-machine ssh myvm1 # Open an SSH session with the VM; type \u0026#34;exit\u0026#34; to end\ndocker-machine ssh myvm2 \u0026#34;docker swarm leave\u0026#34; # Make the worker leave the swarm\ndocker-machine ssh myvm1 \u0026#34;docker swarm leave -f\u0026#34; # Make master leave, kill swarm\ndocker-machine start myvm1 # Start a VM that is currently not running\ndocker-machine stop $(docker-machine ls -q) # Stop all running VMs\ndocker-machine rm $(docker-machine ls -q) # Delete all VMs and their disk images\ndocker-machine scp docker-compose.yml myvm1:~ # Copy file to node\u0026#39;s home dir\ndocker-machine ssh myvm1 \u0026#34;docker stack deploy -c \u0026lt;file\u0026gt; \u0026lt;app\u0026gt;\u0026#34; # Deploy an app\n\u003c/code\u003e\u003c/pre\u003e","title":" Docker 常用命令"},{"content":"firewall 是 Linux CentOS 等操作系统的防火墙。\n使用 rich rule 封禁 IP\nfirewall-cmd --permanent --add-rich-rule=\u0026#34;rule family=\u0026#39;ipv4\u0026#39; source address=\u0026#39;222.222.222.222\u0026#39; reject\u0026#34; 单个IP firewall-cmd --permanent --add-rich-rule=\u0026#34;rule family=\u0026#39;ipv4\u0026#39; source address=\u0026#39;222.222.222.0/24\u0026#39; reject\u0026#34; IP段 firewall-cmd --permanent --add-rich-rule=\u0026#34;rule family=ipv4 source address=192.168.1.2 port port=80 protocol=tcp accept\u0026#34; 单个IP的某个端口 firewall-cmd --list-rich-rules 查看封禁IP 使用 ip set 封禁 IP\nfirewall-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=\u0026#39;rule source ipset=blacklist drop\u0026#39; 使ipset生效 删除一个 ipset\nfirewall-cmd --permanent --delete-ipset=dog 如何防止自己被防火墙拦截，添加到 IP 白名单\nfirewall-cmd --permanent --zone=trusted --add-source=x.x.x.x 使配置生效\nfirewall-cmd --reload ","permalink":"https://blog.91demo.top/wiki/firewall.html","summary":"\u003cp\u003efirewall 是 Linux CentOS 等操作系统的防火墙。\u003c/p\u003e\n\u003cp\u003e使用 rich rule 封禁 IP\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efirewall-cmd --permanent --add-rich-rule=\u0026#34;rule family=\u0026#39;ipv4\u0026#39; source address=\u0026#39;222.222.222.222\u0026#39; reject\u0026#34;  单个IP\nfirewall-cmd --permanent --add-rich-rule=\u0026#34;rule family=\u0026#39;ipv4\u0026#39; source address=\u0026#39;222.222.222.0/24\u0026#39; reject\u0026#34; IP段\nfirewall-cmd --permanent --add-rich-rule=\u0026#34;rule family=ipv4 source address=192.168.1.2 port port=80  protocol=tcp  accept\u0026#34; 单个IP的某个端口\nfirewall-cmd --list-rich-rules 查看封禁IP\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e使用 ip set 封禁 IP\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efirewall-cmd --permanent --new-ipset=dog --type=hash:ip 封禁IP\nfirewall-cmd --permanent --ipset=dog --add-entry=ip地址\n\nfirewall-cmd --permanent --new-ipset=blacklist --type=hash:net 封禁网段\nfirewall-cmd --permanent --ipset=blacklist --add-entry=222.222.222.0/24\n\nfirewall-cmd --permanent --add-rich-rule=\u0026#39;rule source ipset=blacklist drop\u0026#39; 使ipset生效\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e删除一个 ipset\u003c/p\u003e","title":" Firewall-Cmd 常用命令"},{"content":"mdBook 是一个工具，可以将 Markdown 文件呈现为更适合 HTML 或 EPUB 等最终用户形式。\nmdbook 使用步骤：\n1， 编写 markdown 格式的文章内容\n2， 整理 SUMMARY.md 文件\n3， 使用 mdbook 浏览和编译\n4， 上传编译后的书籍内容到 Web 服务器\nmdbook 使用新的端口开发：\nmdbook serve -p 13000 将文章内容编译为书籍，默认存放在项目目录的 book 文件夹下。可以将该目录上传到 Web 服务器，然后启动 Web 服务器就可以访问书籍了。\nmdbook build 集成 google 统计 一番搜索后，找到可以集成 google 统计，并且这个功能在 7 年前已经有了，但是网上没有一篇文章介绍这个。经过查看源码，现将集成 google 统计的步骤记录下来。\n第一阶段，是在[output.html]配置中直接配置 google 统计，例如在 book.toml 中下面的配置\n[output.html] default-theme = \u0026#34;rust\u0026#34; google-analytics = \u0026#34;XXXXXXX\u0026#34; 当使用这种配置后，mdbook 编译会输出警告，这个 google 统计在将来的版本中会删除，推荐放在 theme 中的 head.hbs 文件中。\n首先，在项目目录下创建 mytheme 文件夹，然后放入 head.hbs 文件。文件的内容，就是 google 统计的代码，例如：\n\u0026lt;!-- Google tag (gtag.js) --\u0026gt; \u0026lt;script async src=\u0026#34;https://www.googletagmanager.com/gtag/js?id=XXXXXXX\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag(\u0026#39;js\u0026#39;, new Date()); gtag(\u0026#39;config\u0026#39;, \u0026#39;XXXXXXX\u0026#39;); \u0026lt;/script\u0026gt; 然后，在 book.toml 文件中调整配置，配置如下：\n[output.html] theme = \u0026#34;./mytheme\u0026#34; default-theme = \u0026#34;rust\u0026#34; 最后，使用 Mdbook 编译之后上传你的 Web 服务器即可。\n禁用打印功能 默认是打开打印功能的，书籍的右上角有个打印按钮，现在如果禁用打印功能，可以这样设置：\n[output.html.print] enable = false 激活书籍菜单折叠功能 默认是禁用书籍菜单折叠的，如果要启用菜单折叠功能，可以这样设置：\n[output.html.fold] enable = true level = 0 我们还可以设置折叠层级。默认 0，全部折叠。\n","permalink":"https://blog.91demo.top/wiki/mdbook.html","summary":"\u003cp\u003emdBook 是一个工具，可以将 Markdown 文件呈现为更适合 HTML 或 EPUB 等最终用户形式。\u003c/p\u003e\n\u003cp\u003emdbook 使用步骤：\u003cbr\u003e\n1， 编写 markdown 格式的文章内容\u003cbr\u003e\n2， 整理 SUMMARY.md 文件\u003cbr\u003e\n3， 使用 mdbook 浏览和编译\u003cbr\u003e\n4， 上传编译后的书籍内容到 Web 服务器\u003c/p\u003e\n\u003cp\u003emdbook 使用新的端口开发：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emdbook serve -p 13000\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e将文章内容编译为书籍，默认存放在项目目录的 book 文件夹下。可以将该目录上传到 Web 服务器，然后启动 Web 服务器就可以访问书籍了。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emdbook build\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"集成-google-统计\"\u003e集成 google 统计\u003c/h2\u003e\n\u003cp\u003e一番搜索后，找到可以集成 google 统计，并且这个功能在 7 年前已经有了，但是网上没有一篇文章介绍这个。经过查看源码，现将集成 google 统计的步骤记录下来。\u003c/p\u003e\n\u003cp\u003e第一阶段，是在[output.html]配置中直接配置 google 统计，例如在 book.toml 中下面的配置\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[output.html]\ndefault-theme = \u0026#34;rust\u0026#34;\ngoogle-analytics = \u0026#34;XXXXXXX\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e当使用这种配置后，mdbook 编译会输出警告，这个 google 统计在将来的版本中会删除，推荐放在 theme 中的 head.hbs 文件中。\u003c/p\u003e\n\u003cp\u003e首先，在项目目录下创建 mytheme 文件夹，然后放入 head.hbs 文件。文件的内容，就是 google 统计的代码，例如：\u003c/p\u003e","title":" Mdbook 常用命令"},{"content":"MQTT（Message Queuing Telemetry Transport）是一种轻量级、基于发布-订阅模式的消息传输协议，适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。它在物联网应用中广受欢迎，能够实现传感器、执行器和其它设备之间的高效通信。\nEclipse Mosquitto 是一个开源（EPL/EDL 许可）消息代理，实现了 MQTT 协议版本 5.0、3.1.1 和 3.1。Mosquitto 重量轻，适用于从低功耗单板计算机到全服务器的所有设备。\n在 CentOS 源码安装 mosquitto 1，从 https://mosquitto.org/download/ 下载源码到 Linux 服务器。\n2，解压缩后，编译安装\nmake make install 在编译时，提示 cJSON 找不动，先安装 cJSON。\n从该地址下载：https://github.com/DaveGamble/cJSON\nmake make install 3，配置 mosquitto，在/etc/mosquitto 目录下，将 mosquitto.conf.example 改为 mosquitto.conf。\n将以下内容修改：\n# 端口 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 这里，浪费了我很长时间，在添加 password_file 之后，服务无法启动。后经排查，是权限的问题。\n我使用的 root 账户，这是最终的修复脚本：\nchmod 0700 /etc/mosquitto/pwfile chown mosquitto:mosquitto /etc/mosquitto/pwfile chmod 0700 /etc/mosquitto/aclfile chown mosquitto:mosquitto /etc/mosquitto/aclfile pwfile 可以使用 mosquitto_passwd 命令创建，脚本如下：\nmosquitto_passwd -c /etc/mosquitto/pwfile roger 将创建用户 roger,如果文件已经存在，可以去掉-c 参数。\naclfile 文件可以复制模板，样例如下：\n# This affects access control for clients with no username. topic read $SYS/# # This only affects clients with username \u0026#34;roger\u0026#34;. user roger topic test/topic # This affects all clients. pattern write $SYS/broker/connection/%c/state 经过测试，当 aclfile 文件中如果没有配置某个 topic，而在应用中使用时，消息无法送达，起到了控制作用。\n4，配置成服务，可以在源码中复制 service/systemd/mosquitto.service.example 到/usr/lib/systemd/system 这个目录中。例如：\ncp /root/mosquitto-2.0.18/service/systemd/mosquitto.service /usr/lib/systemd/system # 启动服务 systemctl start mosquitto 5，连接 mosquitto 并进行测试\n开启一个窗口，启动订阅\nmosquitto_sub -p 1883 -u \u0026#39;用户名\u0026#39; -P \u0026#39;密码\u0026#39; -t \u0026#39;test/topic\u0026#39; -v 开启一个窗口，发布消息\nmosquitto_pub -p 1883 -u \u0026#39;用户名\u0026#39; -P \u0026#39;密码\u0026#39; -t \u0026#39;test/topic\u0026#39; -m \u0026#39;hello world\u0026#39; 最后，如果你要在公网访问，请把防火墙打开。\n使用 golang mqtt 库连接 broker 示例代码：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; mqtt \u0026#34;github.com/eclipse/paho.mqtt.golang\u0026#34; ) var msgPubHandler mqtt.MessageHandler = func(c mqtt.Client, m mqtt.Message) { fmt.Printf(\u0026#34;Received message: %s from topic: %s\\n\u0026#34;, m.Payload(), m.Topic()) } var connHandler mqtt.OnConnectHandler = func(c mqtt.Client) { fmt.Println(\u0026#34;Connected\u0026#34;) } func sub(client mqtt.Client) { topic := \u0026#34;cn/91demo/wander\u0026#34; token := client.Subscribe(topic, 1, nil) token.Wait() fmt.Printf(\u0026#34;Subscribed to topic %s\u0026#34;, topic) } func pub(client mqtt.Client) { num := 10 for i := 0; i \u0026lt; num; i++ { text := fmt.Sprintf(\u0026#34;Message %d\u0026#34;, i) token := client.Publish(\u0026#34;cn/91demo/wander\u0026#34;, 0, false, text) token.Wait() time.Sleep(time.Second) } } func main() { opts := mqtt.NewClientOptions() opts.AddBroker(\u0026#34;mqtt://localhost:1883\u0026#34;) opts.SetClientID(\u0026#34;go_mqtt_client\u0026#34;) opts.SetUsername(\u0026#34;demo\u0026#34;) opts.SetPassword(\u0026#34;123123\u0026#34;) opts.SetDefaultPublishHandler(msgPubHandler) opts.OnConnect = connHandler client := mqtt.NewClient(opts) if token := client.Connect(); token.Wait() \u0026amp;\u0026amp; token.Error() != nil { panic(token.Error()) } sub(client) pub(client) client.Disconnect(250) } Mosquitto 配置认证 在 Mosquitto 实例上配置身份验证非常重要，这样未经授权的客户端就无法连接。\n在 Mosquitto 2.0 及更高版本中，你必须明确选择身份验证选项，然后客户端才能连接。早期版本中，默认设置是允许客户端无需身份验证即可连接。\n身份验证有三种选择：密码文件、身份验证插件和匿名访问。可以使用三个选项的组合。\n在 Mosquitto2.0 及更高版本中，可以通过配置文件中将 per_listener_settings 设置为 true，让不同的侦听器使用不同的身份验证方法。\n除了身份验证，您还应该考虑访问权限控制，以确定哪些客户端可以访问哪些主题。\n密码文件设置 密码文件是一种将用户名和密码存储在单个文件中的简单机制。适合于有相对较少的静态用户。请注意，修改了密码文件后，需要重新加载 mosquitto。\n创建密码文件，可以使用mosquitto_passwd工具，命令如下：\nmosquitto_passwd -c \u0026lt;password file\u0026gt; \u0026lt;username\u0026gt; 请注意，使用-c 标志将覆盖已存在的文件，如果向已存在文件添加，请去掉-c 标志。\n要开始使用密码文件，需要配置 broker，在配置文件中，添加如下项：\npassword_file \u0026lt;path to the configuration file\u0026gt; 密码文件必须能够被运行 mosquitto 的用户读取。\n身份验证插件设置 如果你需要更多的控制来认证用户，你需要使用认证插件。具体的特性依赖于你使用的认证插件。\n可使用的插件：\nmosquitto-go-auth ，它提供了各种后端来存储用户数据，例如 mysql,postgresql,jwt 或 redis 等。 Dynamic security，动态安全插件，仅适用于 2.0 及更高版本，可提供原创管理的灵活的代理客户端、组和角色。 配置身份验证插件依赖你的 Mosquitto 的版本。\n版本 1.6.x 和以前的版本，使用auth_plugin选项。这个选项在版本 2.0 也被支持。\nlistener 1883 auth_plugin \u0026lt;path to plugin\u0026gt; 版本 2.0 及更高，使用plugin选项。\nlistener 1883 plugin \u0026lt;path to plugin\u0026gt; 匿名访问设置 配置匿名访问，请使用allow_anonymous选项。\nlistener 1883 allow_anonymous true 允许对同一代理进行匿名和身份验证访问是有效的。特别是，动态安全插件允许您为匿名用户分配与经过身份验证的用户不同的权限，例如，这可能对数据的只读访问有用。\n给 Mosquitto 配置 Go Auth 插件 Mosquitto Go Auth 是 Mosquitto MQTT 代理的身份验证和授权插件。之所以叫这个名字，是因为历史的原因，现在改名字太晚了，影响太大。\nMosquitto 是一个很流行的开源 MQTT 代理。go-auth 主要使用 Go 语言编写，它使用 cgo 来调用 mosquitto 的 auth-plugin 函数。\n它支持并实现了以下后端：\n文件 Postgresql 数据库 Mysql 数据库 Sqlite3 数据库 MongoDB 数据库 Redis JWT HTTP gRPC Javascript 解释器 定制后端 每个后端都提供用户、超级用户和 ACL 检查，并包括适当的测试。\n我们先来构建它，构建它需要 cgo 环境，并且需要先构建 mosquitto。我们在 Linux 主机上安装 gcc，并安装 golang 环境，golang 版本要求 1.18 以上。\n我们从https://github.com/eclipse/mosquitto下载源码文件。然后参考说明文档编译安装 mosquitto。我使用的 CentOS7，直接通过yum install mosquitto安装，安装的 mosquitto 版本为 1.6.10。所以我要下载 mosquitto1.6.10 的源代码。将代码中的 mosquitto.h，mosqutto_plugin.h 和 mosquitto_broker.h 文件复制到/usr/local/include 目录。\n我们从https://github.com/iegomez/mosquitto-go-auth下载 go auth 的源码文件。我下载的当前版本为 2.1.0，解压缩并进入到源文件目录。输入命令make，将编译一个 pw 二进制文件和一个 go-auth.so 文件。我们将 go-auth.so 文件复制到/usr/local/lib目录，将 pw 二进制文件复制到/usr/local/bin目录。pw 可以用来生成密码，go-auth.so 是 mosquitto 调用的库。\n现在我们调整配置，启用 go auth 插件。在/etc/mosquitto/mosquitto.conf 文件中添加如下内容：\ninclude_dir /etc/mosquitto/conf.d 然后，在/etc/mosquitto 目录下，创建 conf.d 文件夹。进入 conf.d 文件夹，创建 go-auth.conf 文件。在该文件中，先加入如下内容：\nauth_plugin /etc/mosquitto/conf.d/go-auth.so auth_opt_backends files, postgres ## 添加日志 auth_opt_log_level debug auth_opt_log_dest file auth_opt_log_file /var/log/mosquitto.log ## 添加加密算法 auth_opt_hasher bcrypt auth_opt_hasher_cost 10 我只使用了文件和 PG 数据库这两种后端，所以先讲这两种的配置方法。先来配置文件。\nauth_opt_files_password_path /etc/mosquitto/conf.d/password_file auth_opt_files_acl_path /etc/mosquitto/conf.d/acl_file 先给这两个文件修改拥有者和属性。这样 mosquitto 服务启动后，可以有权限调用这两个文件。\nchown mosquitto:mosquitto password_file chown mosquitto:mosquitto acl_file chmod 0600 password_file chmod 0600 acl_file 使用 pw 为用户生成密码，\npw -h brcrypt -p 123456(你的密码) password_file 格式如下：\ntest1:$2a$10$ATi9XlzxcevYuiznP90cuuWDCvrKJKguF6KBhMKrIgWWtBSjd44XO test2:$2a$10$DSbIaUqwJ8nTBFHyEt.5GOvSbOcRVREJGNFdquOnRdk9.4QopcOjC test3:$2a$10$do17Uiopj9kQfPsmRIboh.3KwbUNHAS/7cUtxN8a44kUBQiAqbZ6i 注意，你的密码肯定和我的不一样。\nacl_file 格式如下：\nuser test1 topic write test/topic/1 topic read test/topic/2 user test2 topic read test/topic/+ user test3 topic read test/# pattern read test/%u pattern read test/%c 重启 mosquitto 服务，文件后端配置就生效了。\n下面我们再来讲讲如何配置 PG 数据库。需要先创建两个表，表的结构如下：\ncreate table test_user( id bigserial primary key, username character varying (100) not null, password_hash character varying (200) not null, is_admin boolean not null); create table test_acl( id bigserial primary key, test_user_id bigint not null references test_user on delete cascade, topic character varying (200) not null, rw int not null); 更具体的 PG 创建数据库和用户就不细节了。在库中创建这两个表。然后添加如下配置到 go-auth.conf 文件中。\nauth_opt_pg_host localhost auth_opt_pg_port 5432 auth_opt_pg_dbname appserver auth_opt_pg_user appserver auth_opt_pg_password appserver auth_opt_pg_connect_tries 5 auth_opt_pg_userquery select password_hash from test_user where username = $1 limit 1 auth_opt_pg_superquery select count(*) from test_user where username = $1 and is_admin = true auth_opt_pg_aclquery SELECT a.topic FROM test_user u left join test_acl a on u.id = a.test_user_id WHERE (u.username = $1) AND a.rw = $2 上面的配置信息仅作为参考，请根据你实际的情况进行调整。\n在实际调试连接时，碰到两个问题，记录下来，以备查询。\n1，连接 pg 数据库后，使用 mqtt 客户端无法连接，一直报错没有认证，数据报文 mqtt3.1.1 返回 5，mqtt5.0 返回 135。\n经过排查，是配置文件问题，我使用的 mosquitto 的版本为 1.6.10。在 mosquitto 配置文件中，将下面两行注释掉，让代理取 go-auth 中的配置。\nallow_anonymous false #password_file /etc/mosquitto/pwfile #acl_file /etc/mosquitto/aclfile include_dir /etc/mosquitto/conf.d 2，使用 pg 的账户连接成功后，一直无法订阅主题，提示权限问题。\n经过排查，是在 pg 数据库设置订阅权限问题，Readme 中有提到，但没有注意。\nMosquitto 1.5 introduced a new ACL access value, MOSQ_ACL_SUBSCRIBE, which is similar to the classic MOSQ_ACL_READ value but not quite the same: Also, these are the current available values from mosquitto: #define MOSQ_ACL_NONE 0x00 #define MOSQ_ACL_READ 0x01 #define MOSQ_ACL_WRITE 0x02 #define MOSQ_ACL_SUBSCRIBE 0x04 所以，如果你使用 1.5 版本以后，需要添加 MOSQ_ACL_SUBSCRIBE 0x04。否则没有权限。\n下面是调通之后的正常日志：\ntime=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;Superuser check with backend Postgres\u0026#34; time=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;Acl check with backend Postgres\u0026#34; time=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;user c7a610f7176ebf8f0056 acl authenticated with backend Postgres\u0026#34; time=\u0026#34;2024-09-30T11:44:07+08:00\u0026#34; level=debug msg=\u0026#34;Acl is true for user c7a610f7176ebf8f0056\u0026#34; ","permalink":"https://blog.91demo.top/wiki/mosquitto.html","summary":"\u003cp\u003eMQTT（Message Queuing Telemetry Transport）是一种轻量级、基于发布-订阅模式的消息传输协议，适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。它在物联网应用中广受欢迎，能够实现传感器、执行器和其它设备之间的高效通信。\u003c/p\u003e\n\u003cp\u003eEclipse Mosquitto 是一个开源（EPL/EDL 许可）消息代理，实现了 MQTT 协议版本 5.0、3.1.1 和 3.1。Mosquitto 重量轻，适用于从低功耗单板计算机到全服务器的所有设备。\u003c/p\u003e\n\u003ch2 id=\"在-centos-源码安装-mosquitto\"\u003e在 CentOS 源码安装 mosquitto\u003c/h2\u003e\n\u003cp\u003e1，从 \u003ca href=\"https://mosquitto.org/download/\"\u003ehttps://mosquitto.org/download/\u003c/a\u003e 下载源码到 Linux 服务器。\u003c/p\u003e\n\u003cp\u003e2，解压缩后，编译安装\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emake\nmake install\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在编译时，提示 cJSON 找不动，先安装 cJSON。\u003cbr\u003e\n从该地址下载：https://github.com/DaveGamble/cJSON\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emake\nmake install\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e3，配置 mosquitto，在/etc/mosquitto 目录下，将 mosquitto.conf.example 改为 mosquitto.conf。\u003cbr\u003e\n将以下内容修改：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# 端口\nlistener 1883\n# 日志\nlog_dest file /var/log/mosquitto/mosquitto.log\nlog_type warning\nconnection_messages true\nlog_timestamp true\nlog_timestamp_format %Y-%m-%dT%H:%M:%S\n# 安全\nallow_anonymous false\npassword_file /etc/mosquitto/pwfile\nacl_file /etc/mosquitto/aclfile\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这里，浪费了我很长时间，在添加 password_file 之后，服务无法启动。后经排查，是权限的问题。\u003cbr\u003e\n我使用的 root 账户，这是最终的修复脚本：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003echmod 0700 /etc/mosquitto/pwfile\nchown mosquitto:mosquitto /etc/mosquitto/pwfile\nchmod 0700 /etc/mosquitto/aclfile\nchown mosquitto:mosquitto /etc/mosquitto/aclfile\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003epwfile 可以使用 mosquitto_passwd 命令创建，脚本如下：\u003c/p\u003e","title":" Mosquitto安装与配置"},{"content":"SQLite 是一个软件库，实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。常用在单机模式或嵌入式模式下。\n官网地址：https://www.sqlite.org/\n只导出数据库表结构 sqlite3 DATABASE_FILE.sqlite \u0026#39;.schema\u0026#39; \u0026gt; schema.sql 导出所有内容 sqlite3 DATABASE_FILE.sqlite \u0026#39;.dump\u0026#39; \u0026gt; dump.sql 导出 某张表内容 sqlite3 DATABASE_FILE.sqlite \u0026#39;.dump demotable1\u0026#39; \u0026gt; dump.sql 将一个表中的数据导入到另一张表中。 insert into v_arts(uuid,openid,title,keyword,views,status,ispub,islock,createtime,updatetime) select uuid,openid,title,\u0026#39;\u0026#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; ","permalink":"https://blog.91demo.top/wiki/sqlite3.html","summary":"\u003cp\u003eSQLite 是一个软件库，实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。常用在单机模式或嵌入式模式下。\u003c/p\u003e\n\u003cp\u003e官网地址：https://www.sqlite.org/\u003c/p\u003e\n\u003ch2 id=\"只导出数据库表结构\"\u003e只导出数据库表结构\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esqlite3 DATABASE_FILE.sqlite \u0026#39;.schema\u0026#39; \u0026gt; schema.sql\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"导出所有内容\"\u003e导出所有内容\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esqlite3 DATABASE_FILE.sqlite \u0026#39;.dump\u0026#39; \u0026gt; dump.sql\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"导出-某张表内容\"\u003e导出 某张表内容\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esqlite3 DATABASE_FILE.sqlite \u0026#39;.dump demotable1\u0026#39; \u0026gt; dump.sql\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"将一个表中的数据导入到另一张表中\"\u003e将一个表中的数据导入到另一张表中。\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003einsert into v_arts(uuid,openid,title,keyword,views,status,ispub,islock,createtime,updatetime) select uuid,openid,title,\u0026#39;\u0026#39; as keyword,views,status,1 as ispub,0 as islock,createtime,updatetime from blog_articles;\n\ninsert into v_artconts(uuid,content) select uuid,content from blog_article_contents;\n\u003c/code\u003e\u003c/pre\u003e","title":" Sqlite 常用命令"},{"content":"一个完整的跨平台解决方案，用于录制、转换和流式传输音频和视频。\n网址：https://www.ffmpeg.org/\n将视频转换为音频文件：\nffmpeg -i input.mp4 output.avi m4a 文件转换为 mp3 文件：\nffmpeg -i input.m4a -c:v copy -c:a libmp3lame -q:a 2 output.mp3 mp3 转 g711a 命令：\nffmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw 使用 ffplay 播放测试\nffplay -i test.alaw -f alaw -ac 1 -ar 8000 mp3 转 gsm 命令：\nffmpeg -i test.mp3 -ar 8000 -ac 1 test.gsm WAV 转 MP3 的 FFMPEG 命令：\nffmpeg -i input.wav -codec:a libmp3lame -qscale:a 2 output.mp3 ","permalink":"https://blog.91demo.top/wiki/ffmpeg.html","summary":"\u003cp\u003e一个完整的跨平台解决方案，用于录制、转换和流式传输音频和视频。\u003c/p\u003e\n\u003cp\u003e网址：https://www.ffmpeg.org/\u003c/p\u003e\n\u003cp\u003e将视频转换为音频文件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effmpeg -i input.mp4 output.avi\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003em4a 文件转换为 mp3 文件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effmpeg -i input.m4a -c:v copy -c:a libmp3lame -q:a 2 output.mp3\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003emp3 转 g711a 命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e使用 ffplay 播放测试\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effplay -i test.alaw -f alaw -ac 1 -ar 8000\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003emp3 转 gsm 命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effmpeg -i test.mp3 -ar 8000 -ac 1 test.gsm\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eWAV 转 MP3 的 FFMPEG 命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e ffmpeg -i input.wav -codec:a libmp3lame -qscale:a 2 output.mp3\n\u003c/code\u003e\u003c/pre\u003e","title":"FFmpeg 常用命令"},{"content":"在 APP 和小程序网络请求中，都会出现同一时间并发请求两次的情况，以前没有碰到，不代表没有发生，现在在用户登陆后，添加了赠送豆子点数的逻辑，在查看记录的时候，发现了此问题。会出现同一用户在同一时间登陆两次的请求。不知道造成会同时请求两次的问题的根本原因是什么？但需要解决赠送两次的问题。\n经过排查，在用户同时请求两次时，没有加锁，导致判断添加一直有效。最简单的方法是加锁。在 golang 中，锁是 sync.Mutex。加锁会影响一点点性能。\n在加锁后，此问题得到解决。代码片段：\nvar lock sync.Mutex var beanMap map[string]int64 lock.Lock() defer lock.UnLock() val,ok:=map[openid] // 查看是否已赠送？ if ok { // 已赠送，可继续业务逻辑 }else{ // 还未赠送 // 赠送逻辑 } ","permalink":"https://blog.91demo.top/wiki/twicehttp.html","summary":"\u003cp\u003e在 APP 和小程序网络请求中，都会出现同一时间并发请求两次的情况，以前没有碰到，不代表没有发生，现在在用户登陆后，添加了赠送豆子点数的逻辑，在查看记录的时候，发现了此问题。会出现同一用户在同一时间登陆两次的请求。不知道造成会同时请求两次的问题的根本原因是什么？但需要解决赠送两次的问题。\u003c/p\u003e\n\u003cp\u003e经过排查，在用户同时请求两次时，没有加锁，导致判断添加一直有效。最简单的方法是加锁。在 golang 中，锁是 sync.Mutex。加锁会影响一点点性能。\u003c/p\u003e\n\u003cp\u003e在加锁后，此问题得到解决。代码片段：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003evar lock sync.Mutex\nvar beanMap map[string]int64\n\nlock.Lock()\ndefer lock.UnLock()\nval,ok:=map[openid] // 查看是否已赠送？\nif ok {\n    // 已赠送，可继续业务逻辑\n}else{\n    // 还未赠送\n    // 赠送逻辑\n}\n\u003c/code\u003e\u003c/pre\u003e","title":"HTTP并发同一时间请求了两次"},{"content":"Iperf3 是一个网络性能测试工具。Iperf 可以测试最大 TCP 和 UDP 带宽性能，具有多种参数和 UDP 特性，可以根据需要调整，可以报告带宽、延迟抖动和数据包丢失.对于每个测试，它都会报告带宽，丢包和其他参数，可在 Windows、Mac OS X、Linux、FreeBSD 等各种平台使用，是一个简单又实用的小工具。\n安装 yum install iperf3 使用 网络带宽测试\nIperf3 是 C/S(客户端/服务器端)架构模式，在使用 iperf3 测试时，要同时在 server 端与 client 端都各执行一个程序，让它们互相传送报文进行测试。\n启动 Server 端的程序：\niperf3 -s 启动 Client 端的程序：\niperf3 -c 服务端IP地址 从打印内容可以查看网络带宽等参数。\n更多内容请查看命令手册。\n","permalink":"https://blog.91demo.top/wiki/iperf3.html","summary":"\u003cp\u003eIperf3 是一个网络性能测试工具。Iperf 可以测试最大 TCP 和 UDP 带宽性能，具有多种参数和 UDP 特性，可以根据需要调整，可以报告带宽、延迟抖动和数据包丢失.对于每个测试，它都会报告带宽，丢包和其他参数，可在 Windows、Mac OS X、Linux、FreeBSD 等各种平台使用，是一个简单又实用的小工具。\u003c/p\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eyum install iperf3\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"使用\"\u003e使用\u003c/h2\u003e\n\u003cp\u003e网络带宽测试\u003c/p\u003e\n\u003cp\u003eIperf3 是 C/S(客户端/服务器端)架构模式，在使用 iperf3 测试时，要同时在 server 端与 client 端都各执行一个程序，让它们互相传送报文进行测试。\u003c/p\u003e\n\u003cp\u003e启动 Server 端的程序：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eiperf3 -s\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e启动 Client 端的程序：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eiperf3 -c 服务端IP地址\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e从打印内容可以查看网络带宽等参数。\u003c/p\u003e\n\u003cp\u003e更多内容请查看命令手册。\u003c/p\u003e","title":"Iperf3 常用命令"},{"content":"1，Ubuntu18.04 设置固定 IP\n修改/etc/netplan 下的 yaml 文件，修改为\nnetwork: ethernets: eth0: addresses: - 172.28.45.220/20 nameservers: addresses: [202.102.224.68,202.102.227.68] dhcp4: false version: 2 然后执行\nsudo netplan apply 使用 ip a 查看地址是否变更\n当 ping www.baidu.com提示Temporary failure in name resolution\n那么需要执行\nsudo dhclient -v -4 就可以解决了。\n","permalink":"https://blog.91demo.top/wiki/setip.html","summary":"\u003cp\u003e1，Ubuntu18.04 设置固定 IP\u003cbr\u003e\n修改/etc/netplan 下的 yaml 文件，修改为\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetwork:\nethernets:\neth0:\naddresses: - 172.28.45.220/20\nnameservers:\naddresses: [202.102.224.68,202.102.227.68]\ndhcp4: false\nversion: 2\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后执行\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esudo netplan apply\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e使用 ip a 查看地址是否变更\u003cbr\u003e\n当 ping \u003ca href=\"https://www.baidu.com\"\u003ewww.baidu.com\u003c/a\u003e提示Temporary failure in name resolution\u003cbr\u003e\n那么需要执行\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esudo dhclient -v -4\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e就可以解决了。\u003c/p\u003e","title":"Linux Debian系设置固定 IP"},{"content":"今天学习 Linux 脚本自动交互。需要完成 IP 归属地数据文件的维护，而 gmaker 工具里面有交互命令，现在想用脚本替代手动输入。\n在我们日常的 Linux 指令中，有很多命令提供交互式操作，例如 sudo、ssh、ftp 等，如何使用脚本完成自动交互呢？\n1，使用重定向\n重定向是将标准输入输出进行重定向操作，例如将标准输入输出重定向到文件。用重定向标准输入到文件的前提，是命令支持有参数指定输入方式，如 ftp 就有-i 参数来指定使用标准输入来输入密码。\nshell 使用重定向标准输入的方法有：\ncommand \u0026lt; file 将输入重定向到file 命令交互式重定向\ncommand \u0026lt;\u0026lt; delimiter document delimiter shell 会将分界符 delimiter 之间的内容作为输入。\n例如，我们来使用 ftp 实现自动登录并运行 ls 的脚本，其中用户名为 test，密码为 123456。\nftp -i -n 192.168.1.10 \u0026lt;\u0026lt;EOF user test 123456 ls EOF 2，使用管道\n管道命令使用|这个符号，它会将前一个命令的输出作为下个命令的输入。它也需要命令支持。\n例如，实现 sudo 自动输入密码的脚本：\necho \u0026#39;123456\u0026#39; | sudo -S cp file1 /backup/file1 其中，123456 是密码。\n3，使用 expect 工具\nexpect 是用来做交互，基本任何交互的场合都能使用。在像 ssh 命令没有指定输入的参数，就很适合这种方式。\n需要先安装 expect，安装命令如下：\nyum install expect 例如：我们使用 ssh 远程登录某台服务器的脚本\n#!/usr/bin/expect set timeout 30 spawn ssh -l root 192.168.1.10 expect \u0026#34;password*\u0026#34; send \u0026#34;123456\\r\u0026#34; interact 其中，\n第一行是脚本执行引擎，第二行设置超时 30 秒，第三行执行 ssh 登录，spawn 是 expect 内部命令，意思是启动一个新的交互进程后面跟命令或者指定程序，第四行匹配 password 关键字，如果匹配到往下继续执行，只有 spawn 执行的命令结果会被 expect 捕捉到。如果捕获到立即返回，否则等待超时后返回，超时时间在第二行设置了。expect \u0026ldquo;#*\u0026rdquo; 或\u0026quot;]#\u0026ldquo;代表匹配任何内容。第五行发送密码，相当于手动输入密码。send 是 expect 内部命令，会将密码输入到 spawn 启动的进程中，第六行进入交互模式，留在远程控制台，如果没有这句就返回本地控制台。\n附上 expect 常用命令\n命令 说明 spawn 启动新的交互进程后面跟命令或者指定程序 expect 从进程接收消息，如果匹配成功，就执行 expect 后的动作 send 向进程发送字符串 send exp_send 用于发送指定的字符串信息 exp_continue 在 expect 中多次匹配就需要用到 send_user 用来打印输出 interact 允许用户交互 exit 退出 expect 脚本 eof expect 执行结束，退出 set 定义变量 puts 输出变量 set timeout 设置超时时间 下面，我们来实现我们的脚本，用来自动更新我们的 IP 源。\n#!/bin/bash change_num=`wc -l ipchange.txt|awk \u0026#39;{print $1}\u0026#39;` if [ $change_num == 0 ] then echo \u0026#34;file is empty\u0026#34; exit fi expect \u0026lt;\u0026lt;EOF set timeout 10 spawn gmaker edit --src=ip.merge.txt expect \u0026#34;editor*\u0026#34; {send \u0026#34;put_file ipchange.txt\\n\u0026#34;} expect \u0026#34;editor*\u0026#34; {send \u0026#34;save\\n\u0026#34;} expect \u0026#34;editor*\u0026#34; {send \u0026#34;quit\\n\u0026#34;} expect \u0026#34;#*\u0026#34; {send \u0026#34;exit\\n\u0026#34;} expect eof EOF sleep 1 cat /dev/null\u0026gt;ipchange.txt gmaker gen --src=ip.merge.txt --dst=ip2region.xdb 经实测，可用。\n","permalink":"https://blog.91demo.top/wiki/scriptinteractive.html","summary":"\u003cp\u003e今天学习 Linux 脚本自动交互。需要完成 IP 归属地数据文件的维护，而 gmaker 工具里面有交互命令，现在想用脚本替代手动输入。\u003c/p\u003e\n\u003cp\u003e在我们日常的 Linux 指令中，有很多命令提供交互式操作，例如 sudo、ssh、ftp 等，如何使用脚本完成自动交互呢？\u003c/p\u003e\n\u003cp\u003e1，使用重定向\u003c/p\u003e\n\u003cp\u003e重定向是将标准输入输出进行重定向操作，例如将标准输入输出重定向到文件。用重定向标准输入到文件的前提，是命令支持有参数指定输入方式，如 ftp 就有-i 参数来指定使用标准输入来输入密码。\u003c/p\u003e\n\u003cp\u003eshell 使用重定向标准输入的方法有：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecommand \u0026lt; file 将输入重定向到file\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e命令交互式重定向\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecommand \u0026lt;\u0026lt; delimiter\n    document\ndelimiter\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eshell 会将分界符 delimiter 之间的内容作为输入。\u003c/p\u003e\n\u003cp\u003e例如，我们来使用 ftp 实现自动登录并运行 ls 的脚本，其中用户名为 test，密码为 123456。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eftp -i -n 192.168.1.10 \u0026lt;\u0026lt;EOF\nuser test 123456\nls\nEOF\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e2，使用管道\u003c/p\u003e\n\u003cp\u003e管道命令使用|这个符号，它会将前一个命令的输出作为下个命令的输入。它也需要命令支持。\u003c/p\u003e\n\u003cp\u003e例如，实现 sudo 自动输入密码的脚本：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eecho \u0026#39;123456\u0026#39; | sudo -S cp file1 /backup/file1\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e其中，123456 是密码。\u003c/p\u003e\n\u003cp\u003e3，使用 expect 工具\u003c/p\u003e\n\u003cp\u003eexpect 是用来做交互，基本任何交互的场合都能使用。在像 ssh 命令没有指定输入的参数，就很适合这种方式。\u003c/p\u003e\n\u003cp\u003e需要先安装 expect，安装命令如下：\u003c/p\u003e","title":"Linxu脚本自动交互"},{"content":"MongoDB 是一个流行的开源文档型数据库，它使用类似 JSON 的文档模型存储数据，这使得数据存储变得非常灵活。\n官网地址：https://www.mongodb.com/\n当你需要迁移数据库时，或者进行备份时，使用 mongodump，可以将文件导出。\nmongodump -h 127.0.0.1 --username \u0026#39;xxx\u0026#39; -p \u0026#39;密码\u0026#39; --authenticationDatabase admin -d 数据库 --gzip --archive=xxx.gz 将数据导入到另一个服务器 mongo 数据库，或者进行升级。\nmongorestore --uri=\u0026#34;mongodb://localhost:27017\u0026#34; --gzip --archive=xxx.gz 如何你的数据库需要认证，那么请在 uri 中添加用户名和密码。\nmongorestore --uri=\u0026#34;mongodb://user:password@localhost:27017\u0026#34; --gzip --archive=xxx.gz 如果你的密码中包含如@等特殊字符，需要使用 uri encoded 工具将密码加密一下。\n如果你要在 mongo 中创建用户，请使用如下命令：\ndb.createUser({user:\u0026#34;user\u0026#34;,pwd:\u0026#34;你的密码\u0026#34;,roles:[{role:\u0026#34;readWrite\u0026#34;,db:\u0026#34;你的数据库\u0026#34;}]}) 如果你已经拥有账户，想赋予新的数据库操作权限，请使用如下命令：\ndb.grantRolesToUser(\u0026#39;你的用户名\u0026#39;,[{role:\u0026#39;readWrite\u0026#39;,db:\u0026#39;你的数据库\u0026#39;}]) 查看数据库的命令show dbs;\n删除数据库命令：\nuse yourdb; db.dropDatabase(); 删除 id 为 4f29e4860b2e2ecb9910e304 的数据,操作\ndb.logs.remove({\u0026#39;_id\u0026#39;:ObjectId(\u0026#39;4f29e4860b2e2ecb9910e304\u0026#39;)}) db.logs.deleteOne({\u0026#39;_id\u0026#39;:ObjectId(\u0026#39;4f29e4860b2e2ecb9910e304\u0026#39;)}) 查询文档记录\ndb.logs.find({\u0026#39;adId\u0026#39;:\u0026#39;887760470\u0026#39;}) 当报错no geo indices for geoNear，在集合中创建：\ndb.feed.createIndex({loc:\u0026#34;2dsphere\u0026#34;}) 清空 logs 表记录\ndb.logs.deleteMany({}) 修改 logs 表记录\ndb.logs.update({userId:{$in:[id1,id2]}},{$set:{\u0026#39;sex\u0026#39;:1}},{multi:true}); ","permalink":"https://blog.91demo.top/wiki/mongodb.html","summary":"\u003cp\u003eMongoDB 是一个流行的开源文档型数据库，它使用类似 JSON 的文档模型存储数据，这使得数据存储变得非常灵活。\u003c/p\u003e\n\u003cp\u003e官网地址：https://www.mongodb.com/\u003c/p\u003e\n\u003cp\u003e当你需要迁移数据库时，或者进行备份时，使用 mongodump，可以将文件导出。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emongodump -h 127.0.0.1 --username \u0026#39;xxx\u0026#39; -p \u0026#39;密码\u0026#39; --authenticationDatabase admin -d 数据库 --gzip --archive=xxx.gz\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e将数据导入到另一个服务器 mongo 数据库，或者进行升级。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emongorestore --uri=\u0026#34;mongodb://localhost:27017\u0026#34; --gzip --archive=xxx.gz\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如何你的数据库需要认证，那么请在 uri 中添加用户名和密码。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emongorestore --uri=\u0026#34;mongodb://user:password@localhost:27017\u0026#34; --gzip --archive=xxx.gz\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如果你的密码中包含如@等特殊字符，需要使用 uri encoded 工具将密码加密一下。\u003c/p\u003e\n\u003cp\u003e如果你要在 mongo 中创建用户，请使用如下命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.createUser({user:\u0026#34;user\u0026#34;,pwd:\u0026#34;你的密码\u0026#34;,roles:[{role:\u0026#34;readWrite\u0026#34;,db:\u0026#34;你的数据库\u0026#34;}]})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如果你已经拥有账户，想赋予新的数据库操作权限，请使用如下命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.grantRolesToUser(\u0026#39;你的用户名\u0026#39;,[{role:\u0026#39;readWrite\u0026#39;,db:\u0026#39;你的数据库\u0026#39;}])\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看数据库的命令\u003ccode\u003eshow dbs;\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e删除数据库命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003euse yourdb;\ndb.dropDatabase();\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e删除 id 为 4f29e4860b2e2ecb9910e304 的数据,操作\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.logs.remove({\u0026#39;_id\u0026#39;:ObjectId(\u0026#39;4f29e4860b2e2ecb9910e304\u0026#39;)})\ndb.logs.deleteOne({\u0026#39;_id\u0026#39;:ObjectId(\u0026#39;4f29e4860b2e2ecb9910e304\u0026#39;)})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查询文档记录\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.logs.find({\u0026#39;adId\u0026#39;:\u0026#39;887760470\u0026#39;})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e当报错\u003ccode\u003eno geo indices for geoNear\u003c/code\u003e，在集合中创建：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.feed.createIndex({loc:\u0026#34;2dsphere\u0026#34;})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e清空 logs 表记录\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.logs.deleteMany({})\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e修改 logs 表记录\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003edb.logs.update({userId:{$in:[id1,id2]}},{$set:{\u0026#39;sex\u0026#39;:1}},{multi:true});\n\u003c/code\u003e\u003c/pre\u003e","title":"MongoDB 常用命令"},{"content":"MySQL 是最流行的关系型数据库管理系统，在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System：关系数据库管理系统)应用软件之一。\n官网地址：https://www.mysql.com/\n备份数据\nmysqldump -uroot -p --database logs \u0026gt; logs.sql mysql 快速导入数百万级记录，这种方案可以节省很多时间。\nmysql -uroot -p create database dbname; set sql_log_bin=off; set autocommit=0; use dbname; start transaction; source dbfilename; commit; 查询用户 ID，在有规律的名称中，例如群 mKe，群 aer，等中，查找出这些群，然后取出创建者，并去重。\n在这里没有使用%模糊匹配，而使用_进行匹配一个任意字符。\nselect distinct creater_id from group_info where name like \u0026#39;群___\u0026#39;; 修改表的主键 key 自增值\nalter table users auto_increment=1000; 查询用户留存率\nselect id,DATEDIFF(now(),ifnull(login_date,add_date)) as \u0026#39;days\u0026#39; from user; 查询天数\nselect id,floor((DATEDIFF(now(),login_date))/7) as \u0026#39;groupid\u0026#39; from user where login_date != \u0026#39;\u0026#39;; select t2.* from (select id,floor((DATEDIFF(now(),login_date))/7) as \u0026#39;groupid\u0026#39; from user t1 where login_date != \u0026#39;\u0026#39;) t2 where groupid \u0026lt; 2 and groupid \u0026gt;=1; select id,floor(0.05) as \u0026#39;groupid\u0026#39; from user where login_date !=\u0026#39;\u0026#39;; select now from user ; select t1.id,now() from user t1 where id = \u0026#39;9xxxxxxx\u0026#39;; 查询周数\nselect t.weekid,count(t.id) as num from ( select id,floor((DATEDIFF(now(),login_date))/7) as \u0026#39;weekid\u0026#39; from user where login_date != \u0026#39;\u0026#39; ) as t group by t.weekid order by t.weekid 查询月数\nselect floor(groupid / 4) as mon,sum(num) from ( select t.groupid,count(t.id) as num from ( select id,floor((DATEDIFF(now(),login_date))/7) as \u0026#39;groupid\u0026#39; from user where login_date != \u0026#39;\u0026#39; ) as t group by t.groupid order by t.groupid ) t3 group by floor(groupid / 4) 查看视图定义者\nselect TABLE_SCHEMA,TABLE_NAME,DEFINER from information_schema.VIEWS; 查询待修改视图定义者，dbs@%是老的 view 定义者，app@localhost 是新的 view 定义者。\nselect concat(\u0026#34;alter DEFINER=app@localhost SQL SECURITY DEFINER VIEW \u0026#34;,TABLE_SCHEMA,\u0026#34;.\u0026#34;,TABLE_NAME,\u0026#34; as \u0026#34;,VIEW_DEFINITION,\u0026#34;;\u0026#34;) from information_schema.VIEWS where DEFINER = \u0026#39;dbs@%\u0026#39;; 在 MYSQL 命令中，再次使用得到的结果语句执行即可。\n","permalink":"https://blog.91demo.top/wiki/mysql.html","summary":"\u003cp\u003eMySQL 是最流行的关系型数据库管理系统，在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System：关系数据库管理系统)应用软件之一。\u003c/p\u003e\n\u003cp\u003e官网地址：https://www.mysql.com/\u003c/p\u003e\n\u003cp\u003e备份数据\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emysqldump -uroot -p --database logs \u0026gt; logs.sql\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003emysql 快速导入数百万级记录，这种方案可以节省很多时间。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emysql -uroot -p\n\ncreate database dbname;\n\nset sql_log_bin=off;\nset autocommit=0;\n\nuse dbname;\nstart transaction;\nsource dbfilename;\n\ncommit;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查询用户 ID，在有规律的名称中，例如群 mKe，群 aer，等中，查找出这些群，然后取出创建者，并去重。\u003c/p\u003e\n\u003cp\u003e在这里没有使用%模糊匹配，而使用_进行匹配一个任意字符。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eselect distinct creater_id from group_info where name like \u0026#39;群___\u0026#39;;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e修改表的主键 key 自增值\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ealter table users auto_increment=1000;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查询用户留存率\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eselect id,DATEDIFF(now(),ifnull(login_date,add_date)) as \u0026#39;days\u0026#39; from user;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查询天数\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eselect id,floor((DATEDIFF(now(),login_date))/7) as \u0026#39;groupid\u0026#39; from user  where login_date != \u0026#39;\u0026#39;;\n\nselect t2.* from (select id,floor((DATEDIFF(now(),login_date))/7) as \u0026#39;groupid\u0026#39; from user t1  where login_date != \u0026#39;\u0026#39;) t2 where groupid \u0026lt; 2 and groupid \u0026gt;=1;\n\n\nselect id,floor(0.05) as \u0026#39;groupid\u0026#39; from user where login_date !=\u0026#39;\u0026#39;;\n\nselect now from user ;\n\nselect  t1.id,now() from user t1 where id = \u0026#39;9xxxxxxx\u0026#39;;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查询周数\u003c/p\u003e","title":"Mysql 常用SQL语句"},{"content":"PostgreSQL 是一个免费的对象-关系数据库服务器(ORDBMS)，在灵活的 BSD 许可证下发行。\n官网地址：https://www.postgresql.org/\n数据库安装 yum install postgresql 数据库启动停止 systemctl start postgresql systemctl stop postgresql Postgresql 数据文件目录/var/lib/postgres，使用的端口5432\npg9.4 启动脚本 #!/bin/bash #su - postgres -c \u0026#34;/usr/pgsql-9.4/bin/postmaster -D /var/lib/pgsql/9.4/data/\u0026#34; su - postgres -c \u0026#34;/usr/pgsql-9.4/bin/pg_ctl -D /var/lib/pgsql/9.4/data/ -l logfile start\u0026#34; pg9.4 关闭脚本 #!/bin/bash su - postgres -c \u0026#34;kill -INT `head -1 /var/lib/pgsql/9.4/data/postmaster.pid`\u0026#34; pg9.4 安装脚本 #!/bin/bash # install pg9.4 for centos7 type wget \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 || { echo \u0026gt;\u0026amp;2 \u0026#34;I require wget but it\u0026#39;s not installed. I will install it!\u0026#34;;yum install -y wget; } echo \u0026#34;Downloading pg9.4\u0026#34; wget -c https://download.postgresql.org/pub/repos/yum/9.4/redhat/rhel-7-x86_64/pgdg-centos94-9.4-3.noarch.rpm \u0026amp;\u0026amp; rpm -ivh pgdg-centos94-9.4-3.noarch.rpm \u0026amp;\u0026amp; yum install postgresql94-server -y /usr/pgsql-9.4/bin/postgresql94-setup initdb echo \u0026#34;Install Done!\u0026#34; 登录 postgresql 使用 rd 登录 postgresql\npsql -h 192.168.11.110 -U rd -d postgres php 无法连接 pg 数据库的时候，最后最好检查一下是否开启了 selinux\n创建用户 例如创建张三\nzhangsan 为用户名，修改成你自己的用户名 123123 为密码，修改你自己的密码 postgres=\u0026gt; create user zhangsan createdb password \u0026#39;123123\u0026#39;; 创建用户，并赋予查询权限。 create role read; alter role read login; alter role password \u0026#39;123123\u0026#39;; grant select on all tables in schema public to read; 创建数据库 使用创建的用户名及密码登录\npsql -h 192.168.11.110 -U zhangsan -d postgres 例如创建数据库 demo_production\npostgres=\u0026gt; create database demo_production; 备份数据库 使用创建的用户名和密码\npg_dump -h 192.168.11.110 -U zhangsan demo_production \u0026gt; demo_production.dmp zhangsan 为用户名， demo_production 为数据库名 demo_production.dmp 为备份的文件名 备份数据库时传入密码 1，使用环境变量 PGPASSWORD\nexport PGPASSWORD=\u0026#34;PG密码\u0026#34; pg_dump -U user \u0026gt; pg.dump 或放在一行中 PGPASSWORD=\u0026#34;PG密码\u0026#34; pg_dump -U user \u0026gt; pg.dump 注意：这种方法因为安全性问题，不推荐使用。\n2，使用.pgpass 文件\n在主目录下创建.pgpass 文件，并将权限调整为 0600，文件内容为：\nhostname:port:database:username:password 还原数据库 使用创建的用户名和密码\npsql -h 192.168.11.110 -U zhangsan -d demo_production \u0026lt; demo_production.dmp 还原数据库 2 gunzip -c demo_production_20170814.gz |psql -h 192.168.10.77 -U zhangsan demo_production 创建表索引 使用索引时，需要考虑下列准则：\n索引不应该使用在较小的表上。 索引不应该使用在有频繁的大批量的更新或插入操作的表上。 索引不应该使用在含有大量的 NULL 值的列上。 索引不应该使用在频繁操作的列上。 -- 单列索引 CREATE INDEX index_name ON table_name (column_name); -- 多列索引 CREATE INDEX index_name ON table_name (column1_name, column2_name); 删除表索引 DROP INDEX index_name; 删除还有活动连接的 pg 数据库 使用下面的语句中止活动连接，然后删除数据库\nselect pg_terminate_backend(pid) from pg_stat_activity where datname=\u0026#39;nc_dove_production\u0026#39; and pid\u0026lt;\u0026gt;pg_backend_pid(); pg_stat_activity 是一个系统视图，每一行代表一个服务进程的属性和状态\nboolean pg_terminate_backend(pid int)是一个系统函数，用于终止一个后端服务进程\nint pg_backend_pid()系统函数用于获取附加到当前会话的服务器进程 ID\n查看是否有锁 select * from pg_locks where not granted; 慢查询和全表扫描的查询 select * from pg_stat_statements where calls \u0026gt; 0 order by total_time / calls desc limit 10; select * from pg_stat_all_tables where seq_scan \u0026gt; 0 order by seq_scan desc limit 10; 安装 contrib，解决没有 pg_stat_statements yum install postgresql94-contrib 然后再配置文件 postgresql.con 中配置\nshared_preload_libraries = \u0026#39;pg_stat_statements\u0026#39; # (change requires restart) pg_stat_statements.max = 1000 pg_stat_statements.track = all 最后在数据库中执行\ncreate extension pg_stat_statements 查询最大连接数 --当前总共正在使用的连接数 postgres=# select count(1) from pg_stat_activity; --显示系统允许的最大连接数 postgres=# show max_connections; --显示系统保留的用户数 postgres=# show superuser_reserved_connections ; --按照用户分组查看 select usename, count(*) from pg_stat_activity group by usename order by count(*) desc; 更新某些字符串 当你的表中有一个字段里面的内容，某些要更改，例如将 code 字段内容中的 L0 修改为 LO，请使用下面的命令。\nupdate storage_positions set code =replace(code,\u0026#39;L0\u0026#39;,\u0026#39;LO\u0026#39;); 修改列名 alter table 表名 rename column 老字段名 to 新字段名; 修改表名 alter table 老表名 rename to 新表名; 添加新字段 alter table 表名 add column 字段名 数据类型 限制; 修改字段类型 alter table 表名 alter column 字段名 type 字段类型 using 字段新类型; 或者 alter table 表名 alter 字段名 type 新字段类型; 删除字段 alter table 表名 drop 字段名; 使脚本在事务中运行 psql -l -f myscript.sql psql --single-transaction -f myscript.sql 使脚本遇到错误停止 psql -f test.sql -v ON_ERROR_STOP=on 动态制造脚本(make-script.sql) file:make-script.sql \\t on \\o script-:i.sql select sql from ( select \u0026#39;alter table \u0026#39; || n.nspname || \u0026#39;.\u0026#39; || c.relname || \u0026#39; add column last_update_timestamp timestamp with time zone default now();\u0026#39; as sql , row_number() over (order by pg_relation_size(c.oid)) from pg_class c join pg_namespace n on c.relnamespace = n.oid where n.nspname = \u0026#39;test\u0026#39; order by 2 desc) as s where row_number % 2 = :i; \\o --通过他生成两个脚本 psql -v i=0 -f make-script.sql psql -v i=1 -f make-script.sql --最后，我们并行运行这两个工作，如下： psql -f script-0.sql \u0026amp; psql -f script-1.sql \u0026amp; 更换列的类型 alter table birthday alter column dob set data type date using date(to_date(dob::text,\u0026#39;YYMMDD\u0026#39;) - (case when dob/10000 between 16 and 69 then interval \u0026#39;100 years\u0026#39; else interval \u0026#39;0\u0026#39; end)); 查询某用户是否连接到数据库 select datname from pg_stat_activity where usename=\u0026#39;demo\u0026#39;; 我希望知道“这台电脑连接了吗” select datname,usename,client_addr,client_port,application_name from pg_stat_activity; 如何在 psql 中反复执行某个查询 使用元命令 \\watch 先写入查询语句，然后执行 \\watch 5 代表每5秒执行一次 使用Ctrl + C退出 检查哪个查询在运行 首先配置文件postgresql.conf中需要设置track_activities=on;或 使用sql语句设置set track_activities = on select datname,usename,state,query from pg_stat_activity; 获取运行时间超过 1 分钟的语句 select current_timestamp -query_start as runtime, datname,usename,query from pg_stat_activity where state=\u0026#39;active\u0026#39; and current_timestamp - query_start \u0026gt; \u0026#39;1 min\u0026#39; order by 1 desc; 检查哪个查询正在运行或被阻塞 select datname,usename,query from pg_stat_activity where waiting =true; 确定谁阻塞了一个查询 select w.query as waiting_query,w.pid as waiting_pid,w.usename as waiting_user,l.query as locking_query,l.pid as locking_pid,l.usename as locking_user,t.schemaname || \u0026#39;.\u0026#39; || t.relname as tablename from pg_stat_activity w join pg_locks l1 on w.pid = l1.pid and not l1.granted join pg_locks l2 on l1.relation = l2.relation and l2.granted join pg_stat_activity l on l2.pid = l.pid join pg_stat_user_tables t on l1.relation = t.relid where w.waiting; 杀死在事务中空闲的查询 select pg_terminate_backend(pid) from pg_stat_activity where state=\u0026#39;idle in transaction\u0026#39; and current_timestamp - query_start \u0026gt; \u0026#39;10 min\u0026#39;; 理解查询变慢的原因 首先，查看监控工具，查询cpu，内存，磁盘性能是否有变化 然后分析你的数据库，使用analyse; 如何改善了目前的表现，就意味着autovacuum没有工作很好。 （1）查询返回的数据比之前明显多很多吗？ （2）查询是否单独运行时很慢？关闭其它应用连接，在数据库中进行查询，确定是否服务器已经超负荷了。记着，百万的数据0.3秒就可以完成 （3）表和索引膨胀，使用这个语句判断 select pg_relation_size(relid) as tablesize,schemaname,relname,n_live_tup from pg_stat_user_tables where relname = \u0026lt;tablename\u0026gt;; pgBadger可以分析日志，生成查询耗时报表 分析查询性能 安装pg_stat_statements,我使用的Postgresql94，在centos7系统中，执行 yum install postgresql94-contrib 即可 需要在postgresql.conf中配置 shared_preload_libraries = \u0026#39;pg_stat_statements\u0026#39; 在数据库中执行 create extension pg_stat_statements; select query,calls from pg_stat_statements order by calls desc; 查询有多少活动连接 select count(*) from pg_stat_activity; 解决占用连接的空闲查询 select pg_terminate_backend(pid) from pg_stat_activity where state=\u0026#39;idle\u0026#39; and current_timestamp - query_start \u0026gt; \u0026#39;30 min\u0026#39;; postgresql 运行在独立模式 postgresql --single -D /ful/path/to/datadir postgres 执行清理动作 清理数据库 vacuumdb postgres 自动清理命令 vacuum 重建索引 单数据库 reindexdb 所有数据库 reindexdb -a 查询未被使用得索引 select schemaname,relname,indexrelname,idx_scan from pg_stat_user_indexes order by idx_scan; 谨慎删除不必要的索引 create or replace function trial_drop_index(index text) return void language sql as $$ update pg_index set indisvalid = false where indexrelid = $1::regclass; 修改数据库表拥有者脚本 #!/bin/bash usage() { cat \u0026lt;\u0026lt; EOF usage: $0 options This script set ownership for all table, sequence and views for a given database Credit: Based on http://stackoverflow.com/a/2686185/305019 by Alex Soto Also merged changes from @sharoonthomas OPTIONS: -h Show this message -d Database name -o Owner EOF } DB_NAME= NEW_OWNER= PGSQL_USER=postgres while getopts \u0026#34;hd:o:\u0026#34; OPTION do case $OPTION in h) usage exit 1 ;; d) DB_NAME=$OPTARG ;; o) NEW_OWNER=$OPTARG ;; esac done if [[ -z $DB_NAME ]] || [[ -z $NEW_OWNER ]] then usage exit 1 fi for tbl in `psql -U $PGSQL_USER -qAt -c \u0026#34;select tablename from pg_tables where schemaname = \u0026#39;public\u0026#39;;\u0026#34; ${DB_NAME}` \\ `psql -U $PGSQL_USER -qAt -c \u0026#34;select sequence_name from information_schema.sequences where sequence_schema = \u0026#39;public\u0026#39;;\u0026#34; ${DB_NAME}` \\ `psql -U $PGSQL_USER -qAt -c \u0026#34;select table_name from information_schema.views where table_schema = \u0026#39;public\u0026#39;;\u0026#34; ${DB_NAME}` ; do psql -U $PGSQL_USER -c \u0026#34;alter table \\\u0026#34;$tbl\\\u0026#34; owner to ${NEW_OWNER}\u0026#34; ${DB_NAME} ; done postgresql 启用 SSL 1，申请服务器证书，确保\u0026#34;Common Name\u0026#34;为数据库用户名 openssl req -new -text -out server.req 2，为了去除密码，执行 openssl rsa -in privkey.pem -out server.key rm privkey.pem 3，基于 server.key 生成服务器证书 openssl req -x509 -in server.req -text -key server.key -out server.crt 4，修改 server.key 的权限 chmod og-rwx server.key 5，为了得到自己的证书，把生成的服务器证书作为受信任的根证书，只需要复制并取一个合适的名字 cp server.crt root.crt 6，三个文件放置到 pgdata 目录下，在 postgresql.conf 开启 ssl 7，修改 postgresql.conf 文件 ssl = on ssl_ca_file = \u0026#39;root.crt\u0026#39; 8，修改 pg_hba.conf 文件 hostssl all webadmin 192.168.1.0/24 md5 clientcert=1 9，重启 postgresql 数据库 systemctl restart postgresql9.4 10，客户端证书 生成客户端私钥，参考服务器端 openssl genrsa -des3 -out /tmp/postgresql.key 1024 openssl rsa -in /tmp/postgresql.key -out /tmp/postgresql.key 11，为 PostgreSQL 数据库用户创建 SSL 证书并且使用服务器上的 root.crt 文件来签名 openssl req -new -key /tmp/postgresql.key -out /tmp/postgresql.csr -subj \u0026#39;/C=CN/ST=Henan/L=Zhengzhou/O=Msyy/CN=tdmsyy/emailAddress=eagle@joyoio.com\u0026#39; openssl x509 -req -in /tmp/postgresql.csr -CA root.crt -CAkey server.key -out /tmp/postgresql.crt -CAcreateserial 12，将准备好的三个文件（postgresql.key, postgresql.crt, root.crt）放置到客户机的.postgresql 文件夹下面 chmod 0400 ~/.postgresql/postgresql.key 将 sqlite3 数据库导入到 postgresql 中 sqlite3 mjsqlite.db .dump|tee -i mypg.sql 修改表结构 SQLITE3 到 PG\nsed -i \u0026#39;s/integer primary key autoincrement/serial primary key/g;s/PRAGMA foreign_keys=OFF;//;s/datetime/timestamp/g;s/tinyint/smallint/g\u0026#39; mypg.sql 然后使用下面的语句进行导入\npsql -d database -U username -W \u0026lt; /the/path/to/sqlite-dumpfile.sql 设置某个表的自增主键，防止报错：duplicated key not allowed\nselect max(id) from w_user_points; select nextval(\u0026#39;w_user_points_id_seq\u0026#39;); select setval(\u0026#39;w_user_points_id_seq\u0026#39;,2631); 导出单张表的数据 pg_dump -h \u0026lt;主机名\u0026gt; -U \u0026lt;用户名\u0026gt; -d \u0026lt;数据库名\u0026gt; -t \u0026lt;表名\u0026gt; \u0026gt; \u0026lt;保存路径\u0026gt;/table_export.sql // 仅导出数据（不含建表语句）： // 添加 -a（--data-only）参数。 导入修改后的单表数据 psql -h \u0026lt;主机名\u0026gt; -U \u0026lt;用户名\u0026gt; -d \u0026lt;数据库名\u0026gt; -f \u0026lt;修改后的文件\u0026gt;.sql ","permalink":"https://blog.91demo.top/wiki/postgresql.html","summary":"\u003cp\u003ePostgreSQL 是一个免费的对象-关系数据库服务器(ORDBMS)，在灵活的 BSD 许可证下发行。\u003c/p\u003e\n\u003cp\u003e官网地址：https://www.postgresql.org/\u003c/p\u003e\n\u003ch2 id=\"数据库安装\"\u003e数据库安装\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eyum install postgresql\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"数据库启动停止\"\u003e数据库启动停止\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esystemctl start postgresql\nsystemctl stop postgresql\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003ePostgresql 数据文件目录\u003ccode\u003e/var/lib/postgres\u003c/code\u003e，使用的端口\u003ccode\u003e5432\u003c/code\u003e\u003c/p\u003e\n\u003ch2 id=\"pg94-启动脚本\"\u003epg9.4 启动脚本\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#!/bin/bash\n#su - postgres -c \u0026#34;/usr/pgsql-9.4/bin/postmaster -D /var/lib/pgsql/9.4/data/\u0026#34;\nsu - postgres -c \u0026#34;/usr/pgsql-9.4/bin/pg_ctl -D /var/lib/pgsql/9.4/data/ -l logfile start\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"pg94-关闭脚本\"\u003epg9.4 关闭脚本\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#!/bin/bash\nsu - postgres -c \u0026#34;kill -INT `head -1 /var/lib/pgsql/9.4/data/postmaster.pid`\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"pg94-安装脚本\"\u003epg9.4 安装脚本\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#!/bin/bash\n# install pg9.4 for centos7\ntype wget \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 || { echo \u0026gt;\u0026amp;2 \u0026#34;I require wget but it\u0026#39;s not installed. I will install it!\u0026#34;;yum install -y wget; }\necho \u0026#34;Downloading pg9.4\u0026#34;\nwget -c https://download.postgresql.org/pub/repos/yum/9.4/redhat/rhel-7-x86_64/pgdg-centos94-9.4-3.noarch.rpm \u0026amp;\u0026amp; rpm -ivh pgdg-centos94-9.4-3.noarch.rpm \u0026amp;\u0026amp; yum install postgresql94-server -y\n/usr/pgsql-9.4/bin/postgresql94-setup initdb\n\necho \u0026#34;Install Done!\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"登录-postgresql\"\u003e登录 postgresql\u003c/h2\u003e\n\u003cp\u003e使用 rd 登录 postgresql\u003c/p\u003e","title":"Postgresql 常用SQL语句"},{"content":"rsync 是一个常用的 Linux 应用程序，用于文件同步。它可以在本地计算机与远程计算机之间，或者两个本地目录之间同步文件。\nrsync 在 Linux 安装，安装命令如下：\n# ubuntu sudo apt install rsync # centos sudo yum install rsync 如果使用 Windows ，可以使用 cwRsync，它是 Windows 客户端 GUI 的一个包含 Rsync 的包装。您可以使用 cwRsync 快速远程文件备份和同步。\n基本用法 在本机使用 rsync 命令时，可以作为 cp 和 mv 命令的替代方法，将源目录同步到目标目录。\nrsync -r source destination 其中，-r 表示递归，即包含子目录。source 目录为源目录，destination 表示目标目录。\n如果有多个目录需要同步，可以写成下面这样。\nrsync -r source1 source2 destination 我们可以使用-a 参数替代-r，除了可以递归同步以外，还可以同步元信息（比如修改时间，权限）。由于 rsync 默认使用文件大小和修改时间决定文件是否需要更新，所以-a 比-r 更有用。下面的用法才是常见的写法。\nrsync -a source destination 目标目录 destination 如果不存在，rsync 会自动创建。执行上面的命令后，源目录 source 被完整地复制到目标目录 destination 下面，即形成了 destination/source 的目录结构。\n如果只想同步源目录 source 下的内容到目标目录 destination，则需要在源目录后面加上斜杠。\nrsync -a source/ destination 如果不确定 rsync 执行后会产生什么结果，我们可以用-n 或 \u0026ndash;dry-run 模拟执行结果。\nrsync -anv source/ destination -n 参数模拟命令执行结果，不真实执行命令。-v 参数将结果打印到终端。\n默认情况下，rsync 只确保源目录的所有内容都复制到目标目录。它不会使两个目录保持相同，并且不会删除文件。如果要使用目标目录成为源目录的镜像副本，使用\u0026ndash;delete 参数，这将删除只存在于目标目录，不在源目录的文件。\nrsync -av --delete source/ destination 如果我们想排除某些文件或目录，可以使用\u0026ndash;exclude 参数指定排除模式。\nrsync -av --exclude \u0026#39;*.txt\u0026#39; source/ destination 该命令将排除所有后缀为 txt 的文件。\n如果要排除隐藏文件，可以这样写--exclude \u0026quot;.*\u0026quot;，如果要排除某个目录里面的所有文件，但不排除目录本身，可以这样写--exclude 'dir1/*'，多个排除目录，可以用多个\u0026ndash;exclude 参数。也可以简写为\u0026ndash;exclude={\u0026lsquo;file1.txt\u0026rsquo;,\u0026lsquo;dir1/*\u0026rsquo;}，这个需要在 Linux 环境下。\n如果排除模式很多，我们可以将其写入一个文件，每个模式一行，然后使用参数\u0026ndash;exclude-from 指定这个文件。\nrsync -av --exclude-from=\u0026#39;exclude-file.txt\u0026#39; source/ destination 如果只想同步某类型文件，可以使用下面的语法：\nrsync -av --include=\u0026#34;*.txt\u0026#34; --exclude=\u0026#34;*\u0026#34; source/ destination rsync 除了支持本地两个目录之间的同步，也支持远程同步。它可以将本地内容，同步到远程服务器。\nrsync -av source/ username@remotehost:destination 如何 ssh 端口不为默认的 22，则使用下面的命令：\nrsync -avP -e \u0026#39;ssh -p 2222\u0026#39; --exclude=\u0026#34;*.log\u0026#34; ./test username@remotehost:destination rsync 默认使用 SSH 进行远程登录和数据传输。如果想使用 rsync 协议，需要在同步的目标服务器安装和运行 rsync 守护程序，则可以使用 rsync://协议（默认端口 873）进行传输。\nrsync -av source/ 192.168.0.1::module/destination 或 rsync -av source/ rsync://192.168.0.1/module/destination module 不是实际路径名，是 rsync 守护程序指定的一个资源名，由管理员分配。想查看 rsync 守护程序分配的 module 列表，使用下面的命令：\nrsync rsync://192.168.0.1 到重点了，rsync 的最大特点是它可以增量备份，即只复制有变动的文件。它也支持使用基准目录，即将源目录于基准目录之间变动的部分，同步到目标目录。\n具体做法是，第一次同步是全量备份，所有文件在基准目录里面同步一份。以后每一次同步都是增量备份，只同步源目录与基准目录之间有变动的部分，将这部分保存在一个新的目标目录。这个新的目标目录中，只有那些变动过的文件。\nrsync -a --delete --link-dest /compare/path /source/path /target/path 上面命令中，\u0026ndash;link-dest 参数指定基准目录/compare/path，然后源目录/source/path 跟基准目录进行比较，找出变动的文件，将它们拷贝到目标目录/target/path。那些没有变动的文件则会生成硬链接。这个命令的第一次备份是全量备份，后面就是增量备份了。\n下面是一个脚本示例，备份用户主目录。\n#!/bin/bash # A script to perform incremental backups using rsync set -o errexit set -o nounset set -o pipefail readonly SOURCE_DIR=\u0026#34;${HOME}\u0026#34; readonly BACKUP_DIR=\u0026#34;/mnt/data/backups\u0026#34; readonly DATETIME=\u0026#34;$(date \u0026#39;+%Y-%m-%d_%H:%M:%S\u0026#39;)\u0026#34; readonly BACKUP_PATH=\u0026#34;${BACKUP_DIR}/${DATETIME}\u0026#34; readonly LATEST_LINK=\u0026#34;${BACKUP_DIR}/latest\u0026#34; mkdir -p \u0026#34;${BACKUP_DIR}\u0026#34; rsync -av --delete \\ \u0026#34;${SOURCE_DIR}/\u0026#34; \\ --link-dest \u0026#34;${LATEST_LINK}\u0026#34; \\ --exclude=\u0026#34;.cache\u0026#34; \\ \u0026#34;${BACKUP_PATH}\u0026#34; rm -rf \u0026#34;${LATEST_LINK}\u0026#34; ln -s \u0026#34;${BACKUP_PATH}\u0026#34; \u0026#34;${LATEST_LINK}\u0026#34; 上面脚本中，每一次同步都会生成一个新目录${BACKUP_DIR}/${DATETIME}，并将软链接${BACKUP_DIR}/latest指向这个目录。下一次备份时，就将${BACKUP_DIR}/latest 作为基准目录，生成新的备份目录。最后，再将软链接${BACKUP_DIR}/latest 指向新的备份目录。\n注意：在 Windows 上你可以使用 WSL Linux，在 Linux 使用 rsync 要可靠稳定很多。我测试 Windows 的 cwRsync 连接 Linux 服务器报错，网上有帖子说是 SSH 问题。\n当在 WSL Linux 使用计划任务 cron 进行定时备份时，你会发现不生效？\n可以通过以下方法解决：\nusermod -a -G crontab (username) service cron start 在 WSL Linux 中，如果要 cron 服务开机自启，有点麻烦。请参考 wsl linux 文章。\nws.run \u0026#34;wsl -d Ubuntu -u root /etc/init.d/cron start\u0026#34;,vbhide 应用场景 备份你的代码，上传 mdbook 书籍到你的网站。\n","permalink":"https://blog.91demo.top/wiki/rsync.html","summary":"\u003cp\u003ersync 是一个常用的 Linux 应用程序，用于文件同步。它可以在本地计算机与远程计算机之间，或者两个本地目录之间同步文件。\u003c/p\u003e\n\u003cp\u003ersync 在 Linux 安装，安装命令如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# ubuntu\nsudo apt install rsync\n# centos\nsudo yum install rsync\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e如果使用 Windows ，可以使用 cwRsync，它是 Windows 客户端 GUI 的一个包含 Rsync 的包装。您可以使用 cwRsync 快速远程文件备份和同步。\u003c/p\u003e\n\u003ch2 id=\"基本用法\"\u003e基本用法\u003c/h2\u003e\n\u003cp\u003e在本机使用 rsync 命令时，可以作为 cp 和 mv 命令的替代方法，将源目录同步到目标目录。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ersync -r source destination\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e其中，-r 表示递归，即包含子目录。source 目录为源目录，destination 表示目标目录。\u003c/p\u003e\n\u003cp\u003e如果有多个目录需要同步，可以写成下面这样。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ersync -r source1 source2 destination\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e我们可以使用-a 参数替代-r，除了可以递归同步以外，还可以同步元信息（比如修改时间，权限）。由于 rsync 默认使用文件大小和修改时间决定文件是否需要更新，所以-a 比-r 更有用。下面的用法才是常见的写法。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ersync -a source destination\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e目标目录 destination 如果不存在，rsync 会自动创建。执行上面的命令后，源目录 source 被完整地复制到目标目录 destination 下面，即形成了 destination/source 的目录结构。\u003c/p\u003e","title":"Rsync 常用命令"},{"content":"MQTT 是一种基于发布/订阅模式的轻量级通讯协议，该协议构建于 TCP/IP 协议上，由 IBM 在 1999 年发布，它是开放消息协议，简单容易实现，消息支持 Qos。MQTT 最大优点在于，可以以极少的代码和有限的带宽，为连接远程设备提供实时可靠的消息服务。这些特性，使其在物联网、小型设备、移动应用、车联网、电力能源等方面有着较为广泛的应用。\n我是在看嵌入式资料时，了解到了 MQTT 协议。嵌入式还有一种协议叫 CoAP。今天主要讲下 MQTT。我们先看下 MQTT 协议的由来。\nMQTT 最初是由 AndyStandford-Clark 博士和 Arlen Nipper 博士于 1999 年发明的通讯协议。他们当时是为了在狭窄的网络带宽和微小电力损耗的前提之下，提供石油管道传感器和人造卫星之间一个轻量、可靠的二进制通讯协议。2011 年，IBM 和 Eurotech 将 MQTT 协议捐赠给 Eclipse 基金会，并加入了 Eclipse M2M Industry 工作组织。2014 年 10 月，MQTT 正式成为一个开放的 OASIS 国际标准。\n在看到这里的时候，我在想，如果 MQTT 不公布出来，不就是一个私有协议吗？前段时间，在学习 TCP/IP 时，我还在想一件事，TCP 连接之后，就可以发送数据，但是数据格式还需要自己进行定义，发送方和接收方都需要按照规定的格式进行发送和解析。还有就是 TCP 连接如何认证？除了 IP 白名单之外，如何在应用上实现认证？这些在看了 MQTT 规范后，这些问题都找到了答案，我不知道他们当时怎么想的，我真想掰开他们的脑子，他们是真的太厉害了。他们在 1999 年就已经完成了，现在 20 多年过去了。对于入门 TCP/IP 的我来说，这是一种精神食粮，能吃还很香。\n这两天一直在看 MQTT 的相关资料，以及理解 MQTT 协议，不知道哪篇资料写的 MQTT 是基于 IP 协议的，后来才基于 TCP/IP 协议。我个人觉得基于 IP 协议反而更贴切当时的环境，肯定是后来协议优化之后选择的 TCP/IP 协议。在说 MQTT 协议之前，先说下我心中的疑惑？我是看嵌入式资料时，知道 MQTT 的，现在的物联网很火，完全可以做到通过私有协议来连接控制，为什么要 MQTT 协议？后来，看看身边的事物，例如 USB，是为了兼容和互通。我不知道为啥选择 MQTT？开源应该算一个原因吧，MQTT 协议设计非常好算另一个原因吧。我们来看下 MQTT 的特点吧。\nMQTT 具有如下特点：\n轻量可靠：MQTT 报文格式精简、紧凑，可在严重受限的硬件设备和低带宽、高延迟的网络上实现稳定传输。 发布订阅模式：可以使发布者与订阅者解耦，实现异步协议，即订阅者和发布者不需要建立直接连接、也不需要同时在线。 为物联网而生：提供心跳机制、遗嘱消息、Qos 质量等级+离线消息、主题和安全管理，符合物联网应用特性。 生态日趋完善：实现多语言平台客户端和 SDK，多家云厂商支持并提供 IOT 服务，多种嵌入式硬件支持。 先说下 MQTT 轻量可靠是怎么做到的？\nMQTT 基于 TCP/IP，TCP/IP 是一种面向连接的、可靠的、基于字节流的通信协议。我这两天一直看的是 MQTT3.1.1 协议，就以它来讲。MQTT 包含 14 种类型的控制包，应用之间的交互就是通过这 14 种包协作来完成。这些控制包有固定的结构，分为 3 部分：固定包头，可变包头，载荷。\n这里处处都是精华，我们来细看：\n固定包头占用 2 个字节，每一个控制包都包含。第一个字节高四位，指示控制包类型。低四位是每个 MQTT 控制包类型的特殊标识。第二个字节用来指示控制包的剩余字节长度，包括可变包头的数据以及载荷。剩余长度不包含用来编码剩余长度的字节。它使用了一种可变长度结构来编码。我在此处费了一些脑细胞，我用白话来讲解下这段，我们知道一个字节一共有 8 位，可以表示十进制 256，现在，这个字节中的第 8 位用来表示是否还有下一个字节，剩下 7 位来表示长度，可以表示十进制 128。MQTT 规定，剩余长度最多可以用四个字节来表示。也就是允许这个控制包最大的长度为 256M 大小。协议给出了编码算法和解码算法，可以看 MQTT 官方协议。\n我这里拿十进制 321 来举例进行编码。你要记住，这个算法的精髓是逢 128 进位。我们将 321 整除 128 求余数，得余数 65，放在第一个字节，然后将 321 整除 128 求商，得商为 2，当为 0 时截止，因为此时商为 2，说明还有下一个字节，我们需要将第一个字节的第 8 位置 1，在计算机中，将某一位置 1，可以使用或计算。这里是将 65 和 128 的二进制进行或运算。我们来计算下一个字节，将得到的商 2，继续整除 128 求余数，得余数 2，放在第二个字节，然后将 2 整除 128 求商，得商为 0，停止计算。最后得出的结果是，第一个字节 65+128=193，第二个字节是 2。然后我们再看下上面的结果如何再解码出长度数据。我们首先对第一个字节和十进制 127（01111111）进行与运算，这一步是取出 7 位实际数据，然后乘以算法因子（算法因子是 1128128*128，即第一个字节乘 1，第二个到第四个都是乘以 128，因为是 128 进位）。得出第一个字节的值为 65，然后将第一个字节和十进制 128（10000000）进行与运算，可以算出第 8 位是否为 1，如果为 1，说明还有下一个字节。然后将第二个字节和十进制 127 进行与运算，得出 2，然后乘以 128，得出的结果 256 和第一个字节 65 相加，得出 321。然后再将第二个字节和十进制 128 进行与运算，查看第 8 位是否为 1，可以知道，这里为 0，停止运算，结果就是 321。\n固定包头讲完了，我们看看可变包头，当时我在想，写到载荷中不行吗？非得加可变包头，深度思考后我个人是这样想的，可变包头是协议定义的，载荷是用户提交的数据，意义不一样。协议中定义那么在实现 SDK 或功能时，必须实现，而最终使用者可以不关心这个，这个是我的理解。可变包头不是每个控制包都包含，MQTT 定义了有哪些控制包必须包含，例如订阅，发布，取消订阅。可变包头的内容取决于包的类型。很多类型的控制包的可变包头都包含了 2 字节的唯一标识字段。它用来唯一识别这个控制包，确保服务质量可靠性。\n载荷存在于一些控制包类型中。MQTT 定义了有哪些控制包必须包含载荷，载荷可以存放应用消息，是应用提交的数据。如果用有用功来表示的话，这部分数据对于应用来说才是有用功。\n在前面介绍我入门 TCP 后的问题中，我提到了需要自定义数据格式，这里 MQTT 规定了数据格式，并且设计的非常精妙，完美解决数据格式问题。那么它是如何解决认证问题呢？MQTT 在 CONNECT 控制包中，可变包头中指定了连接标识，当 UserNameFlag 设置为 1 时，用户名必须出现在载荷中，当 PasswordFlag 设置为 1 时，密码必须出现在载荷中。这样在协议中就解决了认证问题，不依赖服务器 IP 白名单。\n最后，再来说说质量服务 Qos，这个也是核心精髓，MQTT 根据质量服务等级来分发应用消息。Qos 分为 3 个等级：Qos0，最多分发一次，Qos1，最少分发一次，Qos2，精确一次分发。随着服务质量提高，会增加相应的开销。\n在 Qos0 等级下，消息只发送一次，不管接收者是否收到。我们看下流程图：\n发送者 控制包 接收者 PUBLISH Qos 0，DUP=0 \u0026mdash;\u0026mdash;\u0026gt; 分发消息给合适的接收者 在 Qos1 等级下，确保消息至少一次抵达接收者。Qos1 中的 PUBLISH 包的可变包头中包含唯一标识，而且有 PUBACK 包确认。我们看下流程图：\n发送者 控制包 接收者 存储消息，PUBLISH Qos 1,DUP 0,包唯一标识 \u0026mdash;\u0026mdash;\u0026gt; 接收并转发消息 销毁消息 \u0026lt;\u0026mdash;\u0026mdash; 发送 PUBACK 包唯一标识 在 Qos2 等级下，确保消息精确一次分发，不能丢失不能重复。这种服务质量会增加开销。Qos2 消息可变包头中包含唯一标识。Qos2 的 PUBLISH 包的接收者分两步确认接收。我们看下流程图：\n发送者 控制包 接收者 存储消息，PUBLISH Qos 2,DUP 0,包唯一标识 \u0026mdash;\u0026mdash;\u0026gt; 方法 A，存储消息 销毁消息，存储 PUBREC 包 \u0026lt;\u0026mdash;\u0026mdash; PUBREC 包唯一标识 PUBREL 包唯一标识 \u0026mdash;\u0026mdash;\u0026gt; 方法 A，转发消息并销毁消息 销毁存储状态 \u0026lt;\u0026mdash;\u0026mdash; PUBCOMP 包唯一标识 为什么 Qos2 消息不会重复？与 Qos1 相比，Qos2 增加了 PUBREL 报文和 PUBCOMP 报文，在使用 Qos1 消息时，对接收方来说，回复完 PUBACK 报文后，Packet ID 就可以重新使用了，也不管是否确实发送到了发送方。这样，当收到相同 Packet ID 报文后，无法得知时发送方因为没有收到响应而重传，还是发送方收到了响应而重新使用 Packet ID，发送了一个全新的消息。所以，消息去重的关键就在于，通信双方如何正确的同步释放 Packet ID。在 Qos2 中增加的 PUBREL 流程，就是帮助通信双方 Packet ID 何时重用的能力。Qos2 规定，发送方只有收到 PUBREC 报文之前可以重传 PUBLISH 报文。一旦收到 PUBREC 报文并发出 PUBREL 报文，发送方就进入了 Packet ID 释放流程，不可以再使用当前 PacketID 重传 PUBLISH 报文。同时，在收到对端回复的 PUBCOMP 报文确认双方都完成 Packet ID 释放之前，也不可以使用当前 Packet ID 发送新的消息。\n我们用白话来描述一下 Qos 的三种消息。发送方：小张，接收方：小李，中间人：小王，消息：晚上一块打球，场景：一间教室。\nQos0，小张写了一张纸条晚上一块打球，裹成了纸团，传给了小王，小王又将纸团传给了小李，小李收到后知道晚上一块打球。第二天，小张写了一张纸条晚上一块打球，裹成了纸团，传给了小王。但传的时候，纸团掉在了地上。消息丢失。\nQos1，小张写了一张纸条晚上一块打球，并写了一个编号 101，裹成了纸团，传给了小王，小王又将纸团传给了小李，小李收到后知道晚上一块打球，并回了小张一个纸条，101 消息已经收到。第二天，小张写了一张纸条晚上一块打球，并写了一个编号 101，裹成了纸团，传给了小王。但传的时候，纸团掉在了地上。小张等了约定的时间后，发现小李没有回复消息，就又写了一张编号 101 的消息传给了小李。这次很顺利，小李收到之后马上回复。第三天，小张继续传送 101 纸条，小李在回复的时候，小王又给弄掉了，小张继续发送 101 纸条，小李收到后，还是回复了对方消息已收到，只是小李不知道这个是新消息还是老消息。\nQos2，这次，小张买了信封，并和小李约定，收到信封后马上回复我收到信封，看了内容之后，再把信封还给我。所以这次小张将写的晚上一块打球的纸条添加到了信封中，当小李收到后，要回复小纸条信已收到，如果到规定时间不回复，小张继续发 101 信封，当小张收到信已收到的纸条后，将不再发送 101 信封，小张写小纸条让小李还回来 101 信封，当小张收到 101 信封后，就可以继续使用 101 信封写新的小纸条了。对于小李来说，它只收到了一条消息。这里，只是表达这个意思，在计算机中，包可以进行多次发送。\nMQTT 通过 Qos 保证了消息可靠性。最后，我们来说说发布订阅，这也是一个亮点。我们假设没有发布订阅，那么发送者和接受者需要直接连接，或者通过中转器，无论哪一种，发送者个接受者都绑定到了一起。这让我想到了以前一个老总讲的医药厂家和医院的关系，当没有代理商的时候，医药厂家需要和每个医院建立关系，而医院也需要和每个医药厂家建立关系，对于医药厂家，医院都会有巨大的管理成本。如果引入代理商，将会解决这个问题，首先对于医院来说，它只需要对接这个代理商即可，对于厂家来说，也是只需要对接这个代理商即可。我不知道当时两位博士是否知道这个代理商的故事，但是我觉得应该有这方面的考量。在引入这个代理商后，当多家医院向代理商订购了某个厂家的药后，一旦该厂家生产出来。代理商会快速的分发到订购的各家医院。同理，在 MQTT 中，发布者和订阅者也是这样的关系，发布者不知道订阅者是谁？它们进行了解耦。在计算机领域，这样的模式可以使多家硬件厂商，软件厂商进行协作沟通。这应该就是 MQTT 的魅力所在吧。发布者和订阅者这里通过主题进行关联。首先，订阅者需要订阅主题，发布者会向主题上发送消息。订阅者可以订阅多个主题。我们还拿医院和厂家来举例子，如果医院要买感冒药，那么这里买感冒药可以认为是个主题，那么无论哪个厂家生产出来的感冒药都可以卖给它。如果医院要买某个厂家的感冒药，那么只有这个厂家的感冒药生产出来可以卖给它。这又带出来 MQTT 协议安全管理的话题。在这里，代理商会管理这些。比如上面说的 A 医院订购 A 厂家的药，如果是 B 厂家生产的感冒药，代理商不允许分配给 A 医院。同样的，厂家也可以指定只有某医院可以使用他们的药。例如特类品种药，代理商会管理只有某个医院可以订购。在 MQTT 协议中，这些是通过绑定主题，以及配置授权策略来实现的。\nMQTT 的遗嘱消息，这里也聊一下。在 MQTT 协议中，遗嘱的建立是发布者在连接的时候就确定了的，当连接到 Broker 时，连接的遗嘱标识会设置，遗嘱信息，遗嘱主题以及遗嘱 Qos 等信息会放在载荷中。当这个客户端没有发送 DISCONNECT 控制包，就断开了连接，那么 Broker 要将遗嘱消息分发给订阅了遗嘱主题的客户端。这个遗嘱消息常用来监控客户端非正常断开连接。这里依石油管道传感器举例，在真实环境中，传感器可能突然断开连接，在非常断开的时候，Broker 会将遗嘱消息发给订阅者，这样管理人员能够知道是哪个传感器出了问题，方便下一步操作。\n更多的内容，请参考官方文档。\nhttps://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html\n","permalink":"https://blog.91demo.top/wiki/mqtt.html","summary":"\u003cp\u003eMQTT 是一种基于发布/订阅模式的轻量级通讯协议，该协议构建于 TCP/IP 协议上，由 IBM 在 1999 年发布，它是开放消息协议，简单容易实现，消息支持 Qos。MQTT 最大优点在于，可以以极少的代码和有限的带宽，为连接远程设备提供实时可靠的消息服务。这些特性，使其在物联网、小型设备、移动应用、车联网、电力能源等方面有着较为广泛的应用。\u003c/p\u003e\n\u003cp\u003e我是在看嵌入式资料时，了解到了 MQTT 协议。嵌入式还有一种协议叫 CoAP。今天主要讲下 MQTT。我们先看下 MQTT 协议的由来。\u003c/p\u003e\n\u003cp\u003eMQTT 最初是由 AndyStandford-Clark 博士和 Arlen Nipper 博士于 1999 年发明的通讯协议。他们当时是为了在狭窄的网络带宽和微小电力损耗的前提之下，提供石油管道传感器和人造卫星之间一个轻量、可靠的二进制通讯协议。2011 年，IBM 和 Eurotech 将 MQTT 协议捐赠给 Eclipse 基金会，并加入了 Eclipse M2M Industry 工作组织。2014 年 10 月，MQTT 正式成为一个开放的 OASIS 国际标准。\u003c/p\u003e\n\u003cp\u003e在看到这里的时候，我在想，如果 MQTT 不公布出来，不就是一个私有协议吗？前段时间，在学习 TCP/IP 时，我还在想一件事，TCP 连接之后，就可以发送数据，但是数据格式还需要自己进行定义，发送方和接收方都需要按照规定的格式进行发送和解析。还有就是 TCP 连接如何认证？除了 IP 白名单之外，如何在应用上实现认证？这些在看了 MQTT 规范后，这些问题都找到了答案，我不知道他们当时怎么想的，我真想掰开他们的脑子，他们是真的太厉害了。他们在 1999 年就已经完成了，现在 20 多年过去了。对于入门 TCP/IP 的我来说，这是一种精神食粮，能吃还很香。\u003c/p\u003e\n\u003cp\u003e这两天一直在看 MQTT 的相关资料，以及理解 MQTT 协议，不知道哪篇资料写的 MQTT 是基于 IP 协议的，后来才基于 TCP/IP 协议。我个人觉得基于 IP 协议反而更贴切当时的环境，肯定是后来协议优化之后选择的 TCP/IP 协议。在说 MQTT 协议之前，先说下我心中的疑惑？我是看嵌入式资料时，知道 MQTT 的，现在的物联网很火，完全可以做到通过私有协议来连接控制，为什么要 MQTT 协议？后来，看看身边的事物，例如 USB，是为了兼容和互通。我不知道为啥选择 MQTT？开源应该算一个原因吧，MQTT 协议设计非常好算另一个原因吧。我们来看下 MQTT 的特点吧。\u003c/p\u003e","title":"白话消息队列遥测传输协议（MQTT）"},{"content":"编程开发架构的发展史充满了许多重要的里程碑。以下是按时间顺序列出的编程开发架构发展史中的一些关键事件：\n1940 年代 - 单机编程\n早期计算机使用机器语言和汇编语言进行单机编程，程序员直接在单台计算机上编写和运行代码。\n1960 年代 - 批处理系统\n批处理系统允许计算机一次处理一批任务，程序员将代码提交到计算机中心，代码被集中处理后返回结果。\n1960 年代 - 时分多任务操作系统\n开发了时分多任务操作系统（如 IBM 的 OS/360），允许多个用户共享计算机资源，支持多任务处理。\n1970 年代 - 分时系统\n分时系统（如 Multics 和 UNIX）允许多个用户同时使用计算机，通过时间片轮转机制实现资源共享。\n1980 年代 - 客户端/服务器架构\n客户端/服务器架构（Client/Server Architecture）兴起，客户端应用程序与服务器进行通信，服务器处理数据并返回结果。\n1990 年代 - 三层架构\n三层架构（Three-Tier Architecture）被广泛采用，分为表示层（用户界面）、逻辑层（业务逻辑）和数据层（数据库）。\n1990 年代 - Web 开发\n万维网（World Wide Web）的兴起带来了 Web 开发，使用 HTML、CSS 和 JavaScript 开发网页，服务器端使用 CGI、PHP、ASP 等技术。\n2000 年代 - 服务导向架构（SOA）\n服务导向架构（SOA）提出，通过定义服务接口，应用程序可以相互通信和集成，促进了分布式系统的发展。\n2006 年 - 云计算\n亚马逊推出 AWS（亚马逊网络服务），开启了云计算时代，开发者可以按需使用计算资源，提升了应用程序的可扩展性和灵活性。\n2010 年代 - 微服务架构\n微服务架构（Microservices Architecture）逐渐流行，将应用程序拆分为多个小的、独立部署的服务，每个服务负责特定的功能。\n2015 年 - 容器化技术\nDocker 等容器化技术的普及，使得应用程序及其依赖打包为轻量级容器，简化了部署和管理。\n2015 年 - 无服务器架构\n无服务器架构（Serverless Architecture）兴起，开发者可以编写函数作为服务（FaaS），无需管理服务器基础设施。\n2020 年代 - 边缘计算\n边缘计算（Edge Computing）越来越重要，将计算和数据存储从数据中心移到靠近数据源的位置，以降低延迟和提高响应速度。\n这些事件只是编程开发架构发展史中的一部分，它们共同推动了软件架构的演进，并对现代软件开发产生了深远的影响。\n","permalink":"https://blog.91demo.top/wiki/devarch.html","summary":"\u003cp\u003e编程开发架构的发展史充满了许多重要的里程碑。以下是按时间顺序列出的编程开发架构发展史中的一些关键事件：\u003c/p\u003e\n\u003cp\u003e1940 年代 - 单机编程\u003c/p\u003e\n\u003cp\u003e早期计算机使用机器语言和汇编语言进行单机编程，程序员直接在单台计算机上编写和运行代码。\u003c/p\u003e\n\u003cp\u003e1960 年代 - 批处理系统\u003c/p\u003e\n\u003cp\u003e批处理系统允许计算机一次处理一批任务，程序员将代码提交到计算机中心，代码被集中处理后返回结果。\u003c/p\u003e\n\u003cp\u003e1960 年代 - 时分多任务操作系统\u003c/p\u003e\n\u003cp\u003e开发了时分多任务操作系统（如 IBM 的 OS/360），允许多个用户共享计算机资源，支持多任务处理。\u003c/p\u003e\n\u003cp\u003e1970 年代 - 分时系统\u003c/p\u003e\n\u003cp\u003e分时系统（如 Multics 和 UNIX）允许多个用户同时使用计算机，通过时间片轮转机制实现资源共享。\u003c/p\u003e\n\u003cp\u003e1980 年代 - 客户端/服务器架构\u003c/p\u003e\n\u003cp\u003e客户端/服务器架构（Client/Server Architecture）兴起，客户端应用程序与服务器进行通信，服务器处理数据并返回结果。\u003c/p\u003e\n\u003cp\u003e1990 年代 - 三层架构\u003c/p\u003e\n\u003cp\u003e三层架构（Three-Tier Architecture）被广泛采用，分为表示层（用户界面）、逻辑层（业务逻辑）和数据层（数据库）。\u003c/p\u003e\n\u003cp\u003e1990 年代 - Web 开发\u003c/p\u003e\n\u003cp\u003e万维网（World Wide Web）的兴起带来了 Web 开发，使用 HTML、CSS 和 JavaScript 开发网页，服务器端使用 CGI、PHP、ASP 等技术。\u003c/p\u003e\n\u003cp\u003e2000 年代 - 服务导向架构（SOA）\u003c/p\u003e\n\u003cp\u003e服务导向架构（SOA）提出，通过定义服务接口，应用程序可以相互通信和集成，促进了分布式系统的发展。\u003c/p\u003e\n\u003cp\u003e2006 年 - 云计算\u003c/p\u003e\n\u003cp\u003e亚马逊推出 AWS（亚马逊网络服务），开启了云计算时代，开发者可以按需使用计算资源，提升了应用程序的可扩展性和灵活性。\u003c/p\u003e\n\u003cp\u003e2010 年代 - 微服务架构\u003c/p\u003e","title":"编程开发架构发展史"},{"content":"编程开发的发展史充满了许多重要的里程碑。以下是按时间顺序列出的编程开发史中的一些关键事件：\n1940 年代 - 机器语言\n机器语言是计算机的最基本的编程语言，直接用二进制代码来控制计算机硬件。\n1950 年代 - 汇编语言\n汇编语言（Assembly Language）引入了助记符，简化了编程过程，使程序员能够更容易地编写代码。\n1957 年 - Fortran\nIBM 发布了 Fortran（Formula Translation），这是第一个高层次编程语言，主要用于科学和工程计算。\n1959 年 - COBOL\nCOBOL（Common Business-Oriented Language）被开发出来，主要用于商业数据处理。\n1960 年 - LISP\n由 John McCarthy 开发的 LISP（LISt Processing）语言，成为人工智能研究的主要语言之一。\n1964 年 - BASIC\n约翰·肯尼和托马斯·库尔茨开发了 BASIC（Beginner\u0026rsquo;s All-purpose Symbolic Instruction Code），旨在为初学者提供简单的编程语言。\n1970 年 - Pascal\nNiklaus Wirth 开发了 Pascal 语言，主要用于教学和系统编程。\n1972 年 - C 语言\nDennis Ritchie 在贝尔实验室开发了 C 语言，它结合了高效和灵活性，成为系统编程的标准语言。\n1980 年 - Ada\n由美国国防部开发的 Ada 语言，旨在支持大型、复杂系统的开发。\n1983 年 - C++\nBjarne Stroustrup 开发了 C++，在 C 语言的基础上增加了面向对象编程特性。\n1987 年 - Perl\nLarry Wall 开发了 Perl 语言，特别适合文本处理和系统管理任务。\n1991 年 - Python\nGuido van Rossum 开发了 Python 语言，强调代码的可读性和简洁性，适用于各种应用程序开发。\n1995 年 - Java\nSun Microsystems 发布了 Java 语言，由 James Gosling 主导开发，具有跨平台特性，广泛用于企业级应用和移动应用开发。\n1995 年 - JavaScript\nBrendan Eich 在 Netscape 公司开发了 JavaScript 语言，成为 Web 开发的主要语言之一。\n2000 年 - C#\n微软发布了 C#语言，作为其.NET 框架的一部分，广泛用于 Windows 平台的应用程序开发。\n2003 年 - Scala\nMartin Odersky 开发了 Scala 语言，结合了面向对象和函数式编程特性，运行在 JVM 上。\n2009 年 - Go\nGoogle 发布了 Go 语言（也称为 Golang），由 Robert Griesemer、Rob Pike 和 Ken Thompson 主导开发，强调并发编程和性能。\n2010 年 - Rust\nMozilla 发布了 Rust 语言，旨在提供高效、安全的系统编程语言，避免内存管理问题。\n2014 年 - Swift\n苹果公司发布了 Swift 语言，作为 Objective-C 的替代品，用于 iOS 和 macOS 应用程序开发。\n这些事件只是编程开发史中的一部分，它们共同推动了编程语言和开发工具的发展，并对软件开发行业产生了深远的影响。\n","permalink":"https://blog.91demo.top/wiki/devlang.html","summary":"\u003cp\u003e编程开发的发展史充满了许多重要的里程碑。以下是按时间顺序列出的编程开发史中的一些关键事件：\u003c/p\u003e\n\u003cp\u003e1940 年代 - 机器语言\u003c/p\u003e\n\u003cp\u003e机器语言是计算机的最基本的编程语言，直接用二进制代码来控制计算机硬件。\u003c/p\u003e\n\u003cp\u003e1950 年代 - 汇编语言\u003c/p\u003e\n\u003cp\u003e汇编语言（Assembly Language）引入了助记符，简化了编程过程，使程序员能够更容易地编写代码。\u003c/p\u003e\n\u003cp\u003e1957 年 - Fortran\u003c/p\u003e\n\u003cp\u003eIBM 发布了 Fortran（Formula Translation），这是第一个高层次编程语言，主要用于科学和工程计算。\u003c/p\u003e\n\u003cp\u003e1959 年 - COBOL\u003c/p\u003e\n\u003cp\u003eCOBOL（Common Business-Oriented Language）被开发出来，主要用于商业数据处理。\u003c/p\u003e\n\u003cp\u003e1960 年 - LISP\u003c/p\u003e\n\u003cp\u003e由 John McCarthy 开发的 LISP（LISt Processing）语言，成为人工智能研究的主要语言之一。\u003c/p\u003e\n\u003cp\u003e1964 年 - BASIC\u003c/p\u003e\n\u003cp\u003e约翰·肯尼和托马斯·库尔茨开发了 BASIC（Beginner\u0026rsquo;s All-purpose Symbolic Instruction Code），旨在为初学者提供简单的编程语言。\u003c/p\u003e\n\u003cp\u003e1970 年 - Pascal\u003c/p\u003e\n\u003cp\u003eNiklaus Wirth 开发了 Pascal 语言，主要用于教学和系统编程。\u003c/p\u003e\n\u003cp\u003e1972 年 - C 语言\u003c/p\u003e\n\u003cp\u003eDennis Ritchie 在贝尔实验室开发了 C 语言，它结合了高效和灵活性，成为系统编程的标准语言。\u003c/p\u003e\n\u003cp\u003e1980 年 - Ada\u003c/p\u003e","title":"编程语言发展史"},{"content":"当我的点数为 0 时，发现点击文章列表，进入到文章详情，无法查看文章内容。\n以前没有发现这个问题，是因为我的账户中有点数。\n造成的原因是加锁文章逻辑问题。\n原来的代码为：\nif u.Points \u0026lt; 1 { return nil, errors.New(\u0026#34;点数不足\u0026#34;) } 优化后的代码为：\n// 判断是否加锁文章 if b.IsLock == 1 { // 只有不是自己的文章才计算点数 if bopenid != openid { if u.Points \u0026lt; 1 { return nil, errors.New(\u0026#34;点数不足\u0026#34;) } // 给文章作者奖励 VTransAddPoints(bopenid, global.ViewLockArtA, 1, uuid) // 扣减读者 VTransSubPoints(openid, global.ViewLockArtD, 1, uuid) } } ","permalink":"https://blog.91demo.top/wiki/fixartcontentnotfound.html","summary":"\u003cp\u003e当我的点数为 0 时，发现点击文章列表，进入到文章详情，无法查看文章内容。\u003c/p\u003e\n\u003cp\u003e以前没有发现这个问题，是因为我的账户中有点数。\u003c/p\u003e\n\u003cp\u003e造成的原因是加锁文章逻辑问题。\u003c/p\u003e\n\u003cp\u003e原来的代码为：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eif u.Points \u0026lt; 1 {\n    return nil, errors.New(\u0026#34;点数不足\u0026#34;)\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e优化后的代码为：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 判断是否加锁文章\nif b.IsLock == 1 {\n    // 只有不是自己的文章才计算点数\n    if bopenid != openid {\n    \tif u.Points \u0026lt; 1 {\n    \t\treturn nil, errors.New(\u0026#34;点数不足\u0026#34;)\n    \t}\n    \t// 给文章作者奖励\n    \tVTransAddPoints(bopenid, global.ViewLockArtA, 1, uuid)\n    \t// 扣减读者\n    \tVTransSubPoints(openid, global.ViewLockArtD, 1, uuid)\n    }\n}\n\u003c/code\u003e\u003c/pre\u003e","title":"豆子碎片文章内容无法查看解决方法"},{"content":"豆子碎片是展示和搜索文章的微信小程序。它的项目名称是 visit，该项目经历多个版本迭代。现在已经成熟和稳定。\nvisit 项目在小程序端的二维码：\nVisit 项目地址 https://gitee.com/littletow/visit\n云环境版本 visit 项目使用小程序云环境，在云存储中存放文章的 markdown 文件，然后小程序调用云函数获取使用 towxml 包转换后的 json 数据，在小程序端进行渲染。新的 towxml 组件必须使用微信小程序基础库 2.9.4 版本以上，才能正常显示。该项目在搜索时，是根据文章标题和关键字进行模糊查询检索匹配的。在云环境开始收费后，转为服务器版本。服务器灵活性强，可操作空间大，可以有多种用途。\n服务器版本 visit 项目当前使用服务器版本，在服务器使用数据库存储 markdown 文件的数据。小程序调用 API 开放接口从后台获取文件的数据，在前端使用 towxml 组件进行渲染。\n注意：小程序端只有文章显示和搜索功能。 主要是方便上架审核。上传和管理文章等通过后台接口或工具完成，文章使用 Markdown 格式。\n当制作好文档后，需要使用工具或者 API 开放接口上传到后台。\n使用工具上传，工具请使用开源项目 upart-go 项目 使用 API 开放接口上传，API 接口文档请访问开放接口 项目介绍 visit 项目主要是为了在小程序端记录和分享技术文章，也是学习小程序的入门项目。上传的文章以编程技术或技术相关经验为主题。小程序包含首页和我的两个栏目，首页显示公开的和自己上传的文章，可通过关键字搜索文章，搜索文章时，是通过文章标题或关键字进行模糊查询检索匹配。也可以使用快捷按钮检索文章，快捷按钮目前包含 3 个：最新，最近上传的文章；最火，浏览量最高的文章；最冷，浏览量最低的文章。我的页面只有两个功能，一个是获取我的识别码，使用工具，或者开放接口时需要用到此识别码，用作 API 接口认证。另一个是看广告获取点数。上传文章是需要豆子点数的。加入广告，是为了希望获取一点收益，承担一点我的服务器支出。文章可通过工具上传，工具可使用 upart-go 开源项目。如果要公开文章，那么公开的文章需要后台审核。文章也可以加锁，当加锁后，其它用户访问你的文章，你将获得豆子点数奖励。\n直接在 Markdown 文件中添加的图片链接在小程序端是无法打开的。如果确实需要在文章中显示图片，可以将图床域名地址告知我，添加到小程序域名白名单中。\n目前有一个方案是微信公众号图片小程序不会过滤，可以将您的图片上传到微信公众号，然后获取图片链接，添加到 markdown 文件中。\n在 Markdown 文件中添加的 HTTP 和 HTTPS 链接在小程序端无法打开。\n豆子碎片完整使用由四部分应用组成：小程序端，上传文章工具，Web 审核后台，以及后台接口服务。\n小程序端，用来显示和搜索文章。 上传文章工具，用于上传文章，以及管理文章。 Web 审核后台，用来管理待审核的文章。 后台接口服务，为上面三个终端提供服务，将数据存储在服务器，并处理数据。 其中，小程序端和上传文章工具已开源，其它两项也将以教程的方式持续的发布在豆子笔记中。\n注意，该项目是开源项目，请勿上传非法或重要敏感资料。\n豆子碎片如何录入文章？ 在我们使用 markdown 格式写好内容后，我们可以使用命令行上传工具进行上传文章。\n命令行工具下载地址，https://gitee.com/littletow/upart-go/releases/download/v2.0.0/upart-go.zip\n它使用 Go 语言开发，是个开源项目。有以下功能：\n上传文章 搜索文章 删除文章 更新文章标题、关键字、文章内容、是否加锁、是否公开 下载工具后，解压缩到某个目录，需要修改配置文件中的 icode,isecret。这两个值可以从豆子碎片小程序中获取。\n更多内容，可以查看命令行帮助。\n","permalink":"https://blog.91demo.top/wiki/visit-project.html","summary":"\u003cp\u003e豆子碎片是展示和搜索文章的微信小程序。它的项目名称是 visit，该项目经历多个版本迭代。现在已经成熟和稳定。\u003c/p\u003e\n\u003cp\u003evisit 项目在小程序端的二维码：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"豆子碎片\" loading=\"lazy\" src=\"https://ant.91demo.top/imgs/visit.webp#pic_center\"\u003e\u003c/p\u003e\n\u003ch2 id=\"visit-项目地址\"\u003eVisit 项目地址\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://gitee.com/littletow/visit\"\u003ehttps://gitee.com/littletow/visit\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"云环境版本\"\u003e云环境版本\u003c/h2\u003e\n\u003cp\u003evisit 项目使用小程序云环境，在云存储中存放文章的 markdown 文件，然后小程序调用云函数获取使用 towxml 包转换后的 json 数据，在小程序端进行渲染。新的 towxml 组件必须使用微信小程序基础库 2.9.4 版本以上，才能正常显示。该项目在搜索时，是根据文章标题和关键字进行模糊查询检索匹配的。在云环境开始收费后，转为服务器版本。服务器灵活性强，可操作空间大，可以有多种用途。\u003c/p\u003e\n\u003ch2 id=\"服务器版本\"\u003e服务器版本\u003c/h2\u003e\n\u003cp\u003evisit 项目当前使用服务器版本，在服务器使用数据库存储 markdown 文件的数据。小程序调用 API 开放接口从后台获取文件的数据，在前端使用 towxml 组件进行渲染。\u003c/p\u003e\n\u003cp\u003e注意：小程序端只有文章显示和搜索功能。 主要是方便上架审核。上传和管理文章等通过后台接口或工具完成，文章使用 Markdown 格式。\u003c/p\u003e\n\u003cp\u003e当制作好文档后，需要使用工具或者 API 开放接口上传到后台。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e使用工具上传，工具请使用开源项目 \u003ca href=\"https://gitee.com/littletow/upart-go\"\u003eupart-go 项目\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e使用 API 开放接口上传，API 接口文档请访问\u003ca href=\"https://www.91demo.top/zh-cn/api/index.html\"\u003e开放接口\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"项目介绍\"\u003e项目介绍\u003c/h2\u003e\n\u003cp\u003evisit 项目主要是为了在小程序端记录和分享技术文章，也是学习小程序的入门项目。上传的文章以编程技术或技术相关经验为主题。小程序包含首页和我的两个栏目，首页显示公开的和自己上传的文章，可通过关键字搜索文章，搜索文章时，是通过文章标题或关键字进行模糊查询检索匹配。也可以使用快捷按钮检索文章，快捷按钮目前包含 3 个：最新，最近上传的文章；最火，浏览量最高的文章；最冷，浏览量最低的文章。我的页面只有两个功能，一个是获取我的识别码，使用工具，或者开放接口时需要用到此识别码，用作 API 接口认证。另一个是看广告获取点数。上传文章是需要豆子点数的。加入广告，是为了希望获取一点收益，承担一点我的服务器支出。文章可通过工具上传，工具可使用 upart-go 开源项目。如果要公开文章，那么公开的文章需要后台审核。文章也可以加锁，当加锁后，其它用户访问你的文章，你将获得豆子点数奖励。\u003c/p\u003e\n\u003cp\u003e直接在 Markdown 文件中添加的图片链接在小程序端是无法打开的。如果确实需要在文章中显示图片，可以将图床域名地址告知我，添加到小程序域名白名单中。\u003cbr\u003e\n目前有一个方案是微信公众号图片小程序不会过滤，可以将您的图片上传到微信公众号，然后获取图片链接，添加到 markdown 文件中。\u003c/p\u003e\n\u003cp\u003e在 Markdown 文件中添加的 HTTP 和 HTTPS 链接在小程序端无法打开。\u003c/p\u003e\n\u003cp\u003e豆子碎片完整使用由四部分应用组成：小程序端，上传文章工具，Web 审核后台，以及后台接口服务。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e小程序端，用来显示和搜索文章。\u003c/li\u003e\n\u003cli\u003e上传文章工具，用于上传文章，以及管理文章。\u003c/li\u003e\n\u003cli\u003eWeb 审核后台，用来管理待审核的文章。\u003c/li\u003e\n\u003cli\u003e后台接口服务，为上面三个终端提供服务，将数据存储在服务器，并处理数据。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e其中，小程序端和上传文章工具已开源，其它两项也将以教程的方式持续的发布在\u003ca href=\"https://www.91demo.top\"\u003e豆子笔记\u003c/a\u003e中。\u003c/p\u003e","title":"豆子碎片小程序项目介绍"},{"content":"在使用 IP 归属地这么长时间，我发现这个作用越来越大了。今天将源文件重新维护了下，主要是规范省份名称，例如河南调整为河南省，内蒙古调整为内蒙古自治区等，调整之后，源文件明显增大了尺寸，但是为了严谨，是值得的。\n因为我的后台，访问地点是固定的，通过白名单设置 IP 太麻烦，就用 IP 归属地进行拦截。\n我精确到省份，其实可以精确到市，我的 IP 库精确到市。\n添加还是非常简单的，利用开放接口即可。\n我这里不用使用，可以直接调用函数，如果是两个服务，就需要开放接口了。\n先获取归属地，归属地格式，国家|省份|城市，国外的只显示国家名，其它项为 0，返回字符串，以|分割。\n取到归属地字符串后，切分字符串为数组，取出省份。\nstrArr := strings.Split(str, \u0026#34;|\u0026#34;) n := len(strArr) // 这个情况基本不存在，源文件中以0作为占位符。 if n != 3 { return \u0026#34;\u0026#34; } return strArr[1] 归属地数组索引 0 是国家，1 是省份，2 是城市。\n现在，使用获取到的省份信息比对。\n以前，是直接将省份字段和“河南”比较，现在优化了一下，包含就可以。防止数据文件出现河南省这样的情况导致问题。\nok := strings.Contains(province, \u0026#34;河南\u0026#34;) if !ok { c.JSON(http.StatusOK, ErrWxResp(\u0026#34;拒绝登录-1\u0026#34;, nil)) return } 这样，归属地不是河南将不允许登录。\n","permalink":"https://blog.91demo.top/wiki/addipterritory.html","summary":"\u003cp\u003e在使用 IP 归属地这么长时间，我发现这个作用越来越大了。今天将源文件重新维护了下，主要是规范省份名称，例如河南调整为河南省，内蒙古调整为内蒙古自治区等，调整之后，源文件明显增大了尺寸，但是为了严谨，是值得的。\u003c/p\u003e\n\u003cp\u003e因为我的后台，访问地点是固定的，通过白名单设置 IP 太麻烦，就用 IP 归属地进行拦截。\u003c/p\u003e\n\u003cp\u003e我精确到省份，其实可以精确到市，我的 IP 库精确到市。\u003c/p\u003e\n\u003cp\u003e添加还是非常简单的，利用开放接口即可。\u003c/p\u003e\n\u003cp\u003e我这里不用使用，可以直接调用函数，如果是两个服务，就需要开放接口了。\u003c/p\u003e\n\u003cp\u003e先获取归属地，归属地格式，国家|省份|城市，国外的只显示国家名，其它项为 0，返回字符串，以|分割。\u003c/p\u003e\n\u003cp\u003e取到归属地字符串后，切分字符串为数组，取出省份。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003estrArr := strings.Split(str, \u0026#34;|\u0026#34;)\nn := len(strArr)\n// 这个情况基本不存在，源文件中以0作为占位符。\nif n != 3 {\n\treturn \u0026#34;\u0026#34;\n}\n\nreturn strArr[1]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e归属地数组索引 0 是国家，1 是省份，2 是城市。\u003c/p\u003e\n\u003cp\u003e现在，使用获取到的省份信息比对。\u003c/p\u003e\n\u003cp\u003e以前，是直接将省份字段和“河南”比较，现在优化了一下，包含就可以。防止数据文件出现河南省这样的情况导致问题。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eok := strings.Contains(province, \u0026#34;河南\u0026#34;)\nif !ok {\n\tc.JSON(http.StatusOK, ErrWxResp(\u0026#34;拒绝登录-1\u0026#34;, nil))\n\treturn\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这样，归属地不是河南将不允许登录。\u003c/p\u003e","title":"管理后台如何添加 IP 归属地拦截？"},{"content":"网络安全的发展史充满了许多重要的里程碑。以下是按时间顺序列出的网络安全发展史中的一些关键事件：\n1983 年 - 计算机病毒的概念\n弗雷德·科恩（Fred Cohen）在他的论文中首次提出“计算机病毒”的概念，定义了自我复制的软件程序。\n1986 年 - Brain 病毒\nBrain 病毒是第一个 IBM PC 兼容机上的计算机病毒，由巴基斯坦的两个兄弟编写，它感染了引导扇区。\n1988 年 - Morris 蠕虫\nRobert Tappan Morris 发布了 Morris 蠕虫，这是第一个在互联网上广泛传播的蠕虫，导致了大量计算机系统的瘫痪。\n1995 年 - 微软 Word 宏病毒\nConcept 病毒是第一个宏病毒，感染了微软 Word 文档，标志着宏病毒时代的开始。\n1999 年 - Melissa 病毒\nMelissa 病毒通过电子邮件传播，迅速感染了数十万台计算机，导致企业网络瘫痪。\n2000 年 - ILOVEYOU 病毒\nILOVEYOU 病毒通过电子邮件传播，造成了数十亿美元的损失，是历史上传播最快的病毒之一。\n2001 年 - Code Red 蠕虫\nCode Red 蠕虫攻击了微软的 IIS 服务器，感染了超过 35 万台计算机，对互联网造成了严重影响。\n2003 年 - SQL Slammer 蠕虫\nSQL Slammer 蠕虫利用微软 SQL Server 漏洞，在短短几分钟内感染了数十万台计算机，导致互联网中断。\n2004 年 - MyDoom 蠕虫\nMyDoom 蠕虫通过电子邮件传播，成为传播最快的电子邮件蠕虫之一，导致大量邮件服务器瘫痪。\n2007 年 - 爱沙尼亚网络攻击\n爱沙尼亚遭遇了一系列大规模的分布式拒绝服务（DDoS）攻击，标志着国家级网络战争的开始。\n2010 年 - Stuxnet\nStuxnet 蠕虫被发现，它是第一个专门针对工业控制系统的恶意软件，破坏了伊朗的核设施。\n2013 年 - Snowden 泄密事件\n爱德华·斯诺登曝光了美国国家安全局（NSA）的大规模监控计划，引发了全球对隐私和网络安全的关注。\n2014 年 - Heartbleed 漏洞\nHeartbleed 漏洞被发现，它影响了 OpenSSL 库，导致全球数百万台服务器面临数据泄露的风险。\n2017 年 - WannaCry 勒索软件\nWannaCry 勒索软件利用微软 Windows 漏洞，迅速传播并加密了全球数十万台计算机，要求支付赎金才能解密数据。\n2018 年 - Spectre 和 Meltdown 漏洞\n研究人员发现了 Spectre 和 Meltdown 漏洞，这些漏洞影响了几乎所有现代处理器，允许攻击者访问敏感数据。\n2020 年 - SolarWinds 攻击\nSolarWinds 攻击是一起复杂的供应链攻击，黑客通过 SolarWinds Orion 软件更新感染了多家大型企业和政府机构的网络。\n2021 年 - Colonial Pipeline 攻击\nColonial Pipeline 遭到勒索软件攻击，导致美国东海岸的燃料供应中断，凸显了关键基础设施的网络安全风险。\n这些事件只是网络安全发展史中的一部分，它们共同推动了网络安全技术和策略的发展，并对现代社会产生了深远的影响。\n","permalink":"https://blog.91demo.top/wiki/virus.html","summary":"\u003cp\u003e网络安全的发展史充满了许多重要的里程碑。以下是按时间顺序列出的网络安全发展史中的一些关键事件：\u003c/p\u003e\n\u003cp\u003e1983 年 - 计算机病毒的概念\u003c/p\u003e\n\u003cp\u003e弗雷德·科恩（Fred Cohen）在他的论文中首次提出“计算机病毒”的概念，定义了自我复制的软件程序。\u003c/p\u003e\n\u003cp\u003e1986 年 - Brain 病毒\u003c/p\u003e\n\u003cp\u003eBrain 病毒是第一个 IBM PC 兼容机上的计算机病毒，由巴基斯坦的两个兄弟编写，它感染了引导扇区。\u003c/p\u003e\n\u003cp\u003e1988 年 - Morris 蠕虫\u003c/p\u003e\n\u003cp\u003eRobert Tappan Morris 发布了 Morris 蠕虫，这是第一个在互联网上广泛传播的蠕虫，导致了大量计算机系统的瘫痪。\u003c/p\u003e\n\u003cp\u003e1995 年 - 微软 Word 宏病毒\u003c/p\u003e\n\u003cp\u003eConcept 病毒是第一个宏病毒，感染了微软 Word 文档，标志着宏病毒时代的开始。\u003c/p\u003e\n\u003cp\u003e1999 年 - Melissa 病毒\u003c/p\u003e\n\u003cp\u003eMelissa 病毒通过电子邮件传播，迅速感染了数十万台计算机，导致企业网络瘫痪。\u003c/p\u003e\n\u003cp\u003e2000 年 - ILOVEYOU 病毒\u003c/p\u003e\n\u003cp\u003eILOVEYOU 病毒通过电子邮件传播，造成了数十亿美元的损失，是历史上传播最快的病毒之一。\u003cbr\u003e\n2001 年 - Code Red 蠕虫\u003c/p\u003e\n\u003cp\u003eCode Red 蠕虫攻击了微软的 IIS 服务器，感染了超过 35 万台计算机，对互联网造成了严重影响。\u003c/p\u003e\n\u003cp\u003e2003 年 - SQL Slammer 蠕虫\u003c/p\u003e","title":"计算机病毒发展史"},{"content":"计算机发展史充满了许多重要的里程碑。以下是按时间顺序列出的计算机发展史中的一些关键事件：\n1941 年 - 第一台电子计算机：Z3\n由德国工程师康拉德·楚泽（Konrad Zuse）发明的 Z3 被认为是世界上第一台可编程的电子计算机。\n1943 年 - Colossus\n英国在二战期间开发的 Colossus 计算机，用于破解德国的密码，特别是 Lorenz 密码。\n1946 年 - ENIAC\n美国研制的 ENIAC（Electronic Numerical Integrator and Computer）是第一台通用电子计算机。\n1951 年 - UNIVAC I\nUNIVAC I（Universal Automatic Computer I）是第一台商用计算机，由 J. Presper Eckert 和 John Mauchly 设计。\n1956 年 - 磁盘存储器\nIBM 引入了第一个硬盘驱动器 IBM 305 RAMAC，它使用旋转磁盘来存储数据。\n1964 年 - IBM System/360\nIBM 发布的 System/360 系列计算机，成为一种标准化的计算机系统，影响深远。\n1971 年 - 微处理器\n英特尔发布了 4004 微处理器，这是第一款商用微处理器，标志着个人计算机时代的开始。\n1973 年 - 以太网\n罗伯特·梅特卡夫（Robert Metcalfe）在 Xerox PARC 发明以太网，为局域网（LAN）技术奠定基础。\n1975 年 - Altair 8800\nMITS 公司发布的 Altair 8800，被认为是第一台成功的个人计算机。\n1976 年 - Apple I\n史蒂夫·乔布斯和史蒂夫·沃兹尼亚克发布了 Apple I 计算机，开启了个人计算机革命。\n1981 年 - IBM PC\nIBM 发布了其个人计算机 IBM PC，成为企业和个人使用的标准计算机。\n1984 年 - Macintosh\n苹果公司发布了 Macintosh 计算机，这是第一台成功的图形用户界面（GUI）计算机。\n1989 年 - 万维网\n提姆·伯纳斯-李（Tim Berners-Lee）发明了万维网（World Wide Web），彻底改变了信息的传播方式。\n1991 年 - Linux\n林纳斯·托瓦兹（Linus Torvalds）发布了 Linux 内核，这是一个开源的操作系统内核。\n2007 年 - iPhone\n苹果公司发布了 iPhone，融合了计算机和手机技术，开启了智能手机时代。\n2011 年 - 云计算\n云计算技术的普及，例如亚马逊的 AWS 和微软的 Azure，使得计算资源变得更加灵活和可扩展。\n","permalink":"https://blog.91demo.top/wiki/computer.html","summary":"\u003cp\u003e计算机发展史充满了许多重要的里程碑。以下是按时间顺序列出的计算机发展史中的一些关键事件：\u003c/p\u003e\n\u003cp\u003e1941 年 - 第一台电子计算机：Z3\u003c/p\u003e\n\u003cp\u003e由德国工程师康拉德·楚泽（Konrad Zuse）发明的 Z3 被认为是世界上第一台可编程的电子计算机。\u003c/p\u003e\n\u003cp\u003e1943 年 - Colossus\u003c/p\u003e\n\u003cp\u003e英国在二战期间开发的 Colossus 计算机，用于破解德国的密码，特别是 Lorenz 密码。\u003c/p\u003e\n\u003cp\u003e1946 年 - ENIAC\u003c/p\u003e\n\u003cp\u003e美国研制的 ENIAC（Electronic Numerical Integrator and Computer）是第一台通用电子计算机。\u003c/p\u003e\n\u003cp\u003e1951 年 - UNIVAC I\u003c/p\u003e\n\u003cp\u003eUNIVAC I（Universal Automatic Computer I）是第一台商用计算机，由 J. Presper Eckert 和 John Mauchly 设计。\u003c/p\u003e\n\u003cp\u003e1956 年 - 磁盘存储器\u003c/p\u003e\n\u003cp\u003eIBM 引入了第一个硬盘驱动器 IBM 305 RAMAC，它使用旋转磁盘来存储数据。\u003c/p\u003e\n\u003cp\u003e1964 年 - IBM System/360\u003c/p\u003e\n\u003cp\u003eIBM 发布的 System/360 系列计算机，成为一种标准化的计算机系统，影响深远。\u003c/p\u003e\n\u003cp\u003e1971 年 - 微处理器\u003c/p\u003e\n\u003cp\u003e英特尔发布了 4004 微处理器，这是第一款商用微处理器，标志着个人计算机时代的开始。\u003c/p\u003e\n\u003cp\u003e1973 年 - 以太网\u003c/p\u003e","title":"计算机发展史"},{"content":"简单、高效的内网穿透工具。\n网址：https://www.gofrp.org\n通过 SSH 访问内网机器 1, 在具有公网 IP 的机器上部署 frps\n部署 frps 并编辑 frps.toml 文件。以下是简化的配置，其中设置了 frp 服务器用于接收客户端连接的端口：\nbindPort = 7000 2, 在需要被访问的内网机器上部署 frpc\n部署 frpc 并编辑 frpc.toml 文件，假设 frps 所在服务器的公网 IP 地址为 x.x.x.x。以下是示例配置：\nserverAddr = \u0026#34;x.x.x.x\u0026#34; serverPort = 7000 [[proxies]] name = \u0026#34;ssh\u0026#34; type = \u0026#34;tcp\u0026#34; localIP = \u0026#34;127.0.0.1\u0026#34; localPort = 22 remotePort = 6000 localIP 和 localPort 配置为需要从公网访问的内网服务的地址和端口。\nremotePort 表示在 frp 服务端监听的端口，访问此端口的流量将被转发到本地服务的相应端口。\n3, 通过 SSH 访问内网机器\n使用以下命令通过 SSH 访问内网机器，假设用户名为 test：\nssh -o Port=6000 test@x.x.x.x ","permalink":"https://blog.91demo.top/wiki/frp.html","summary":"\u003cp\u003e简单、高效的内网穿透工具。\u003c/p\u003e\n\u003cp\u003e网址：https://www.gofrp.org\u003c/p\u003e\n\u003ch2 id=\"通过-ssh-访问内网机器\"\u003e通过 SSH 访问内网机器\u003c/h2\u003e\n\u003cp\u003e1, 在具有公网 IP 的机器上部署 frps\u003c/p\u003e\n\u003cp\u003e部署 frps 并编辑 frps.toml 文件。以下是简化的配置，其中设置了 frp 服务器用于接收客户端连接的端口：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ebindPort = 7000\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e2, 在需要被访问的内网机器上部署 frpc\u003c/p\u003e\n\u003cp\u003e部署 frpc 并编辑 frpc.toml 文件，假设 frps 所在服务器的公网 IP 地址为 x.x.x.x。以下是示例配置：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eserverAddr = \u0026#34;x.x.x.x\u0026#34;\nserverPort = 7000\n\n[[proxies]]\nname = \u0026#34;ssh\u0026#34;\ntype = \u0026#34;tcp\u0026#34;\nlocalIP = \u0026#34;127.0.0.1\u0026#34;\nlocalPort = 22\nremotePort = 6000\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003elocalIP 和 localPort 配置为需要从公网访问的内网服务的地址和端口。\u003cbr\u003e\nremotePort 表示在 frp 服务端监听的端口，访问此端口的流量将被转发到本地服务的相应端口。\u003c/p\u003e\n\u003cp\u003e3, 通过 SSH 访问内网机器\u003c/p\u003e\n\u003cp\u003e使用以下命令通过 SSH 访问内网机器，假设用户名为 test：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003essh -o Port=6000 test@x.x.x.x\n\u003c/code\u003e\u003c/pre\u003e","title":"记录 Frp 常用配置"},{"content":"在使用我的笔记中，发现登录之后，无法跳转到登录前要到达的页面。\n首先，检查 Nginx 配置，查看是否配置了携带登录前的 URL 路径。查看之后，发现没有配置，现在将其配置好，配置如下：\nlocation @error401{ return 302 https://$host/login/?url=https://$host$request_uri; } 配置好后，继续测试，发现登录后，还是调整到首页，并没有调整到登录前的页面。检查登录应用，发现登录应用没有取 URL 参数，所以直接跳转到根路径。\n将应用配置好，取 URL 参数，当 URL 参数不为空时，跳转到该 URL 路径。\nurl := c.Query(\u0026#34;url\u0026#34;) if url != \u0026#34;\u0026#34; { c.Redirect(http.StatusFound, url) } else { c.Redirect(http.StatusFound, \u0026#34;/\u0026#34;) } 将应用打包，发布到服务器继续测试，还是失败，看日志，浏览器请求时是携带 URL 参数的。\nGET /login/?url=https://www.91demo.top/zh-cn/private/project/visit/ch1.html 一度对人生产生怀疑，为啥我的不可以呢？我是参考网上教程配置的，网上教程使用 Python 举例，我这里使用 Go 实现，原理上应该是可行的啊。\n经过多次细读对比，发现自己的 GET 请求路径是/login/，POST 请求路径是/api/doLogin，而网上教程 GET 和 POST 请求都是/login/，将自己的应该重新调整，无论 GET 请求还是 POST 请求都调整为/login 路径。当将应用推到服务器时，发现浏览器上显示跳转次数过多。排除一番后，将应用路径调整为/login/，Nginx 配置文件也进行了修改，修改为如下配置：\nlocation /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; } 原来的配置如下：\nlocation /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; } 可以看到，在 login 后添加了/，无法解释为什么，反正现在是可以正常访问网站了。然后继续测试，发现登录后还是调整到首页。\n经过对比，发现登录页面，网上教程写法如下：\n\u0026lt;form method=\u0026#34;post\u0026#34;\u0026gt; 我的页面写法如下：\n\u0026lt;form method=\u0026#34;post\u0026#34; action=\u0026#34;/login/\u0026#34;\u0026gt; 和教程调整一致，重新启动服务，继续测试。这次成功了。\n仔细查看日志，发现了问题。\n/login/\t{\u0026#34;status\u0026#34;: 302, \u0026#34;methed\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/login/\u0026#34;, \u0026#34;query\u0026#34;: \u0026#34;\u0026#34; 调整后，日志如下：\n/login/\t{\u0026#34;status\u0026#34;: 302, \u0026#34;methed\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/login/\u0026#34;, \u0026#34;query\u0026#34;: \u0026#34;url=https://www.91demo.top/zh-cn/private/index.html\u0026#34; 哎，不容易。终于搞定了。\n","permalink":"https://blog.91demo.top/wiki/urlredirect.html","summary":"\u003cp\u003e在使用我的笔记中，发现登录之后，无法跳转到登录前要到达的页面。\u003c/p\u003e\n\u003cp\u003e首先，检查 Nginx 配置，查看是否配置了携带登录前的 URL 路径。查看之后，发现没有配置，现在将其配置好，配置如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elocation @error401{\n\t\t\treturn 302 https://$host/login/?url=https://$host$request_uri;\n\t\t}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e配置好后，继续测试，发现登录后，还是调整到首页，并没有调整到登录前的页面。检查登录应用，发现登录应用没有取 URL 参数，所以直接跳转到根路径。\u003c/p\u003e\n\u003cp\u003e将应用配置好，取 URL 参数，当 URL 参数不为空时，跳转到该 URL 路径。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eurl := c.Query(\u0026#34;url\u0026#34;)\n\t\tif url != \u0026#34;\u0026#34; {\n\t\t\tc.Redirect(http.StatusFound, url)\n\t\t} else {\n\t\t\tc.Redirect(http.StatusFound, \u0026#34;/\u0026#34;)\n\t\t}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e将应用打包，发布到服务器继续测试，还是失败，看日志，浏览器请求时是携带 URL 参数的。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eGET /login/?url=https://www.91demo.top/zh-cn/private/project/visit/ch1.html\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e一度对人生产生怀疑，为啥我的不可以呢？我是参考网上教程配置的，网上教程使用 Python 举例，我这里使用 Go 实现，原理上应该是可行的啊。\u003c/p\u003e\n\u003cp\u003e经过多次细读对比，发现自己的 GET 请求路径是/login/，POST 请求路径是/api/doLogin，而网上教程 GET 和 POST 请求都是/login/，将自己的应该重新调整，无论 GET 请求还是 POST 请求都调整为/login 路径。当将应用推到服务器时，发现浏览器上显示跳转次数过多。排除一番后，将应用路径调整为/login/，Nginx 配置文件也进行了修改，修改为如下配置：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elocation /login/ {\n\t\t\tproxy_pass http://127.0.0.1:9982/login/;\n\t\t\tproxy_set_header X-Real-IP $remote_addr;\n\t\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t\t}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e原来的配置如下：\u003c/p\u003e","title":"记录 URL 跳转携带原路径"},{"content":"无线技术的发展史充满了许多重要的里程碑。以下是按时间顺序列出的无线技术发展史中的一些关键事件：\n1864 年 - 电磁波理论\n詹姆斯·克拉克·麦克斯韦（James Clerk Maxwell）提出了电磁波理论，预言了电磁波的存在。\n1888 年 - 电磁波的实验验证\n亨利·赫兹（Heinrich Hertz）通过实验验证了麦克斯韦的电磁波理论，成功产生并检测到电磁波。\n1895 年 - 无线电报\n古列尔莫·马可尼（Guglielmo Marconi）进行了首次成功的无线电报传输，标志着无线通信的开始。\n1901 年 - 跨大西洋无线电信号\n马可尼成功实现了跨大西洋的无线电信号传输，从英国到加拿大，证明了无线电波可以覆盖长距离。\n1933 年 - 频率调制（FM）\n埃德温·霍华德·阿姆斯特朗（Edwin Howard Armstrong）发明了频率调制（FM）技术，改进了无线电传输的质量和抗干扰能力。\n1947 年 - 蜂窝通信概念\n贝尔实验室的研究人员提出了蜂窝通信的概念，奠定了现代移动通信网络的基础。\n1973 年 - 第一部手机通话\n摩托罗拉的工程师马丁·库帕（Martin Cooper）进行了首次手机通话，使用了一部原型移动电话。\n1983 年 - 第一代移动通信（1G）\n美国推出了第一代移动通信系统（1G），基于模拟信号的蜂窝网络。\n1991 年 - 第二代移动通信（2G）\n第二代移动通信系统（2G）在芬兰推出，采用数字信号，提高了通话质量和网络安全性。\n1997 年 - Wi-Fi\nIEEE 802.11 标准发布，标志着 Wi-Fi 技术的诞生，提供了无线局域网（WLAN）解决方案。\n2001 年 - 第三代移动通信（3G）\n第三代移动通信系统（3G）在日本推出，提供了更快的数据传输速率，支持多媒体通信。\n2009 年 - 第四代移动通信（4G）\n第四代移动通信系统（4G）在瑞典和挪威推出，基于 LTE 技术，提供了更高的带宽和更低的延迟。\n2010 年 - 蓝牙 4.0\n蓝牙 4.0 标准发布，引入了低功耗蓝牙（BLE），适用于物联网设备的无线通信。\n2013 年 - Wi-Fi 5（802.11ac）\nIEEE 802.11ac 标准发布，提供了更高的 Wi-Fi 传输速度和更大的带宽。\n2018 年 - 第五代移动通信（5G）\n第五代移动通信系统（5G）开始商用部署，提供了超高速数据传输、低延迟和大规模设备连接能力，支持物联网和智能城市应用。\n2019 年 - Wi-Fi 6（802.11ax）\nIEEE 802.11ax 标准发布，提供了更高的效率、更快的速度和更高的容量，满足现代无线网络的需求。\n2023 年 - Wi-Fi 6E\nWi-Fi 6E 扩展了 Wi-Fi 6 标准，允许在 6 GHz 频段上运行，提供了更多的频道和更高的带宽。\n2025 年 - 初步 6G 研究与开发\n6G 技术的研究与开发开始，目标是提供更高的传输速度、超低延迟和先进的网络功能，进一步支持未来的通信需求。\n这些事件只是无线技术发展史中的一部分，它们共同推动了无线通信技术的演进，并对现代社会产生了深远的影响。\n","permalink":"https://blog.91demo.top/wiki/wireless.html","summary":"\u003cp\u003e无线技术的发展史充满了许多重要的里程碑。以下是按时间顺序列出的无线技术发展史中的一些关键事件：\u003c/p\u003e\n\u003cp\u003e1864 年 - 电磁波理论\u003c/p\u003e\n\u003cp\u003e詹姆斯·克拉克·麦克斯韦（James Clerk Maxwell）提出了电磁波理论，预言了电磁波的存在。\u003c/p\u003e\n\u003cp\u003e1888 年 - 电磁波的实验验证\u003c/p\u003e\n\u003cp\u003e亨利·赫兹（Heinrich Hertz）通过实验验证了麦克斯韦的电磁波理论，成功产生并检测到电磁波。\u003c/p\u003e\n\u003cp\u003e1895 年 - 无线电报\u003c/p\u003e\n\u003cp\u003e古列尔莫·马可尼（Guglielmo Marconi）进行了首次成功的无线电报传输，标志着无线通信的开始。\u003c/p\u003e\n\u003cp\u003e1901 年 - 跨大西洋无线电信号\u003c/p\u003e\n\u003cp\u003e马可尼成功实现了跨大西洋的无线电信号传输，从英国到加拿大，证明了无线电波可以覆盖长距离。\u003c/p\u003e\n\u003cp\u003e1933 年 - 频率调制（FM）\u003c/p\u003e\n\u003cp\u003e埃德温·霍华德·阿姆斯特朗（Edwin Howard Armstrong）发明了频率调制（FM）技术，改进了无线电传输的质量和抗干扰能力。\u003c/p\u003e\n\u003cp\u003e1947 年 - 蜂窝通信概念\u003c/p\u003e\n\u003cp\u003e贝尔实验室的研究人员提出了蜂窝通信的概念，奠定了现代移动通信网络的基础。\u003c/p\u003e\n\u003cp\u003e1973 年 - 第一部手机通话\u003c/p\u003e\n\u003cp\u003e摩托罗拉的工程师马丁·库帕（Martin Cooper）进行了首次手机通话，使用了一部原型移动电话。\u003c/p\u003e\n\u003cp\u003e1983 年 - 第一代移动通信（1G）\u003c/p\u003e\n\u003cp\u003e美国推出了第一代移动通信系统（1G），基于模拟信号的蜂窝网络。\u003c/p\u003e\n\u003cp\u003e1991 年 - 第二代移动通信（2G）\u003c/p\u003e\n\u003cp\u003e第二代移动通信系统（2G）在芬兰推出，采用数字信号，提高了通话质量和网络安全性。\u003c/p\u003e\n\u003cp\u003e1997 年 - Wi-Fi\u003c/p\u003e\n\u003cp\u003eIEEE 802.11 标准发布，标志着 Wi-Fi 技术的诞生，提供了无线局域网（WLAN）解决方案。\u003c/p\u003e\n\u003cp\u003e2001 年 - 第三代移动通信（3G）\u003c/p\u003e\n\u003cp\u003e第三代移动通信系统（3G）在日本推出，提供了更快的数据传输速率，支持多媒体通信。\u003c/p\u003e\n\u003cp\u003e2009 年 - 第四代移动通信（4G）\u003c/p\u003e","title":"记录无线技术发展史"},{"content":"这几日，碰到一个奇怪问题，我一篇要删除的文章，在删除文件后，并提交到 git，当我运行 mdbook build 之后，这篇文章的文件又出现了。\n然后，我在 Git 中删除，然后再次提交。发现几日后又出现了，我想了一下，中间运行过 mdbook build。\n既然这样，我猜想一定是 SUMMARY.md 文件中还存在这篇文章的索引。经检查，果然存在。\n所以，下次删除文章时，先删除 SUMMARY.md 中的索引，然后再删除文件，才能完全删除。\n","permalink":"https://blog.91demo.top/wiki/fixfilecannotremove.html","summary":"\u003cp\u003e这几日，碰到一个奇怪问题，我一篇要删除的文章，在删除文件后，并提交到 git，当我运行 mdbook build 之后，这篇文章的文件又出现了。\u003c/p\u003e\n\u003cp\u003e然后，我在 Git 中删除，然后再次提交。发现几日后又出现了，我想了一下，中间运行过 mdbook build。\u003c/p\u003e\n\u003cp\u003e既然这样，我猜想一定是 SUMMARY.md 文件中还存在这篇文章的索引。经检查，果然存在。\u003c/p\u003e\n\u003cp\u003e所以，下次删除文章时，先删除 SUMMARY.md 中的索引，然后再删除文件，才能完全删除。\u003c/p\u003e","title":"记录项目中某个文件无法删除的问题"},{"content":"最近，又想接入 mqtt 的认证，使用 sqlite 数据库的缺陷更明显了。sqlite3 不支持并发，当另一个应用访问时，会提示数据库已经占用。\n第一想法是将 sqlite 迁移到 mysql 中去，但是安装了 mysql 之后，发现内存占用很大，我的 1G 服务器都占用 800MB 左右，受不了，这样的话，我的服务器无法再安装其它服务。\n然后我又安装了 postgresql，发现内存占用还可以接受，默认没有连接应用的情况下，内存占用 10MB 左右。先观察一下 pg 内存占用是否稳定，现在没有连接，不知道真实情况如何？\n这两天看了一下 PG 的教程，并观察了一下 PG，发现运行还是很稳定的。决定使用 PG。\n如果采用 PG，需要先将 Sqlite3 的数据迁移到 PG 中去。\n这里是我这几天看 Postgresql 记录的，包含迁移 sqlite3 数据到 PG。详情请查看postgresql 数据库\n最近又将语音验证码从 Sqlite3 迁移到了 Postgresql，现在不需要同步了，在豆子工具获取 AST 账户就可以直接使用了。\n哎，这几天挺累的，但收获颇丰。\n","permalink":"https://blog.91demo.top/wiki/sqlite2pg.html","summary":"\u003cp\u003e最近，又想接入 mqtt 的认证，使用 sqlite 数据库的缺陷更明显了。sqlite3 不支持并发，当另一个应用访问时，会提示数据库已经占用。\u003c/p\u003e\n\u003cp\u003e第一想法是将 sqlite 迁移到 mysql 中去，但是安装了 mysql 之后，发现内存占用很大，我的 1G 服务器都占用 800MB 左右，受不了，这样的话，我的服务器无法再安装其它服务。\u003c/p\u003e\n\u003cp\u003e然后我又安装了 postgresql，发现内存占用还可以接受，默认没有连接应用的情况下，内存占用 10MB 左右。先观察一下 pg 内存占用是否稳定，现在没有连接，不知道真实情况如何？\u003c/p\u003e\n\u003cp\u003e这两天看了一下 PG 的教程，并观察了一下 PG，发现运行还是很稳定的。决定使用 PG。\u003c/p\u003e\n\u003cp\u003e如果采用 PG，需要先将 Sqlite3 的数据迁移到 PG 中去。\u003c/p\u003e\n\u003cp\u003e这里是我这几天看 Postgresql 记录的，包含迁移 sqlite3 数据到 PG。详情请查看\u003ca href=\"https://www.91demo.top/zh-cn/public/db/postgresql.html\"\u003epostgresql 数据库\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e最近又将语音验证码从 Sqlite3 迁移到了 Postgresql，现在不需要同步了，在豆子工具获取 AST 账户就可以直接使用了。\u003c/p\u003e\n\u003cp\u003e哎，这几天挺累的，但收获颇丰。\u003c/p\u003e","title":"将sqlite数据库迁移到postgresql数据库"},{"content":"要使用 Postgresql，需要先安装 pg 头文件\nyum install postgresql-devel 在 Asterisk 源文件中，执行\n./configure 执行下面的命令，查看 res_config_pgsql 模块有没有选择，没有请先选择。\nmake menuselect 然后执行\nmake;make install 在/etc/asterisk/modules.conf 配置文件中，添加\nload = res_config_pgsql.so 将源代码中 configs/samples/res_pgsql.conf.sample 复制到/etc/asterisk 目录下，并改为 res_pgsql.conf 文件。修改用户名等参数。\n[general] dbhost=127.0.0.1 dbport=5432 dbname=asterisk dbuser=asterisk dbpass=password 修改配置文件/etc/asterisk/extconfig.conf，将下面三项调整为：\nps_endpoints =\u0026gt; pgsql,general ps_auths =\u0026gt; pgsql,general ps_aors =\u0026gt; pgsql,general 其它配置文件，参考 sqlite3 数据库的配置。\n","permalink":"https://blog.91demo.top/wiki/pjsippgsql.html","summary":"\u003cp\u003e要使用 Postgresql，需要先安装 pg 头文件\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eyum install postgresql-devel\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在 Asterisk 源文件中，执行\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e./configure\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e执行下面的命令，查看 res_config_pgsql 模块有没有选择，没有请先选择。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emake menuselect\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后执行\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emake;make install\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在/etc/asterisk/modules.conf 配置文件中，添加\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eload = res_config_pgsql.so\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e将源代码中 configs/samples/res_pgsql.conf.sample 复制到/etc/asterisk 目录下，并改为 res_pgsql.conf 文件。修改用户名等参数。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[general]\ndbhost=127.0.0.1\ndbport=5432\ndbname=asterisk\ndbuser=asterisk\ndbpass=password\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e修改配置文件/etc/asterisk/extconfig.conf，将下面三项调整为：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eps_endpoints =\u0026gt; pgsql,general\nps_auths =\u0026gt; pgsql,general\nps_aors =\u0026gt; pgsql,general\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e其它配置文件，参考 sqlite3 数据库的配置。\u003c/p\u003e","title":"配置 Asterisk PJSIP 使用Postgresql"},{"content":"Asterisk 如何将 PJSIP 通道驱动程序与实时数据库存储后端相连接。实时接口允许将 PJSIP 的大部分配置（如端点、auth、aor 等）存储在数据库中，而不仅是 pjsip.conf 配置文件中。\n我们假设 Asterisk 安装在 Linux 服务器上，并希望 Asterisk 通过 odbc 连接器连接到 Mysql 数据库。我们还需要安装如下依赖包：\nunixodbc 和 unixodbc-dev odbc 及其开发包 libmyodbc odbc 到 mysql 接口包 python-dev 和 python-pip python-mysqldb 如果在 Ubuntu 服务器，可以直接通过如下命令安装：\n# apt-get install unixodbc unixodbc-dev libmyodbc python-dev python-pip python-mysqldb 一旦安装了这些包，你需要使用 make menuconfig 工具检查 res_config_odbc 和 res_odbc 和 res_pjsip_xxx 资源模块被安装。然后执行\n./configure;make;make install 创建数据库，使用 mysqladmin 工具创建一个数据库，用来存储配置信息。\n# mysqladmin -u root -p create asterisk 安装 Alembic，可以用来创建配置信息表结构。Alembic 是一个完整的数据库迁移工具，支持升级现有数据库的模式、模式版本控制、创建新表和数据库等等。\n# pip install alembic 切换到 Asterisk 源码目录 contrib/ast-db-manage/，它包含 Alembic 脚本。\n# cd contrib/ast-db-manage/ 下一步，编辑配置文件 config.ini.sample，将 sqlalchemy.url 选项改成如下：\nsqlalchemy.url = mysql://root:password@localhost/asterisk 用户名和密码填写为你的真实数据库用户名和密码。然后将 config.ini.sample 重命名为 config.ini。\n最后，使用 Alembic 设置数据库表。\n# alembic -c config.ini upgrade head 你可以通过连接到数据库验证表是否已经创建？\n当创建了数据库表后，我们还需要配置 Asterisk，首先，我们要配置 ODBC 连接到 Mysql。我们配置/etc/odbcinst.ini 配置文件，你的文件看起来像这样\n[MySQL] Description = ODBC for MySQL Driver = /usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so Setup = /usr/lib/x86_64-linux-gnu/odbc/libodbcmyS.so UsageCount = 2 下一步，在/etc/odbc.ini 配置文件中，创建一个 asterisk 数据库连接器。\n[asterisk] Driver = MySQL Description = MySQL connection to ‘asterisk’ database Server = localhost Port = 3306 Database = asterisk UserName = root Password = password Socket = /var/run/mysqld/mysqld.sock 下一步，我们在/etc/asterisk/res_odbc.conf 配置文件中，配置 res_odbc 可以连接到 asterisk 数据库连接器。\n[asterisk] enabled =\u0026gt; yes dsn =\u0026gt; asterisk username =\u0026gt; root password =\u0026gt; password pre-connect =\u0026gt; yes 配置正确后，我们可以在 asterisk 的 cli 中，使用odbc show查看配置信息。\n将 PJSIP Sorcery 连接到数据库\nPJSIP 堆栈在 Asterisk 使用了一个数据抽象层叫 sorcery。它允许用户为 Asterisk 狗叫一个分层数据源，以便在检索、更新、创建或销毁数据时交互使用。\nPJSIP 的配置基于对象类型。在 Sorcery 中配置总共有 5 个对象：\nendpoint auth aor domain identify 我们还可以配置联系人对象，尽管本例子中不需要它。这些配置文件在/etc/asterisk/sorcery.conf。\ntext[res_pjsip] ; Realtime PJSIP configuration wizard endpoint=realtime,ps_endpoints auth=realtime,ps_auths aor=realtime,ps_aors domain_alias=realtime,ps_domain_aliases contact=realtime,ps_contacts [res_pjsip_endpoint_identifier_ip] identify=realtime,ps_endpoint_id_ips 其中，realtime 是关键字，表示实时，ps_endpoints 是可以连接到数据库的表。如果不想实时，可以使用关键字 config。\n最后，我们需要再配置 /etc/asterisk/extconfig.conf，这个配置文件是告诉 Asterisk 使用实时数据库。\ntext[settings] ps_endpoints =\u0026gt; odbc,asterisk ps_auths =\u0026gt; odbc,asterisk ps_aors =\u0026gt; odbc,asterisk ps_domain_aliases =\u0026gt; odbc,asterisk ps_endpoint_id_ips =\u0026gt; odbc,asterisk ps_contacts =\u0026gt; odbc,asterisk 我们还需要配置 Asterisk 在启动之前加载 ODBC 驱动，这样其它模块才能获取到正确的配置信息。\n在/etc/asterisk/modules.conf 配置文件中，添加如下内容：\npreload =\u0026gt; res_odbc.so preload =\u0026gt; res_config_odbc.so noload =\u0026gt; chan_sip.so 我们配置 PJSIP，先创建一个传输。在/etc/asterisk/pjsip.conf 文件中：\n[transport-udp] type=transport protocol=udp bind=0.0.0.0 我们在 Mysql 数据库中维护端点和用户，也就是在数据库中插入 ps_endpoints 记录，ps_aors 记录，ps_auths 记录。\n此时，你应该能够在 asterisk cli 中使用pjsip show endpoints命令检索出记录。\n最后的拨号计划不变，现在就可以使用了，赶快尝试一下吧。\n","permalink":"https://blog.91demo.top/wiki/pjsiprealtime.html","summary":"\u003cp\u003eAsterisk 如何将 PJSIP 通道驱动程序与实时数据库存储后端相连接。实时接口允许将 PJSIP 的大部分配置（如端点、auth、aor 等）存储在数据库中，而不仅是 pjsip.conf 配置文件中。\u003c/p\u003e\n\u003cp\u003e我们假设 Asterisk 安装在 Linux 服务器上，并希望 Asterisk 通过 odbc 连接器连接到 Mysql 数据库。我们还需要安装如下依赖包：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eunixodbc 和 unixodbc-dev\u003c/li\u003e\n\u003cli\u003eodbc 及其开发包\u003c/li\u003e\n\u003cli\u003elibmyodbc\u003c/li\u003e\n\u003cli\u003eodbc 到 mysql 接口包\u003c/li\u003e\n\u003cli\u003epython-dev 和 python-pip\u003c/li\u003e\n\u003cli\u003epython-mysqldb\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e如果在 Ubuntu 服务器，可以直接通过如下命令安装：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# apt-get install unixodbc unixodbc-dev libmyodbc python-dev python-pip python-mysqldb\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e一旦安装了这些包，你需要使用 make menuconfig 工具检查 res_config_odbc 和 res_odbc 和 res_pjsip_xxx 资源模块被安装。然后执行\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e./configure;make;make install\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e创建数据库，使用 mysqladmin 工具创建一个数据库，用来存储配置信息。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# mysqladmin -u root -p create asterisk\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e安装 Alembic，可以用来创建配置信息表结构。Alembic 是一个完整的数据库迁移工具，支持升级现有数据库的模式、模式版本控制、创建新表和数据库等等。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# pip install alembic\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e切换到 Asterisk 源码目录 contrib/ast-db-manage/，它包含 Alembic 脚本。\u003c/p\u003e","title":"配置Asterisk 使用数据库存储PJSIP信息"},{"content":"今天，突发奇想，要控制我的网站在 6:00-23:00 之间访问，其余时间不让访问，为了让我们休息，不要太操劳了。\n网上搜索了一番，还真可以，现在就实践一把。\n如果要限制 IP，请使用 allow 和 deny 指令。\n例如，禁止某个 IP 访问就使用 deny IP，要允许某个 IP 访问就使用 allow IP，禁止所有就是 deny all，允许所有就是 allow all。常见的应用场景，管理后台，工具站等。还有一种方案就是使用防火墙限制，更安全一些。\n不满足需求，我们继续搜索时间段限制。\nNginx 提供了一个叫做 ngx_http_time_module 的时间模块，该模块可以帮助我们根据当前时间来对请求进行访问控制。这个时间模块包含了很多有用的指令，如$time_iso8601、$time_local、$time_gmt 等，它们可以用于获取当前服务器时间，并进行时间相关的判断。\n现在配置网站在每天 6:00-23:00 访问，当配置网站后，生效了，但是体验很差，就将配置加在了管理后台。\n首先，定义一个变量，用于获取当前时间：\nmap $time_iso8601 $currtime { default 0; \u0026#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])\u0026#34; 1; } 上面的配置，使用了 map 模块和正则表达式。map 不能配置在 Server 区块内，请配置在 Http 区块内。\n现在，我们将变量$currtime 应用到我们的资源配置中：\n这是一种写法，设置一个变量，解决 else 问题。\nlocation / { set $ok 0; if($curr_time=1){ // 访问页面资源 set $ok 1; } # 如果没有匹配到，跳转到这个页面 if($ok=0){ // 访问广告页 } } 我们的需求简单，这里可以简化成\nlocation / { if($curr_time=1){ // 访问页面资源 } if($curr_time=0){ // 访问广告页 } } 应用到我的网址，测试一下是否正常。\n上面的配置是自己想简单了，真实的情况如下：\nlocation / { if ($currtime = 0) { return 403 \u0026#34;该休息了，这个时间段不允许访问！\u0026#34;; } index index.html; } 在页面前进行拦截，if 内不支持 index 指令。理论上来说，是可以根据时间段跳转到不同的页面。使用 return 指令添加跳转的 URL 地址。我这里为了简单，就直接返回 403 上了，在浏览器上显示的现象是页面无法打开。\n","permalink":"https://blog.91demo.top/wiki/nginxtimecontrol.html","summary":"\u003cp\u003e今天，突发奇想，要控制我的网站在 6:00-23:00 之间访问，其余时间不让访问，为了让我们休息，不要太操劳了。\u003c/p\u003e\n\u003cp\u003e网上搜索了一番，还真可以，现在就实践一把。\u003c/p\u003e\n\u003cp\u003e如果要限制 IP，请使用 allow 和 deny 指令。\u003c/p\u003e\n\u003cp\u003e例如，禁止某个 IP 访问就使用 deny IP，要允许某个 IP 访问就使用 allow IP，禁止所有就是 deny all，允许所有就是 allow all。常见的应用场景，管理后台，工具站等。还有一种方案就是使用防火墙限制，更安全一些。\u003c/p\u003e\n\u003cp\u003e不满足需求，我们继续搜索时间段限制。\u003c/p\u003e\n\u003cp\u003eNginx 提供了一个叫做 ngx_http_time_module 的时间模块，该模块可以帮助我们根据当前时间来对请求进行访问控制。这个时间模块包含了很多有用的指令，如$time_iso8601、$time_local、$time_gmt 等，它们可以用于获取当前服务器时间，并进行时间相关的判断。\u003c/p\u003e\n\u003cp\u003e现在配置网站在每天 6:00-23:00 访问，当配置网站后，生效了，但是体验很差，就将配置加在了管理后台。\u003c/p\u003e\n\u003cp\u003e首先，定义一个变量，用于获取当前时间：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emap $time_iso8601 $currtime {\n\t\tdefault 0;\n\t\t\u0026#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])\u0026#34; 1;\n\t}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e上面的配置，使用了 map 模块和正则表达式。map 不能配置在 Server 区块内，请配置在 Http 区块内。\u003c/p\u003e\n\u003cp\u003e现在，我们将变量$currtime 应用到我们的资源配置中：\u003c/p\u003e\n\u003cp\u003e这是一种写法，设置一个变量，解决 else 问题。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elocation / {\n    set $ok 0;\n    if($curr_time=1){\n        // 访问页面资源\n        set $ok 1;\n    }\n\n    # 如果没有匹配到，跳转到这个页面\n    if($ok=0){\n        // 访问广告页\n    }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e我们的需求简单，这里可以简化成\u003c/p\u003e","title":"配置Nginx 基于时间控制网页访问"},{"content":"最近，更新了上传文章工具 rust 版本，这个版本使用的库为 nwg，即 native-windows-gui 库，该库支持老的 Windows GUI。\n昨天，当我准备在另一台电脑上录制视频时，发现运行不起来，GUI 界面闪现一下，就退出了。以为版本太老，我先升级了 Rust 版本到最新版本（😭，不应该的原因，固定版本反而更可靠稳定一些）。\nrustup update 升级完成后，cargo clean ,再 cargo run 重新运行。\n郁闷，还是报错。\n将 main.rs 文件中该行注释，#![windows_subsystem = \u0026quot;windows\u0026quot;]，可以在命令行中查看报错信息，提示是找不到文件。我想了下，只有图片是文件，然后我将图片的路径修改，发现修改之后，编辑器还提示错误，找不到文件。重新改回去后，编辑器提示错误消失。说明不是这里的错误。\n我真的晕了。从网上查找问题原因，没有找到此类问题的解决方法，郁闷。在查找时，发现了另一个 Windows 官方支持的 rust 库，就叫 windows，打算有时间了用这个库重写一下。\n今天，我又查看了下另一台电脑上的这个项目，发现可以正常运行，将 rust 升级到最新版本后，还是可以运行。在看到配置文件后，我才恍然大悟，原来是查找的是这个配置文件。我赶紧扒拉代码确认，发现确实是这个问题。\nlet conf = Ini::load_from_file(\u0026#34;conf.ini\u0026#34;).unwrap(); 上面是使用文件的地方，找不到文件 panic。我这里优化了一下，让错误提醒的更明显一些。这样我就瞬间能知道问题原因了。\n优化后的代码如下：\nlet conf = Ini::load_from_file(\u0026#34;conf.ini\u0026#34;).expect(\u0026#34;please config conf.ini file\u0026#34;); 有些问题其实很简单，但由于时间长了，还是会一头懵，如果报错信息提示完善一点，会很快定位到问题。\n","permalink":"https://blog.91demo.top/rust/upartrs.html","summary":"\u003cp\u003e最近，更新了上传文章工具 rust 版本，这个版本使用的库为 nwg，即 native-windows-gui 库，该库支持老的 Windows GUI。\u003c/p\u003e\n\u003cp\u003e昨天，当我准备在另一台电脑上录制视频时，发现运行不起来，GUI 界面闪现一下，就退出了。以为版本太老，我先升级了 Rust 版本到最新版本（😭，不应该的原因，固定版本反而更可靠稳定一些）。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003erustup update\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e升级完成后，cargo clean ,再 cargo run 重新运行。\u003c/p\u003e\n\u003cp\u003e郁闷，还是报错。\u003c/p\u003e\n\u003cp\u003e将 main.rs 文件中该行注释，\u003ccode\u003e#![windows_subsystem = \u0026quot;windows\u0026quot;]\u003c/code\u003e，可以在命令行中查看报错信息，提示是找不到文件。我想了下，只有图片是文件，然后我将图片的路径修改，发现修改之后，编辑器还提示错误，找不到文件。重新改回去后，编辑器提示错误消失。说明不是这里的错误。\u003c/p\u003e\n\u003cp\u003e我真的晕了。从网上查找问题原因，没有找到此类问题的解决方法，郁闷。在查找时，发现了另一个 Windows 官方支持的 rust 库，就叫 windows，打算有时间了用这个库重写一下。\u003c/p\u003e\n\u003cp\u003e今天，我又查看了下另一台电脑上的这个项目，发现可以正常运行，将 rust 升级到最新版本后，还是可以运行。在看到配置文件后，我才恍然大悟，原来是查找的是这个配置文件。我赶紧扒拉代码确认，发现确实是这个问题。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elet conf = Ini::load_from_file(\u0026#34;conf.ini\u0026#34;).unwrap();\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e上面是使用文件的地方，找不到文件 panic。我这里优化了一下，让错误提醒的更明显一些。这样我就瞬间能知道问题原因了。\u003c/p\u003e\n\u003cp\u003e优化后的代码如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elet conf = Ini::load_from_file(\u0026#34;conf.ini\u0026#34;).expect(\u0026#34;please config conf.ini file\u0026#34;);\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e有些问题其实很简单，但由于时间长了，还是会一头懵，如果报错信息提示完善一点，会很快定位到问题。\u003c/p\u003e","title":"深度复盘Rust上传工具崩溃问题，优化报错提示"},{"content":"Caddy 是一个功能强大、可扩展的平台，用 Go 编写，可为您的网站、服务和应用程序提供服务。\n大多数人使用 Caddy 用作 Web 服务器或者代理服务。我是想让我的网站和小程序支持 quic，而 Caddy 是一种解决方案。在使用 Caddy 前，我使用 nginx 作为网站服务器，以及小程序后台接口服务的代理。目前我已经在服务器上部署了 Caddy，并将小程序后台服务和网站都迁移到 Caddy 上。虽然我配置了 HTTP3 协议，但是 HTTP3 并没有生效。网站和小程序可以正常访问，使用的是 HTTP2 协议。我折腾了一段时间，还是没有找到问题点，依旧是 HTTP2 协议或 HTTP1.1 协议。这并没有达到我的初衷。\n之所以给大家继续推荐 Caddy，是因为 Caddy 确实有一些优势。我说下在我服务器部署 Caddy 之后的感受吧。首先，Caddy 在配置上要比 Nginx 简单，下面会详细介绍一下 Caddy 配置 Web 服务器和代理。其次，Caddy 天然支持 HTTPS，这对于网站和小程序后台，有很大的优势，特别是小程序后台，微信强制要求必须是 HTTPS。性能这块，可能因为我的网站比较小，我并没有感到太大的差异（凭感觉，没有经过性能工具测试）因为使用 Nginx 或 Caddy，我的网站和小程序访问都非常流畅。网上的帖子说 Nginx 性能会更好，特别是高并发时。在内存使用上，Caddy 要远远大于 Nginx，我的服务器内存比较小，这块非常明显。Nginx 在我服务器上的内存占用在 1MB 左右，而 Caddy 在 20MB 左右。\n现在我们讲下 Caddy 如何安装？在 Caddy 的官网上，详细的介绍了 Caddy 的安装方法，我以我的服务器 CentOS7 为例，介绍一下 Caddy 的安装。\nyum install yum-plugin-copr yum copr enable @caddy/caddy yum install caddy 安装非常简单，Caddy 支持多种方法配置，API 和配置文件，配置文件中又分 JSON 和 Caddyfile。为了简单，这里仅介绍 Caddyfile。如果你仅需要 Web 网站或服务代理。那么 Caddyfile 已经可以完全满足需求。如果你需要将 Caddy 作为 Web 服务平台，运行非常多的网站或者代理服务，那么你需要更深入的理解 Caddy API。下面的配置都以我的网站和小程序后台服务为例。\n先说下网站，我的网站【豆子笔记】（https://www.91demo.top）使用rust mdbook 制作，通过编写 Markdown 文件，可生成静态的 html 文件。将生成的 html 文件上传到服务器某个目录。然后通过下面的配置，即可提供 Web 服务。让互联网用户访问你的网站。\n下面是我的网站和反向代理配置文件，这些内容存储在/etc/caddy/Caddyfile 文件中：\n# 全局变量 { email myemail@qq.com servers { protocols h1 h2 h3 } } # 日志，可共用 (logging) { log { output file /var/log/caddy/caddy.log { roll_size 50MiB roll_local_time roll_keep 5 } format console { time_format rfc3339 time_local } level INFO } } # 官网 www.91demo.top { import logging # 反向代理 # 开放接口 reverse_proxy /api/* localhost:9982 # mqtt reverse_proxy /mqtt/ws localhost:18083 { stream_timeout 5m stream_close_delay 1m } # 静态网站 root * /web/dist/book encode zstd gzip file_server redir /Ez12Pv https://imgs.91demo.top/visit.webp redir /Ez12Pw https://imgs.91demo.top/wander.webp } # 图片资源 imgs.91demo.top { import logging root * /web/dist/images # 图片 @static { path_regexp \\.(ico|gif|jpg|jpeg|png|svg|webp)$ } header @static Cache-Control max-age=5184000 rewrite /Ez12Pv /visit.webp rewrite /Ez12Pw /wander.webp file_server } # API后台 mp.91demo.top { import logging reverse_proxy localhost:9981 } # 管理后台 h.91demo.top { import logging root * /web/admin encode zstd gzip file_server } 更详细的内容，请参考 Caddy 官方文档，我这里仅以我的配置文件为例，进行简单讲解。\nCaddyfile 中可以定义全局变量，放在文件的最前面。里面存放你的邮件地址，以及 Caddy 提供的服务协议。其中邮件地址用于申请 SSL 证书时使用。接着是共用模块，例如日志模块，日志模块中的 output 配置路径需要存在，例如/var/log/caddy/caddy.log 这个路径需要存在，并且 caddy 文件夹的拥有者需要是 caddy 用户和 caddy 组。这是一个坑，如果没有权限，就不会生成日志文件。再下面的官网www.91demo.top就是静态网站的配置了。www.91demo.top域名必须存在并且解析到了服务器的IP地址上。{}大括号中的内容为www网站的配置内容，里面包含了两个反向代理，以及一个静态文件服务，以及两个跳转配置。分别表示的是访问/api/时反向代理到后端开放接口服务，访问/mqtt/ws时反向代理到mqtt Websocket 服务，以及访问/ 提供网站静态文件服务，访问/Ez12Pv 会进行图片跳转（关注公众号后会看到这两个图片链接）。imgs.91demo.top 提供图片服务，图片服务添加了缓存时间，其它的服务都和 www 服务中类似，更多的基础知识内容可以参考网上 Caddy 使用。\n我所讲的，是基于自己的理解，如果有不对的或者有疑惑的地方，欢迎评论沟通。如果你没有公网云服务器，您想要一台公网云服务器，从下面的链接购买有优惠。\n想在阿里云购买，请使用这个链接：\nhttps://www.aliyun.com/minisite/goods?userCode=zrqh6alb\n想在腾讯云购买，请使用这个链接：\nhttps://curl.qcloud.com/XX6que5w\n通过以上链接购买，有很大优惠。\nCaddy 服务器的 GitHub 地址：\nhttps://github.com/caddyserver/caddy\n","permalink":"https://blog.91demo.top/wiki/caddy.html","summary":"\u003cp\u003eCaddy 是一个功能强大、可扩展的平台，用 Go 编写，可为您的网站、服务和应用程序提供服务。\u003c/p\u003e\n\u003cp\u003e大多数人使用 Caddy 用作 Web 服务器或者代理服务。我是想让我的网站和小程序支持 quic，而 Caddy 是一种解决方案。在使用 Caddy 前，我使用 nginx 作为网站服务器，以及小程序后台接口服务的代理。目前我已经在服务器上部署了 Caddy，并将小程序后台服务和网站都迁移到 Caddy 上。虽然我配置了 HTTP3 协议，但是 HTTP3 并没有生效。网站和小程序可以正常访问，使用的是 HTTP2 协议。我折腾了一段时间，还是没有找到问题点，依旧是 HTTP2 协议或 HTTP1.1 协议。这并没有达到我的初衷。\u003c/p\u003e\n\u003cp\u003e之所以给大家继续推荐 Caddy，是因为 Caddy 确实有一些优势。我说下在我服务器部署 Caddy 之后的感受吧。首先，Caddy 在配置上要比 Nginx 简单，下面会详细介绍一下 Caddy 配置 Web 服务器和代理。其次，Caddy 天然支持 HTTPS，这对于网站和小程序后台，有很大的优势，特别是小程序后台，微信强制要求必须是 HTTPS。性能这块，可能因为我的网站比较小，我并没有感到太大的差异（凭感觉，没有经过性能工具测试）因为使用 Nginx 或 Caddy，我的网站和小程序访问都非常流畅。网上的帖子说 Nginx 性能会更好，特别是高并发时。在内存使用上，Caddy 要远远大于 Nginx，我的服务器内存比较小，这块非常明显。Nginx 在我服务器上的内存占用在 1MB 左右，而 Caddy 在 20MB 左右。\u003c/p\u003e\n\u003cp\u003e现在我们讲下 Caddy 如何安装？在 Caddy 的官网上，详细的介绍了 Caddy 的安装方法，我以我的服务器 CentOS7 为例，介绍一下 Caddy 的安装。\u003c/p\u003e","title":"使用Caddy部署网站"},{"content":"小程序提供登录 API，可以获取小程序的唯一标识（openid）。我们可以通过这个唯一标识（openid）和客户端绑定起来。\n豆子碎片小程序是一款内容类小程序。它包含 Golang、Rust、小程序、Web 开发、数据库以及开发环境等内容的文章。现在，它已经成为一个研究学习平台以及内容查找和内容承载平台。\nupart-go 项目是用来上传文章到豆子碎片的一个命令行工具。它使用 Golang 开发。可以用来上传文章和管理文章。在使用该工具上传文章时，我们需要进行标识，是谁上传了文章？\n以前，我们会提供用户注册服务，让用户使用手机号或者邮箱进行注册。然后通过手机号或邮箱来标识用户。现在，我们还可以使用小程序账号来标识用户，免去用户注册（用户也可能不想不会使用手机号或邮箱注册）。\n我以 upart-go 项目文章上传客户端为例，介绍一下它的账号绑定原理。在用户使用小程序后，会调用登录 API 进行自动登录，然后后台会获取用户当前小程序的 openid。当获取到用户 openid 后，就可以唯一标识用户。此时，我们为小程序用户提供一个识别码的功能。识别码用于开放接口，用来识别用户。它基于 openid，生成 icode 和 isecret，分别代表账号和密码。这样当用户使用 icode 和 isecret 调用开放接口时，我们就可以定位到这个用户的 openid，从而定位到用户。我们的客户端可以在配置文件中配置这个 icode 和 isecret。这样当我们调用后台服务上传文章时，我们就知道将文章划分到谁的名下？在小程序中也可以进行浏览和查看。\n讲完了原理，我们开始实现功能。我们在客户端中已经实现了通过 icode 和 isecret 识别用户。但我们的客户端还可以进行优化，免去用户创建配置文件以及配置识别码。如何去做呢？\n我们启动客户端后，获取客户端主机的 MAC 地址，标识当前的客户端，然后我们提供一个窗口展示小程序码和验证码，小程序码让用户扫描，扫描之后打开的页面会识别当前用户的 openid，在打开的页面填入验证码，我们就可以完成小程序账号和客户端的绑定，是不是很棒？\n如果你对这个功能感兴趣，可参考项目，地址为：\nhttps://gitee.com/littletow/upart-go\n","permalink":"https://blog.91demo.top/wiki/bindmp.html","summary":"\u003cp\u003e小程序提供登录 API，可以获取小程序的唯一标识（openid）。我们可以通过这个唯一标识（openid）和客户端绑定起来。\u003c/p\u003e\n\u003cp\u003e豆子碎片小程序是一款内容类小程序。它包含 Golang、Rust、小程序、Web 开发、数据库以及开发环境等内容的文章。现在，它已经成为一个研究学习平台以及内容查找和内容承载平台。\u003c/p\u003e\n\u003cp\u003eupart-go 项目是用来上传文章到豆子碎片的一个命令行工具。它使用 Golang 开发。可以用来上传文章和管理文章。在使用该工具上传文章时，我们需要进行标识，是谁上传了文章？\u003c/p\u003e\n\u003cp\u003e以前，我们会提供用户注册服务，让用户使用手机号或者邮箱进行注册。然后通过手机号或邮箱来标识用户。现在，我们还可以使用小程序账号来标识用户，免去用户注册（用户也可能不想不会使用手机号或邮箱注册）。\u003c/p\u003e\n\u003cp\u003e我以 upart-go 项目文章上传客户端为例，介绍一下它的账号绑定原理。在用户使用小程序后，会调用登录 API 进行自动登录，然后后台会获取用户当前小程序的 openid。当获取到用户 openid 后，就可以唯一标识用户。此时，我们为小程序用户提供一个识别码的功能。识别码用于开放接口，用来识别用户。它基于 openid，生成 icode 和 isecret，分别代表账号和密码。这样当用户使用 icode 和 isecret 调用开放接口时，我们就可以定位到这个用户的 openid，从而定位到用户。我们的客户端可以在配置文件中配置这个 icode 和 isecret。这样当我们调用后台服务上传文章时，我们就知道将文章划分到谁的名下？在小程序中也可以进行浏览和查看。\u003c/p\u003e\n\u003cp\u003e讲完了原理，我们开始实现功能。我们在客户端中已经实现了通过 icode 和 isecret 识别用户。但我们的客户端还可以进行优化，免去用户创建配置文件以及配置识别码。如何去做呢？\u003c/p\u003e\n\u003cp\u003e我们启动客户端后，获取客户端主机的 MAC 地址，标识当前的客户端，然后我们提供一个窗口展示小程序码和验证码，小程序码让用户扫描，扫描之后打开的页面会识别当前用户的 openid，在打开的页面填入验证码，我们就可以完成小程序账号和客户端的绑定，是不是很棒？\u003c/p\u003e\n\u003cp\u003e如果你对这个功能感兴趣，可参考项目，地址为：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://gitee.com/littletow/upart-go\"\u003ehttps://gitee.com/littletow/upart-go\u003c/a\u003e\u003c/p\u003e","title":"使用小程序账号绑定我的客户端"},{"content":"最近，自助语音验证码使用手机流量拨打，又没有声音了。\n查看了 Ast 星日志，发现连接是正常的，使用 WIFI 拨打正常。于是又各种调整 PJSIP 参数，发现都没有生效。\n打开 RTP 日志后，发现 RTP 推送的音频流是发送到公网的，网上搜索，发现通信运营商会拦截封禁 UDP。我猜测大概率是这个造成的。\n但是如何探测 UDP 是否真的被封禁了？我想做一个工具。\n网上很多内容介绍如何使用 APP 工具探测 UDP，由于不擅长 APP，我想到了微信小程序。可以使用它实现这个功能。\n我们想探测移动、联通、电信是否封禁了 UDP，我们需要一个工具，我这里使用微信小程序，在小程序打开后，向后台服务器发送 UDP 端口的请求，后台服务器根据发送的 UDP 端口，发送 UDP 包请求。可以每 500 毫秒发送一个包，发送 10 次，5 秒钟内发送完。在小程序端，按钮发送后，显示加载动画，如何 5s 内没有收到消息，判定 UDP 消息无法送达。显示结果。\n","permalink":"https://blog.91demo.top/wiki/udpprobe.html","summary":"\u003cp\u003e最近，自助语音验证码使用手机流量拨打，又没有声音了。\u003c/p\u003e\n\u003cp\u003e查看了 Ast 星日志，发现连接是正常的，使用 WIFI 拨打正常。于是又各种调整 PJSIP 参数，发现都没有生效。\u003c/p\u003e\n\u003cp\u003e打开 RTP 日志后，发现 RTP 推送的音频流是发送到公网的，网上搜索，发现通信运营商会拦截封禁 UDP。我猜测大概率是这个造成的。\u003c/p\u003e\n\u003cp\u003e但是如何探测 UDP 是否真的被封禁了？我想做一个工具。\u003c/p\u003e\n\u003cp\u003e网上很多内容介绍如何使用 APP 工具探测 UDP，由于不擅长 APP，我想到了微信小程序。可以使用它实现这个功能。\u003c/p\u003e\n\u003cp\u003e我们想探测移动、联通、电信是否封禁了 UDP，我们需要一个工具，我这里使用微信小程序，在小程序打开后，向后台服务器发送 UDP 端口的请求，后台服务器根据发送的 UDP 端口，发送 UDP 包请求。可以每 500 毫秒发送一个包，发送 10 次，5 秒钟内发送完。在小程序端，按钮发送后，显示加载动画，如何 5s 内没有收到消息，判定 UDP 消息无法送达。显示结果。\u003c/p\u003e","title":"探测手机流量 UDP 是否拦截封禁"},{"content":"Linux 系统中有各种查看网络流量的工具，比如 sar、iftop、nethogs 等，它们可以从不同的纬度来分析系统中流量信息。\nSar sar（System Activity Reporter 系统活动情况报告）是目前 Linux 上最为全面的系统性能分析工具之一，可以从多方面对系统的活动进行报告。sar 可以从网络接口层面来分析数据包的收发情况、错误信息等。\n执行如下命令，使用 sar 每 1 秒统计一次网络接口的活动状况，连续统计 5 次。\nsar -n DEV 1 5 显示结果主要字段说明\nIFACE：网络接口名称。\nrxpck/s、txpck/s：每秒接收或发送的数据包数量。\nrxkB/s、txkB/s：每秒接收或发送的字节数，以 kB/s 为单位。\nrxcmp/s、txcmp/s：每秒接收或发送的压缩过的数据包数量。\nrxmcst/s：每秒接收到的多播数据包。\nIftop iftop 是 Linux 系统中一个免费的网卡实时流量监控工具，可以监控包括指定网卡的实时流量、端口连接信息、反向解析 IP 等信息。\n执行如下命令，查看详细端口流量占用情况。\niftop -i eth0 -P 执行如下命令，查看端口对应的进程。\nnetstat -tunlp |grep [$Port] 确认对应服务后，您可以通过停止服务或使用 iptables 服务来对指定地址进行处理。例如屏蔽 IP 地址或限速，以保证服务器带宽能够正常使用。\nNethogs Nethogs 是一款开源的网络流量监控工具，可用于显示每个进程的带宽占用情况。这样可以更直观定位异常流量的来源。Nethogs 支持 IPv4 和 IPv6 协议，支持本地网卡及 PPP 连接。直接输入 nethogs 启动工具即可。不带任何参数时，Nethogs 默认监控 eth0。用户可以通过 ifconfig 等指令核实具体哪个网络接口（eth1、eth0）对应公网网卡。\n查看网卡上进程级的流量信息。\nnethogs eth1 若确定进程是恶意程序，可以通过执行如下命令，终止进程。\nkill -TERM [$Port1] ","permalink":"https://blog.91demo.top/wiki/netmon.html","summary":"\u003cp\u003eLinux 系统中有各种查看网络流量的工具，比如 sar、iftop、nethogs 等，它们可以从不同的纬度来分析系统中流量信息。\u003c/p\u003e\n\u003ch2 id=\"sar\"\u003eSar\u003c/h2\u003e\n\u003cp\u003esar（System Activity Reporter 系统活动情况报告）是目前 Linux 上最为全面的系统性能分析工具之一，可以从多方面对系统的活动进行报告。sar 可以从网络接口层面来分析数据包的收发情况、错误信息等。\u003c/p\u003e\n\u003cp\u003e执行如下命令，使用 sar 每 1 秒统计一次网络接口的活动状况，连续统计 5 次。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003esar -n DEV 1 5\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e显示结果主要字段说明\u003c/p\u003e\n\u003cp\u003eIFACE：网络接口名称。\u003c/p\u003e\n\u003cp\u003erxpck/s、txpck/s：每秒接收或发送的数据包数量。\u003c/p\u003e\n\u003cp\u003erxkB/s、txkB/s：每秒接收或发送的字节数，以 kB/s 为单位。\u003c/p\u003e\n\u003cp\u003erxcmp/s、txcmp/s：每秒接收或发送的压缩过的数据包数量。\u003c/p\u003e\n\u003cp\u003erxmcst/s：每秒接收到的多播数据包。\u003c/p\u003e\n\u003ch2 id=\"iftop\"\u003eIftop\u003c/h2\u003e\n\u003cp\u003eiftop 是 Linux 系统中一个免费的网卡实时流量监控工具，可以监控包括指定网卡的实时流量、端口连接信息、反向解析 IP 等信息。\u003c/p\u003e\n\u003cp\u003e执行如下命令，查看详细端口流量占用情况。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eiftop -i eth0 -P\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e执行如下命令，查看端口对应的进程。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enetstat -tunlp |grep [$Port]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e确认对应服务后，您可以通过停止服务或使用 iptables 服务来对指定地址进行处理。例如屏蔽 IP 地址或限速，以保证服务器带宽能够正常使用。\u003c/p\u003e\n\u003ch2 id=\"nethogs\"\u003eNethogs\u003c/h2\u003e\n\u003cp\u003eNethogs 是一款开源的网络流量监控工具，可用于显示每个进程的带宽占用情况。这样可以更直观定位异常流量的来源。Nethogs 支持 IPv4 和 IPv6 协议，支持本地网卡及 PPP 连接。直接输入 nethogs 启动工具即可。不带任何参数时，Nethogs 默认监控 eth0。用户可以通过 ifconfig 等指令核实具体哪个网络接口（eth1、eth0）对应公网网卡。\u003c/p\u003e\n\u003cp\u003e查看网卡上进程级的流量信息。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enethogs eth1\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e若确定进程是恶意程序，可以通过执行如下命令，终止进程。\u003c/p\u003e","title":"网络流量监控常用命令"},{"content":"芯片的发展史充满了许多重要的里程碑。以下是按时间顺序列出的芯片发展史中的一些关键事件：\n1947 年 - 晶体管的发明\n约翰·巴丁（John Bardeen）、沃尔特·布拉顿（Walter Brattain）和威廉·肖克利（William Shockley）在贝尔实验室发明了晶体管，取代了电子管，成为后续芯片发展的基础。\n1958 年 - 第一块集成电路\n杰克·基尔比（Jack Kilby）在德州仪器公司发明了第一块集成电路（IC），它将多个晶体管集成到一个硅片上。\n1960 年 - 平面工艺技术\n罗伯特·诺伊斯（Robert Noyce）在仙童半导体公司开发了平面工艺技术，使得大规模生产集成电路成为可能。\n1965 年 - 摩尔定律\n戈登·摩尔（Gordon Moore）提出摩尔定律，预测集成电路中的晶体管数目每两年翻一番，这一预测指导了半导体行业的发展。\n1971 年 - 英特尔 4004 微处理器\n英特尔发布了 4004 微处理器，这是世界上第一款商用微处理器，标志着微处理器时代的开始。\n1974 年 - 英特尔 8080 微处理器\n英特尔发布了 8080 微处理器，是第一款广泛应用于个人计算机的微处理器。\n1982 年 - ARM 架构\n英国 Acorn 公司开发了 ARM 架构（Advanced RISC Machine），低功耗高性能的设计使其在移动设备中得到广泛应用。\n1985 年 - 英特尔 80386 微处理器\n英特尔发布了 80386 微处理器，首次引入了 32 位架构，极大地提升了计算性能。\n1993 年 - 英特尔 Pentium 处理器\n英特尔发布了 Pentium 处理器，采用超标量架构，大幅提升了处理速度和性能。\n2006 年 - 多核处理器\n英特尔和 AMD 发布了多核处理器，将多个处理核心集成到一个芯片上，提高了并行处理能力。\n2008 年 - GPU 计算\nNVIDIA 发布了 CUDA 编程模型，使得图形处理单元（GPU）可以用于通用计算，推动了深度学习和人工智能的发展。\n2011 年 - ARM Cortex-A 系列\nARM 发布了 Cortex-A 系列处理器，广泛应用于智能手机和平板电脑，成为移动设备的主流处理器架构。\n2017 年 - 谷歌 TPU\n谷歌发布了 Tensor Processing Unit（TPU），专门用于加速机器学习和深度学习任务。\n2018 年 - 7nm 制程工艺\n台积电（TSMC）和三星电子开始量产 7 纳米制程工艺的芯片，提高了芯片的性能和能效。\n2020 年 - 苹果 M1 芯片\n苹果发布了自研的 M1 芯片，基于 ARM 架构，集成了 CPU、GPU 和神经引擎，性能和能效显著提升。\n2023 年 - 3nm 制程工艺\n台积电和三星电子开始量产 3 纳米制程工艺的芯片，进一步提升了芯片的性能和能效。\n这些事件只是芯片发展史中的一部分，它们共同推动了半导体技术的发展，并对现代计算设备和技术产生了深远的影响。\n","permalink":"https://blog.91demo.top/wiki/chip.html","summary":"\u003cp\u003e芯片的发展史充满了许多重要的里程碑。以下是按时间顺序列出的芯片发展史中的一些关键事件：\u003c/p\u003e\n\u003cp\u003e1947 年 - 晶体管的发明\u003c/p\u003e\n\u003cp\u003e约翰·巴丁（John Bardeen）、沃尔特·布拉顿（Walter Brattain）和威廉·肖克利（William Shockley）在贝尔实验室发明了晶体管，取代了电子管，成为后续芯片发展的基础。\u003c/p\u003e\n\u003cp\u003e1958 年 - 第一块集成电路\u003c/p\u003e\n\u003cp\u003e杰克·基尔比（Jack Kilby）在德州仪器公司发明了第一块集成电路（IC），它将多个晶体管集成到一个硅片上。\u003c/p\u003e\n\u003cp\u003e1960 年 - 平面工艺技术\u003c/p\u003e\n\u003cp\u003e罗伯特·诺伊斯（Robert Noyce）在仙童半导体公司开发了平面工艺技术，使得大规模生产集成电路成为可能。\u003c/p\u003e\n\u003cp\u003e1965 年 - 摩尔定律\u003c/p\u003e\n\u003cp\u003e戈登·摩尔（Gordon Moore）提出摩尔定律，预测集成电路中的晶体管数目每两年翻一番，这一预测指导了半导体行业的发展。\u003c/p\u003e\n\u003cp\u003e1971 年 - 英特尔 4004 微处理器\u003c/p\u003e\n\u003cp\u003e英特尔发布了 4004 微处理器，这是世界上第一款商用微处理器，标志着微处理器时代的开始。\u003c/p\u003e\n\u003cp\u003e1974 年 - 英特尔 8080 微处理器\u003c/p\u003e\n\u003cp\u003e英特尔发布了 8080 微处理器，是第一款广泛应用于个人计算机的微处理器。\u003c/p\u003e\n\u003cp\u003e1982 年 - ARM 架构\u003c/p\u003e\n\u003cp\u003e英国 Acorn 公司开发了 ARM 架构（Advanced RISC Machine），低功耗高性能的设计使其在移动设备中得到广泛应用。\u003c/p\u003e\n\u003cp\u003e1985 年 - 英特尔 80386 微处理器\u003c/p\u003e\n\u003cp\u003e英特尔发布了 80386 微处理器，首次引入了 32 位架构，极大地提升了计算性能。\u003c/p\u003e\n\u003cp\u003e1993 年 - 英特尔 Pentium 处理器\u003c/p\u003e","title":"芯片发展史"},{"content":"mermaid 是很强大的一个库，可以使用文本展示图表。mdbook 是一个可以通过 Markdown 格式的文章内容生成在线书籍网站。mdbook-mermaid 这个库将 mermaid 和 mdbook 粘合在了一起。\n下面是 mdbook-mermaid 的一个示例，\ngraph TD; A--\u0026gt;B; A--\u0026gt;C; B--\u0026gt;D; C--\u0026gt;D; 该插件使用 Rust 开发，可以通过 Cargo 安装，\ncargo install mdbook-mermaid 在首次使用 mdbook-mermaid 时，需要下载一些依赖文件和配置，使用命令：\nmdbook-mermaid install path/to/your/book 上面的/path/to/your/book 是你的数据路径，运行之后，将会在你的书籍 book.toml 配置文件中添加如下内容：\n[preprocessor.mermaid] command = \u0026#34;mdbook-mermaid\u0026#34; [output.html] additional-js = [\u0026#34;mermaid.min.js\u0026#34;, \u0026#34;mermaid-init.js\u0026#34;] 插件将检测是否已配置 mdbbok-mermaid，如果已配置将跳过。否则，将添加上面的内容到 book.toml 配置文件中，并将文件 mermaid.min.js，mermaid-init.js 复制到你书籍的目录中。你可以在 src/bin/assets 目录中找到这些文件。你还可以修改 mermaid-init.js 来配置 mermaid。\n最后，重新编译书籍上传即可。\n","permalink":"https://blog.91demo.top/devops/addmermaid.html","summary":"\u003cp\u003emermaid 是很强大的一个库，可以使用文本展示图表。mdbook 是一个可以通过 Markdown 格式的文章内容生成在线书籍网站。mdbook-mermaid 这个库将 mermaid 和 mdbook 粘合在了一起。\u003c/p\u003e\n\u003cp\u003e下面是 mdbook-mermaid 的一个示例，\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-mermaid\" data-lang=\"mermaid\"\u003e    graph TD;\n        A--\u0026gt;B;\n        A--\u0026gt;C;\n        B--\u0026gt;D;\n        C--\u0026gt;D;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e该插件使用 Rust 开发，可以通过 Cargo 安装，\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecargo install mdbook-mermaid\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在首次使用 mdbook-mermaid 时，需要下载一些依赖文件和配置，使用命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emdbook-mermaid install path/to/your/book\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e上面的/path/to/your/book 是你的数据路径，运行之后，将会在你的书籍 book.toml 配置文件中添加如下内容：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[preprocessor.mermaid]\ncommand = \u0026#34;mdbook-mermaid\u0026#34;\n\n[output.html]\nadditional-js = [\u0026#34;mermaid.min.js\u0026#34;, \u0026#34;mermaid-init.js\u0026#34;]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e插件将检测是否已配置 mdbbok-mermaid，如果已配置将跳过。否则，将添加上面的内容到 book.toml 配置文件中，并将文件 mermaid.min.js，mermaid-init.js 复制到你书籍的目录中。你可以在 src/bin/assets 目录中找到这些文件。你还可以修改 mermaid-init.js 来配置 mermaid。\u003c/p\u003e\n\u003cp\u003e最后，重新编译书籍上传即可。\u003c/p\u003e","title":"在 mdbook 中集成 Mermaid 实现自动化流程图渲染"},{"content":"首先，下载最新版本的命令行工具。下载地址：\nhttps://gitee.com/littletow/upart-go/releases/download/v2.1.0/upart-go.zip 或者使用源码自行编译，源码地址：\nhttps://gitee.com/littletow/upart-go/archive/refs/tags/v2.1.0.zip 如何上传豆子碎片文章？ 命令行工具提供了上传文章的功能。你需要先使用 Markdown 格式写好内容。\n上传文章使用 upload 命令，后面跟参数题目，关键字，Markdown 文件，是否公开，是否加锁。其中前 3 项必填。\ngart upload title keyword content ispub islock 注意：上传完成后，可以在豆子碎片小程序中进行体验。\n如何更新维护豆子碎片文章？ 命令行工具提供了维护文章的功能。已经上传的文章可以更新标题、关键字、文件内容、是否公开、是否加锁、限制同城访问、还可以强制公开。\n相关的命令可通过下面的命令查看\ngart --help 注意：更新完成后，可以在豆子碎片小程序中进行体验。\n","permalink":"https://blog.91demo.top/wiki/gupart-use.html","summary":"\u003cp\u003e首先，下载最新版本的命令行工具。下载地址：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehttps://gitee.com/littletow/upart-go/releases/download/v2.1.0/upart-go.zip\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e或者使用源码自行编译，源码地址：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehttps://gitee.com/littletow/upart-go/archive/refs/tags/v2.1.0.zip\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"如何上传豆子碎片文章\"\u003e如何上传豆子碎片文章？\u003c/h2\u003e\n\u003cp\u003e命令行工具提供了上传文章的功能。你需要先使用 Markdown 格式写好内容。\u003c/p\u003e\n\u003cp\u003e上传文章使用 upload 命令，后面跟参数题目，关键字，Markdown 文件，是否公开，是否加锁。其中前 3 项必填。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egart upload title keyword content ispub islock\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e注意：上传完成后，可以在豆子碎片小程序中进行体验。\u003c/p\u003e\n\u003ch2 id=\"如何更新维护豆子碎片文章\"\u003e如何更新维护豆子碎片文章？\u003c/h2\u003e\n\u003cp\u003e命令行工具提供了维护文章的功能。已经上传的文章可以更新标题、关键字、文件内容、是否公开、是否加锁、限制同城访问、还可以强制公开。\u003c/p\u003e\n\u003cp\u003e相关的命令可通过下面的命令查看\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egart --help\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e注意：更新完成后，可以在豆子碎片小程序中进行体验。\u003c/p\u003e","title":"用户如何上传和更新Markdown文章？"},{"content":"如何获取有效城市？ 命令行工具提供了同城访问文章功能。在限制文章同城时，需要提供城市信息，为此，我们需要获取正确的城市名称。\n下载最新版本的命令行工具。\n先获取有效省份名称，如果是自治区或者直辖市，下面的命令也都支持。\ngart area 1 然后获取有效城市名称，例如我查询河南省的有效城市。\ngart area 2 河南省 注意：以上数据来源参考微信地址信息，精确到地级市或同等行政级别。\n如何限制文章同城访问？ 命令行工具提供了限制文章同城访问功能。 下载最新版本的命令行工具。\n需要先使用 area 命令获取有效城市，然后使用下面命令限制文章访问。\ngart city uuid 城市名称 注意：更新成功后，可以在豆子碎片小程序中进行体验。\n","permalink":"https://blog.91demo.top/wiki/gupart-city.html","summary":"\u003ch2 id=\"如何获取有效城市\"\u003e如何获取有效城市？\u003c/h2\u003e\n\u003cp\u003e命令行工具提供了同城访问文章功能。在限制文章同城时，需要提供城市信息，为此，我们需要获取正确的城市名称。\u003c/p\u003e\n\u003cp\u003e下载最新版本的命令行工具。\u003c/p\u003e\n\u003cp\u003e先获取有效省份名称，如果是自治区或者直辖市，下面的命令也都支持。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egart area 1\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后获取有效城市名称，例如我查询河南省的有效城市。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egart area 2 河南省\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e注意：以上数据来源参考微信地址信息，精确到地级市或同等行政级别。\u003c/p\u003e\n\u003ch2 id=\"如何限制文章同城访问\"\u003e如何限制文章同城访问？\u003c/h2\u003e\n\u003cp\u003e命令行工具提供了限制文章同城访问功能。 下载最新版本的命令行工具。\u003c/p\u003e\n\u003cp\u003e需要先使用 area 命令获取有效城市，然后使用下面命令限制文章访问。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egart city uuid 城市名称\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e注意：更新成功后，可以在豆子碎片小程序中进行体验。\u003c/p\u003e","title":"上传工具实现获取有效城市功能"},{"content":"更新文章标题 有的时候，我们上传完文章之后，会发现我们的文章标题不合适，或想更好的描述文章内容。如果没有这个接口，我们需要删除文章，然后再重新上传。所以，我们实现这个功能，方便我们修改文章标题。\n修改文章标题，我打算使用的命令格式如下：\ngart title uuid newtitle 其中，紧挨着 gart 的 title 是命令，表示要更新文章标题，uuid 是文章的主键，可以查询识别是哪篇文章，newtitle 就是要修改的文章标题了。\n实现的代码如下：\nfunc init() { rootCmd.AddCommand(uptTitleCmd) } var uptTitleCmd = \u0026amp;cobra.Command{ Use: \u0026#34;title\u0026#34;, Short: \u0026#34;更新文章标题，参数需要UUID，新的标题。\u0026#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, \u0026amp;uar) if err != nil { fmt.Println(\u0026#34;更新标题发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;更新成功\u0026#34;) } }, } 实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。\n更新文章关键字 如果想要更好的搜索结果，或者想要给文章分类，都可以添加关键字，关键字是搜索中的一个因子。例如，我的文章是描述的一个 go 教程，而标题中没有 go 的字眼，这是我在关键字中添加 go，就可以根据 go 搜索出这篇文章。关键字如此重要，所以我们要认真的定义，依据你的规则，你想要的效果来设计关键字。今天，我们实现这个功能，方便我们修改文章关键字。\n修改文章关键字，我打算使用的命令格式如下：\ngart keyword uuid newkeyword 其中，紧挨着 gart 的 keyword 是命令，表示要更新文章关键字，uuid 是文章的主键，可以查询识别是哪篇文章，newkeyword 就是要修改的文章关键字了，注意，这里是整个替换，所以你要加新的关键字，也把要保留的关键字加上去。\n实现的代码如下：\nfunc init() { rootCmd.AddCommand(uptKeywordCmd) } var uptKeywordCmd = \u0026amp;cobra.Command{ Use: \u0026#34;keyword\u0026#34;, Short: \u0026#34;更新文章关键字，参数需要UUID，新的关键字。\u0026#34;, Long: `更新文章关键字，参数需要UUID，新的关键字，需要先获取文章的UUID。`, Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { uuid := args[0] keyword := args[1] uar := service.UpdateArtReq{ Uuid: uuid, Keyword: keyword, UptType: 2, } err := service.UpdateArt(token, \u0026amp;uar) if err != nil { fmt.Println(\u0026#34;更新关键字发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;更新成功\u0026#34;) } }, } 实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。\n更新文章内容 当我们写好了文章，并上传到服务器，我们在修改调整本地 Markdown 文件后，服务器上的文章并不会更新，所以，如果你想要将更新的内容反馈到服务器上时，你可以修改文章的内容。今天，我们实现这个功能，方便我们修改文章内容。\n修改文章内容，我打算使用的命令格式如下：\ngart content uuid newfilename 其中，紧挨着 gart 的 content 是命令，表示要更新文章内容，uuid 是文章的主键，可以查询识别是哪篇文章，newfilename 是修改内容后的文件名称。\n实现的代码如下：\nfunc init() { rootCmd.AddCommand(uptContentCmd) } var uptContentCmd = \u0026amp;cobra.Command{ Use: \u0026#34;content\u0026#34;, Short: \u0026#34;更新文章内容，参数需要UUID，新的文件内容。\u0026#34;, Long: `更新文章内容，参数需要UUID，新的文件内容，需要先获取文章的UUID。`, Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { uuid := args[0] filename := args[1] content, err := service.GetFileContent(filename) if err != nil { fmt.Println(\u0026#34;读取文件内容发生错误,\u0026#34;, err) return } uar := service.UpdateArtReq{ Uuid: uuid, Content: string(content), UptType: 3, } err = service.UpdateArt(token, \u0026amp;uar) if err != nil { fmt.Println(\u0026#34;更新内容发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;更新成功\u0026#34;) } }, } 实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。\n是否将文章公开？ 我们会有这样的需求，想将文章公开，这样其它的用户可以查询到你的文章，或者我们想将公开的文章重新设置为私密。今天，我们实现这个功能，方便我们修改文章是否公开。\n注意，将文章公开，后台要进行审核，审核通过的文章其它用户才可以查询到。要公开的文章请以技术为主题，可以是技术文章，或者经验分享。文章的展示是以微信小程序为载体的，所以要符合小程序审核的要求。\n修改文章是否公开，我打算使用的命令格式如下：\ngart public uuid ispub 其中，紧挨着 gart 的 public 是命令，表示要更新文章是否公开，uuid 是文章的主键，可以查询识别是哪篇文章，ispub 只允许设置 0 或者 1,1 为公开。\n实现的代码如下：\nfunc init() { rootCmd.AddCommand(uptPublicCmd) } var uptPublicCmd = \u0026amp;cobra.Command{ Use: \u0026#34;public\u0026#34;, Short: \u0026#34;更新文章是否公开，参数需要UUID，新的公开开关，只允许0或1,1为公开。\u0026#34;, Long: `更新文章是否公开，参数需要UUID，新的公开开关，只允许0或1,1为公开，需要先获取文章的UUID。`, Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { uuid := args[0] ispub := utils.Str2Int(args[1]) uar := service.UpdateArtReq{ Uuid: uuid, IsPub: ispub, UptType: 4, } err := service.UpdateArt(token, \u0026amp;uar) if err != nil { fmt.Println(\u0026#34;更新公开开关发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;更新成功\u0026#34;) } }, } 实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。\n强制更新文章公开 当我的文章需要马上公开时，可以使用强制更新文章公开功能。这个功能需要消耗更多的豆子点数。\n下载最新版本的命令行工具。\n使用如下命令即可，请先获取文章的 UUID。\ngart forcepub uuid 注意：文章内容必须合法合规，符合技术相关的主题。\n是否将文章加锁？ 我们会有这样的需求，想将文章加锁，这样其它的用户访问你将获得豆子点数奖励，或者我们想将加锁的文章重新设置为不加锁。今天，我们实现这个功能，方便我们修改文章是否加锁。\n注意，将文章加锁，需要先将文章公开。\n修改文章是否加锁，我打算使用的命令格式如下：\ngart lock uuid islock 其中，紧挨着 gart 的 lock 是命令，表示要更新文章是否加锁，uuid 是文章的主键，可以查询识别是哪篇文章，islock 只允许设置 0 或者 1,1 为加锁。\n实现的代码如下：\nfunc init() { rootCmd.AddCommand(uptLockCmd) } var uptLockCmd = \u0026amp;cobra.Command{ Use: \u0026#34;lock\u0026#34;, Short: \u0026#34;更新文章是否加锁，参数需要UUID，新的加锁开关，只允许0或1,1为加锁。\u0026#34;, Long: `更新文章是否加锁，参数需要UUID，新的加锁开关，只允许0或1,1为加锁，需要先获取文章的UUID。`, Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { uuid := args[0] islock := utils.Str2Int(args[1]) uar := service.UpdateArtReq{ Uuid: uuid, IsLock: islock, UptType: 5, } err := service.UpdateArt(token, \u0026amp;uar) if err != nil { fmt.Println(\u0026#34;更新加锁开关发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;更新成功\u0026#34;) } }, } 实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。\n如何多个命令共用一个接口？ 前面修改文章的几个命令，我们看到一个现象，它们共同调用了同一个函数 UpdateArt，我今天讲下，多个命令是如何做到调用同一个函数实现的？\n先看下实现源码：\n// 更新文章请求 type UpdateArtReq struct { Uuid string `json:\u0026#34;uuid\u0026#34;` // uuid Title string `json:\u0026#34;title\u0026#34;` // 题目 Keyword string `json:\u0026#34;keyword\u0026#34;` // 关键字 Content string `json:\u0026#34;content\u0026#34;` // 内容 IsPub int `json:\u0026#34;ispub\u0026#34;` // 是否公开 IsLock int `json:\u0026#34;islock\u0026#34;` // 是否加锁 UptType int `json:\u0026#34;utype\u0026#34;` // 更新类型 1，题目 2，关键字 3，内容 4，是否公开 5，是否加锁 } // 更新文章信息 func UpdateArt(token string, uar *UpdateArtReq) error { if token == \u0026#34;\u0026#34; { return errors.New(\u0026#34;token不能为空\u0026#34;) } url := fmt.Sprintf(\u0026#34;%s/uptVArt?token=%s\u0026#34;, OPEN_URL, token) reqBody := new(bytes.Buffer) json.NewEncoder(reqBody).Encode(uar) req, err := http.NewRequest(\u0026#34;POST\u0026#34;, url, reqBody) if err != nil { return err } req.Header.Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) client := \u0026amp;http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return err } var result RespMsg err = json.Unmarshal(data, \u0026amp;result) if err != nil { return err } if result.Code == 1 { return nil } else { return errors.New(result.Msg) } } 它的原理非常简单，定义一个结构体，保存所有要变更的字段信息，再新增一个字段定义变更类型，例如源码中的注释，后台根据这个字段类型，去修改对应的内容。\n文章命令行工具开发部分基本就完成了，后续有新的功能或者优化再开新的文章。\n感谢您的观看！\n","permalink":"https://blog.91demo.top/wiki/gupart-update.html","summary":"\u003ch2 id=\"更新文章标题\"\u003e更新文章标题\u003c/h2\u003e\n\u003cp\u003e有的时候，我们上传完文章之后，会发现我们的文章标题不合适，或想更好的描述文章内容。如果没有这个接口，我们需要删除文章，然后再重新上传。所以，我们实现这个功能，方便我们修改文章标题。\u003c/p\u003e\n\u003cp\u003e修改文章标题，我打算使用的命令格式如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egart title uuid newtitle\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e其中，紧挨着 gart 的 title 是命令，表示要更新文章标题，uuid 是文章的主键，可以查询识别是哪篇文章，newtitle 就是要修改的文章标题了。\u003c/p\u003e\n\u003cp\u003e实现的代码如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\nfunc init() {\n\trootCmd.AddCommand(uptTitleCmd)\n}\n\nvar uptTitleCmd = \u0026amp;cobra.Command{\n\tUse:   \u0026#34;title\u0026#34;,\n\tShort: \u0026#34;更新文章标题，参数需要UUID，新的标题。\u0026#34;,\n\tLong:  `更新文章标题，参数需要UUID，新的标题，需要先获取文章的UUID。`,\n\tArgs:  cobra.MinimumNArgs(2),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tuuid := args[0]\n\t\ttitle := args[1]\n\t\tuar := service.UpdateArtReq{\n\t\t\tUuid:    uuid,\n\t\t\tTitle:   title,\n\t\t\tUptType: 1,\n\t\t}\n\t\terr := service.UpdateArt(token, \u0026amp;uar)\n\t\tif err != nil {\n\t\t\tfmt.Println(\u0026#34;更新标题发生错误,\u0026#34;, err)\n\t\t} else {\n\t\t\tfmt.Println(\u0026#34;更新成功\u0026#34;)\n\t\t}\n\t},\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e实现比较简单，除了看命令行输出，还可以去小程序中查看实际结果。\u003c/p\u003e","title":"上传工具实现更新功能"},{"content":"删除文章 当上传完文章之后，由于各种原因，会有删除文章的需求。今天我们实现了删除文章的功能。删除文章，需要先知道文章的 UUID，这个可以通过上一节介绍的搜索文章查到。\nfunc init() { rootCmd.AddCommand(rmCmd) } var rmCmd = \u0026amp;cobra.Command{ Use: \u0026#34;remove\u0026#34;, Short: \u0026#34;删除文章\u0026#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(\u0026#34;删除发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;删除成功\u0026#34;) } }, } 实现比较简单，当删除后，可以看命令行输出，也可以去小程序中查看还有没有该篇文章。\n","permalink":"https://blog.91demo.top/wiki/gupart-delete.html","summary":"\u003ch2 id=\"删除文章\"\u003e删除文章\u003c/h2\u003e\n\u003cp\u003e当上传完文章之后，由于各种原因，会有删除文章的需求。今天我们实现了删除文章的功能。删除文章，需要先知道文章的 UUID，这个可以通过上一节介绍的搜索文章查到。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\nfunc init() {\n\trootCmd.AddCommand(rmCmd)\n}\n\nvar rmCmd = \u0026amp;cobra.Command{\n\tUse:   \u0026#34;remove\u0026#34;,\n\tShort: \u0026#34;删除文章\u0026#34;,\n\tLong:  `删除文章， 需要先获取文章的UUID。`,\n\tArgs:  cobra.MinimumNArgs(1),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tuuid := args[0]\n\t\terr := service.RemoveArt(token, uuid)\n\t\tif err != nil {\n\t\t\tfmt.Println(\u0026#34;删除发生错误,\u0026#34;, err)\n\t\t} else {\n\t\t\tfmt.Println(\u0026#34;删除成功\u0026#34;)\n\t\t}\n\t},\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e实现比较简单，当删除后，可以看命令行输出，也可以去小程序中查看还有没有该篇文章。\u003c/p\u003e","title":"上传工具实现删除文章"},{"content":"查询文章 当我们上传了文章之后，肯定希望能够查询我们上传的文章了，查询文章根据用户输入的内容，后台进行文章的标题和关键字进行匹配。然后返回查询到的数据。后台限制最多返回 20 条记录，这是为了服务器性能综合考虑的。如果用户查找的内容，不在返回的记录中，请输入更多内容，进行更精确的匹配。\nfunc init() { rootCmd.AddCommand(qCmd) } var qCmd = \u0026amp;cobra.Command{ Use: \u0026#34;search\u0026#34;, Short: \u0026#34;查找文章，最多返回20条记录。\u0026#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(\u0026#34;查询发生错误,\u0026#34;, err) } else { n := len(list) if n \u0026gt; 0 { var ( cts string uts string ispub string = \u0026#34;否\u0026#34; islock string = \u0026#34;否\u0026#34; ) t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{\u0026#34;UUID\u0026#34;, \u0026#34;题目\u0026#34;, \u0026#34;关键字\u0026#34;, \u0026#34;是否公开\u0026#34;, \u0026#34;是否加锁\u0026#34;, \u0026#34;创建时间\u0026#34;, \u0026#34;修改时间\u0026#34;}) for _, v := range list { cts = utils.TS2Str(v.Createtime) uts = utils.TS2Str(v.Updatetime) if v.IsPub == 1 { ispub = \u0026#34;是\u0026#34; } if v.IsLock == 1 { islock = \u0026#34;是\u0026#34; } t.AppendRows([]table.Row{ {v.Uuid, v.Title, v.Keyword, ispub, islock, cts, uts}, }) t.AppendSeparator() } t.Render() } else { fmt.Println(\u0026#34;未找到记录，尝试更换内容试试。\u0026#34;) } } }, } 代码的组织架构和上传的差不多。都需要先定义命令，然后添加到 root 命令中。这里的输出结果使用美化的 table 显示。当查询到内容后，就可以使用结果中的 UUID 进行文章的管理维护了。\n","permalink":"https://blog.91demo.top/wiki/gupart-query.html","summary":"\u003ch2 id=\"查询文章\"\u003e查询文章\u003c/h2\u003e\n\u003cp\u003e当我们上传了文章之后，肯定希望能够查询我们上传的文章了，查询文章根据用户输入的内容，后台进行文章的标题和关键字进行匹配。然后返回查询到的数据。后台限制最多返回 20 条记录，这是为了服务器性能综合考虑的。如果用户查找的内容，不在返回的记录中，请输入更多内容，进行更精确的匹配。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\nfunc init() {\n\trootCmd.AddCommand(qCmd)\n}\n\nvar qCmd = \u0026amp;cobra.Command{\n\tUse:   \u0026#34;search\u0026#34;,\n\tShort: \u0026#34;查找文章，最多返回20条记录。\u0026#34;,\n\tLong:  `查找文章， 根据文章的标题和关键字匹配查询，最多返回20条记录。可以输入更多的内容进行精确查找。`,\n\tArgs:  cobra.MinimumNArgs(1),\n\tRun: func(cmd *cobra.Command, args []string) {\n\n\t\tcontent := args[0]\n\n\t\tlist, err := service.SearchArt(token, content)\n\t\tif err != nil {\n\t\t\tfmt.Println(\u0026#34;查询发生错误,\u0026#34;, err)\n\t\t} else {\n\t\t\tn := len(list)\n\t\t\tif n \u0026gt; 0 {\n\t\t\t\tvar (\n\t\t\t\t\tcts    string\n\t\t\t\t\tuts    string\n\t\t\t\t\tispub  string = \u0026#34;否\u0026#34;\n\t\t\t\t\tislock string = \u0026#34;否\u0026#34;\n\t\t\t\t)\n\n\t\t\t\tt := table.NewWriter()\n\t\t\t\tt.SetOutputMirror(os.Stdout)\n\t\t\t\tt.AppendHeader(table.Row{\u0026#34;UUID\u0026#34;, \u0026#34;题目\u0026#34;, \u0026#34;关键字\u0026#34;, \u0026#34;是否公开\u0026#34;, \u0026#34;是否加锁\u0026#34;, \u0026#34;创建时间\u0026#34;, \u0026#34;修改时间\u0026#34;})\n\t\t\t\tfor _, v := range list {\n\t\t\t\t\tcts = utils.TS2Str(v.Createtime)\n\t\t\t\t\tuts = utils.TS2Str(v.Updatetime)\n\t\t\t\t\tif v.IsPub == 1 {\n\t\t\t\t\t\tispub = \u0026#34;是\u0026#34;\n\t\t\t\t\t}\n\t\t\t\t\tif v.IsLock == 1 {\n\t\t\t\t\t\tislock = \u0026#34;是\u0026#34;\n\t\t\t\t\t}\n\t\t\t\t\tt.AppendRows([]table.Row{\n\t\t\t\t\t\t{v.Uuid, v.Title, v.Keyword, ispub, islock, cts, uts},\n\t\t\t\t\t})\n\t\t\t\t\tt.AppendSeparator()\n\t\t\t\t}\n\n\t\t\t\tt.Render()\n\t\t\t} else {\n\t\t\t\tfmt.Println(\u0026#34;未找到记录，尝试更换内容试试。\u0026#34;)\n\t\t\t}\n\t\t}\n\n\t},\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e代码的组织架构和上传的差不多。都需要先定义命令，然后添加到 root 命令中。这里的输出结果使用美化的 table 显示。当查询到内容后，就可以使用结果中的 UUID 进行文章的管理维护了。\u003c/p\u003e","title":"上传工具实现查询文章"},{"content":"上传文章 定义上传文章的 command，并将 command 添加到 Root 中。\nfunc init() { rootCmd.AddCommand(upCmd) } var upCmd = \u0026amp;cobra.Command{ Use: \u0026#34;upload\u0026#34;, Short: \u0026#34;上传文章，可在豆子碎片小程序中查看。\u0026#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(\u0026#34;上传参数如下：\u0026#34;) var isPubStr string = \u0026#34;否\u0026#34; var isLockStr string = \u0026#34;否\u0026#34; if ispub == 1 { isPubStr = \u0026#34;是\u0026#34; } if islock == 1 { isLockStr = \u0026#34;是\u0026#34; } t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{\u0026#34;题目\u0026#34;, \u0026#34;关键字\u0026#34;, \u0026#34;文件名称\u0026#34;, \u0026#34;是否公开\u0026#34;, \u0026#34;是否加锁\u0026#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(\u0026#34;上传发生错误,\u0026#34;, err) } else { fmt.Println(\u0026#34;上传成功\u0026#34;) } }, } 在上面的实现中，使用了 cobra 参数约束函数，Args: cobra.RangeArgs(3, 5),，保证参数最少 3 个，最多不能超过 5 个。这完全满足我的需求。\n在打印列表时，使用 go-pretty 库的 table 组件，可以美化控制台输出，我这里以表格的形式显示上传的参数，一目了然。\n剩下的就简单了，调用后台上传接口，返回结果然后展示就可以了。\n","permalink":"https://blog.91demo.top/wiki/gupart-upload.html","summary":"\u003ch2 id=\"上传文章\"\u003e上传文章\u003c/h2\u003e\n\u003cp\u003e定义上传文章的 command，并将 command 添加到 Root 中。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efunc init() {\n\trootCmd.AddCommand(upCmd)\n}\n\nvar upCmd = \u0026amp;cobra.Command{\n\tUse:   \u0026#34;upload\u0026#34;,\n\tShort: \u0026#34;上传文章，可在豆子碎片小程序中查看。\u0026#34;,\n\tLong: `上传文章，可在豆子碎片小程序中查看。参数依次为题目，关键字，Markdown文件，是否公开，是否加锁。\n\t参数题目，关键字，Markdown文件必填，是否公开，是否加锁选填，默认为否。\n\t例如：gart upload 上传示例1 命令行，工具 ./example.md\n\t`,\n\tArgs: cobra.RangeArgs(3, 5),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tvar (\n\t\t\ttitle    string\n\t\t\tkeyword  string\n\t\t\tfilename string\n\t\t\tispub    int\n\t\t\tislock   int\n\t\t)\n\t\ttitle = args[0]\n\t\tkeyword = args[1]\n\t\tfilename = args[2]\n\t\tl := len(args)\n\t\tswitch l {\n\t\tcase 4:\n\t\t\tispub = utils.Str2Int(args[3])\n\t\tcase 5:\n\t\t\tispub = utils.Str2Int(args[3])\n\t\t\tislock = utils.Str2Int(args[4])\n\t\t}\n\t\tfmt.Println(\u0026#34;上传参数如下：\u0026#34;)\n\t\tvar isPubStr string = \u0026#34;否\u0026#34;\n\t\tvar isLockStr string = \u0026#34;否\u0026#34;\n\t\tif ispub == 1 {\n\t\t\tisPubStr = \u0026#34;是\u0026#34;\n\t\t}\n\t\tif islock == 1 {\n\t\t\tisLockStr = \u0026#34;是\u0026#34;\n\t\t}\n\t\tt := table.NewWriter()\n\t\tt.SetOutputMirror(os.Stdout)\n\t\tt.AppendHeader(table.Row{\u0026#34;题目\u0026#34;, \u0026#34;关键字\u0026#34;, \u0026#34;文件名称\u0026#34;, \u0026#34;是否公开\u0026#34;, \u0026#34;是否加锁\u0026#34;})\n\t\tt.AppendRows([]table.Row{\n\t\t\t{title, keyword, filename, isPubStr, isLockStr},\n\t\t})\n\t\tt.AppendSeparator()\n\t\tt.Render()\n\t\terr := service.UploadArt(token, title, keyword, filename, ispub, islock)\n\t\tif err != nil {\n\t\t\tfmt.Println(\u0026#34;上传发生错误,\u0026#34;, err)\n\t\t} else {\n\t\t\tfmt.Println(\u0026#34;上传成功\u0026#34;)\n\t\t}\n\t},\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在上面的实现中，使用了 cobra 参数约束函数，\u003ccode\u003eArgs: cobra.RangeArgs(3, 5),\u003c/code\u003e，保证参数最少 3 个，最多不能超过 5 个。这完全满足我的需求。\u003c/p\u003e","title":"上传工具实现上传文章命令"},{"content":"添加更多的命令 对于添加更多的命令，使用 flag，就有点麻烦了，这次我们使用一个更高级的库 cobra。同时，我们使用 viper 替换 ini 库，这个库可以读取多种格式的配置文件，可以读取环境变量。\n要实现的功能如下：\n上传文章 文章删除 更新文章标题 更新文章关键字 更新文章内容 文章公开开关 文章加锁开关 根据标题查找文章 根据关键字查询文章 现在使用 cobra 实现上面的命令。\n首先，我想要的效果如下：\n上传文章 gart upload title keyword filename ispub islock 删除文章 gart remove uuid 更新文章标题 gart updatetitle uuid title 更新文章关键字 gart updatekeyword uuid keyword 更新文章内容 gart updatecontent uuid filename 更新文章公开或不公开 gart updatepub uuid ispub 更新文章加锁或不加锁 gart updatelock uuid islock 根据标题或关键字查找文章 gart search content 上面的 upload,remove,updatetitle,updatekeyword 等，在 cobra 中都是命令。title keyword 等都是参数。\n我在读 cobra 文档后，最疑惑的地方就是标志和参数，这两者最大的区别就是标志需要在命令中添加\u0026ndash;flag 然后是值，而参数是直接跟在命令后，直接就是内容的。标志可以更改程序的行为。\n我在这个程序中，配置了标志 config，就是获取配置文件。当用户在命令行中配置\u0026ndash;config xxx.file 就从命令中读取配置文件，否则就在用户的 HOME 目录查找配置文件。获取 icode 和 isecret。\n详细阅读 cobra 文档后，开始撸代码。参考样例文档，我们也先改造 go 入口文件 main.go。\npackage main import \u0026#34;gart/cmd\u0026#34; func main() { cmd.Execute() } 就是这样的简单，只有一个执行函数。它会加载 cmd 文件夹下的 root 命令。\ncmd 文件夹组织如下：\ncmd root.go upload.go remove.go ... 我们看下 root.go，这个文件还是很重要的。\nvar ( cfgFile string token string ) func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(\u0026amp;cfgFile, \u0026#34;config\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;config file (default is $HOME/conf.toml)\u0026#34;) } func initConfig() { if cfgFile != \u0026#34;\u0026#34; { viper.SetConfigFile(cfgFile) } else { home, err := homedir.Dir() if err != nil { fmt.Println(err) os.Exit(1) } viper.AddConfigPath(home) viper.AddConfigPath(\u0026#34;.\u0026#34;) viper.SetConfigName(\u0026#34;conf\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) } if err := viper.ReadInConfig(); err != nil { fmt.Println(\u0026#34;Cant\u0026#39;t read config:\u0026#34;, err) os.Exit(1) } // 每次启动都调用token getToken() } // 根命令 var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;gart\u0026#34;, Short: \u0026#34;gart 是文章管理命令行工具。\u0026#34;, Long: `gart 是文章管理命令行工具，主要用来管理豆子碎片小程序中的文章。`, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;该命令行工具用来管理豆子碎片小程序的文章。\u0026#34;) }, } // main函数中调用的该函数 func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } 首先，需要定义根命令 rootCmd，然后实现 Execute()函数。\n在初始化 init()函数中，定义了 cobra 初始化回调函数，进行了全局标志的配置以及配置文件的加载。\n我这里还添加了 token 的获取，这个花费了我一些时间，琢磨它的调用顺序。它的调用顺序如下：\ninit()-\u0026gt;initConfig()-\u0026gt;getToken()\n这之后，token 的变量就是有效的。在其它的命令中就可以直接使用。\ngetToken 函数定义如下：\nfunc getToken() { var err error icode := viper.GetString(\u0026#34;icode\u0026#34;) isecret := viper.GetString(\u0026#34;isecret\u0026#34;) expire := viper.GetString(\u0026#34;expire_at\u0026#34;) expireAt := utils.Str2Int64(expire) now := time.Now().Unix() if now \u0026gt; expireAt { token, err = service.GetToken(icode, isecret) if err != nil { fmt.Println(\u0026#34;获取token错误,\u0026#34;, err) os.Exit(1) } expireAtStr := strconv.FormatInt(now+7000, 10) viper.Set(\u0026#34;expire_at\u0026#34;, expireAtStr) viper.Set(\u0026#34;token\u0026#34;, token) err = viper.WriteConfig() if err != nil { fmt.Println(\u0026#34;写入配置文件错误,\u0026#34;, err) os.Exit(1) } } else { token = viper.GetString(\u0026#34;token\u0026#34;) } } 在 Viper 这回写到文件中，遇到了点问题，刚开始获取 token 后，无法写入到配置文件中。参考文档，已经设置了新的有效时间和 token。就是这两句代码：\nviper.Set(\u0026#34;expire_at\u0026#34;, expireAtStr) viper.Set(\u0026#34;token\u0026#34;, token) 排查后，发现没有调用下面的函数原因：\nerr = viper.WriteConfig() if err != nil { fmt.Println(\u0026#34;写入配置文件错误,\u0026#34;, err) os.Exit(1) } 经历一番挫折后，该库的简单使用已掌握，接下来先完成上传文章。\n","permalink":"https://blog.91demo.top/wiki/gupart-cobra.html","summary":"\u003ch2 id=\"添加更多的命令\"\u003e添加更多的命令\u003c/h2\u003e\n\u003cp\u003e对于添加更多的命令，使用 flag，就有点麻烦了，这次我们使用一个更高级的库 cobra。同时，我们使用 viper 替换 ini 库，这个库可以读取多种格式的配置文件，可以读取环境变量。\u003c/p\u003e\n\u003cp\u003e要实现的功能如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e上传文章\u003c/li\u003e\n\u003cli\u003e文章删除\u003c/li\u003e\n\u003cli\u003e更新文章标题\u003c/li\u003e\n\u003cli\u003e更新文章关键字\u003c/li\u003e\n\u003cli\u003e更新文章内容\u003c/li\u003e\n\u003cli\u003e文章公开开关\u003c/li\u003e\n\u003cli\u003e文章加锁开关\u003c/li\u003e\n\u003cli\u003e根据标题查找文章\u003c/li\u003e\n\u003cli\u003e根据关键字查询文章\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e现在使用 cobra 实现上面的命令。\u003c/p\u003e\n\u003cp\u003e首先，我想要的效果如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e上传文章 gart upload title keyword filename ispub islock\u003c/li\u003e\n\u003cli\u003e删除文章 gart remove uuid\u003c/li\u003e\n\u003cli\u003e更新文章标题 gart updatetitle uuid title\u003c/li\u003e\n\u003cli\u003e更新文章关键字 gart updatekeyword uuid keyword\u003c/li\u003e\n\u003cli\u003e更新文章内容 gart updatecontent uuid filename\u003c/li\u003e\n\u003cli\u003e更新文章公开或不公开 gart updatepub uuid ispub\u003c/li\u003e\n\u003cli\u003e更新文章加锁或不加锁 gart updatelock uuid islock\u003c/li\u003e\n\u003cli\u003e根据标题或关键字查找文章 gart search content\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e上面的 upload,remove,updatetitle,updatekeyword 等，在 cobra 中都是命令。title keyword 等都是参数。\u003c/p\u003e","title":"上传工具使用Cobra实现添加更多的命令"},{"content":"这是 Golang 实现的上传文章以及管理文章的一个命令行工具。\n项目地址 https://gitee.com/littletow/upart-go\n实现一个上传文章的命令行工具 这是一个最初的版本，使用 flag 和 ini 来实现文章上传功能。使用 flag 来解析命令行参数，使用 Ini 配置文件，记录识别码，以及 token。记录 token 的原因是因为每次启动命令行，都需要重新获取 token，为了减少 token 获取次数，在获取到 token 后，同时存储到 Ini 配置文件中。每次命令行启动，优先查看配置文件中的 token。\n开发这个工具主要有以下几个技术要点：\n从命令行获取参数 从配置文件中读取参数 读取文件内容 请求后端接口 我们通过分析后，需要定义以下几个参数：\ntitle 标题 keyword 关键字 filename MD 文件名 ispub 是否公开 islock 是否加锁 使用 Go 代码定义参数和结构体\nvar ( title string keyword string filename string ispub int // 1 pub islock int // 1 lock ) func init() { flag.StringVar(\u0026amp;title, \u0026#34;b\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;文章题目\u0026#34;) flag.StringVar(\u0026amp;keyword, \u0026#34;k\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;文章关键字\u0026#34;) flag.StringVar(\u0026amp;filename, \u0026#34;f\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;MD文件\u0026#34;) flag.IntVar(\u0026amp;islock, \u0026#34;l\u0026#34;, 0, \u0026#34;是否加锁\u0026#34;) flag.IntVar(\u0026amp;ispub, \u0026#34;p\u0026#34;, 0, \u0026#34;是否开放\u0026#34;) } func main(){ flag.Parse() } init() 在 main 函数前调用。需要在 main 函数中调用 flag.Parse()，这一步非常关键。之后就可以使用变量了。\n配置文件样例如下：\n[User] icode = 你的icode isecret = 你的isecret token = expire_at = 0 其中，icode 和 isecret 为识别码内容，从小程序端获取。\n配置文件库使用gopkg.in/ini.v1，可以读取和写入 ini 文件。\ndir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { fmt.Println(\u0026#34;获取系统目录错误,\u0026#34;, err) os.Exit(1) } confFile := filepath.Join(dir, \u0026#34;conf.ini\u0026#34;) cfg, err := ini.Load(confFile) if err != nil { fmt.Println(\u0026#34;读取配置文件错误,\u0026#34;, err) os.Exit(1) } icode := cfg.Section(\u0026#34;User\u0026#34;).Key(\u0026#34;icode\u0026#34;).String() isecret := cfg.Section(\u0026#34;User\u0026#34;).Key(\u0026#34;isecret\u0026#34;).String() token := cfg.Section(\u0026#34;User\u0026#34;).Key(\u0026#34;token\u0026#34;).String() expireAt := cfg.Section(\u0026#34;User\u0026#34;).Key(\u0026#34;expire_at\u0026#34;).MustInt64() 设计的默认配置文件存放在命令行同级目录，但是当在其它目录运行时，会报错配置文件无法找到，为了解决这个问题，需要找到命令行的目录，然后在该目录下再查找配置文件。\n为了解决每次运行命令行，都需要重新获取 token，将 token 存储在 ini 配置文件中，并添加超时时间，当在有效时间内，不再重新获取 token。\n// 刷新token now := time.Now().Unix() if now \u0026gt; expireAt { token, err = getToken(icode, isecret) if err != nil { fmt.Println(\u0026#34;获取token错误,\u0026#34;, err) os.Exit(1) } expireAtStr := strconv.FormatInt(now+7000, 10) cfg.Section(\u0026#34;User\u0026#34;).Key(\u0026#34;token\u0026#34;).SetValue(token) cfg.Section(\u0026#34;User\u0026#34;).Key(\u0026#34;expire_at\u0026#34;).SetValue(expireAtStr) cfg.SaveTo(confFile) } 看看 token 的获取方式，使用 net/http 标准库。\nfunc getToken(icode string, isecret string) (string, error) { url := fmt.Sprintf(\u0026#34;%s/vtoken?icode=%s\u0026amp;isecret=%s\u0026#34;, OPEN_URL, icode, isecret) resp, err := http.Get(url) if err != nil { return \u0026#34;\u0026#34;, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return \u0026#34;\u0026#34;, err } var result RespData err = json.Unmarshal(data, \u0026amp;result) if err != nil { return \u0026#34;\u0026#34;, err } if result.Code == 1 { return result.Data, nil } else { return \u0026#34;\u0026#34;, errors.New(result.Msg) } } 我一般提前编制好 Markdown 文件，然后放在某个目录，然后使用命令工具上传，在上传的时候，就需要读取文件。\nfilecontent, err := os.ReadFile(filename) if err != nil { return err } 这里实现，简单粗暴，因为文件不会很大，所以一次性读取。当要读取很大的文件时，可以使用缓存，提高性能。\n最后就是上传文章接口了。这个实现如下：\nfunc uploadArt(token string, title string, keyword string, filename string, ispub int, islock int) error { if token == \u0026#34;\u0026#34; { return errors.New(\u0026#34;token不能为空\u0026#34;) } url := fmt.Sprintf(\u0026#34;%s/upVArt?token=%s\u0026#34;, OPEN_URL, token) filecontent, err := os.ReadFile(filename) if err != nil { return err } snippet := string(filecontent) reqBody := new(bytes.Buffer) art := ArtReq{ Title: title, Keyword: keyword, Content: snippet, IsPub: ispub, IsLock: islock, } json.NewEncoder(reqBody).Encode(art) req, err := http.NewRequest(\u0026#34;POST\u0026#34;, url, reqBody) if err != nil { return err } req.Header.Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) client := \u0026amp;http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return err } var result RespMsg err = json.Unmarshal(data, \u0026amp;result) if err != nil { return err } if result.Code == 1 { return nil } else { return errors.New(result.Msg) } } 上传功能已经完成。\n现在有了新的需求，可以更新文章，删除文章。请看下一章如何实现？\n","permalink":"https://blog.91demo.top/wiki/gupart.html","summary":"\u003cp\u003e这是 Golang 实现的上传文章以及管理文章的一个命令行工具。\u003c/p\u003e\n\u003ch2 id=\"项目地址\"\u003e项目地址\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://gitee.com/littletow/upart-go\"\u003ehttps://gitee.com/littletow/upart-go\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"实现一个上传文章的命令行工具\"\u003e实现一个上传文章的命令行工具\u003c/h2\u003e\n\u003cp\u003e这是一个最初的版本，使用 flag 和 ini 来实现文章上传功能。使用 flag 来解析命令行参数，使用 Ini 配置文件，记录识别码，以及 token。记录 token 的原因是因为每次启动命令行，都需要重新获取 token，为了减少 token 获取次数，在获取到 token 后，同时存储到 Ini 配置文件中。每次命令行启动，优先查看配置文件中的 token。\u003c/p\u003e\n\u003cp\u003e开发这个工具主要有以下几个技术要点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从命令行获取参数\u003c/li\u003e\n\u003cli\u003e从配置文件中读取参数\u003c/li\u003e\n\u003cli\u003e读取文件内容\u003c/li\u003e\n\u003cli\u003e请求后端接口\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我们通过分析后，需要定义以下几个参数：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003etitle 标题\u003c/li\u003e\n\u003cli\u003ekeyword 关键字\u003c/li\u003e\n\u003cli\u003efilename MD 文件名\u003c/li\u003e\n\u003cli\u003eispub 是否公开\u003c/li\u003e\n\u003cli\u003eislock 是否加锁\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e使用 Go 代码定义参数和结构体\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003evar (\n\ttitle    string\n\tkeyword  string\n\tfilename string\n\tispub    int // 1 pub\n\tislock   int // 1 lock\n\n)\n\nfunc init() {\n\tflag.StringVar(\u0026amp;title, \u0026#34;b\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;文章题目\u0026#34;)\n\tflag.StringVar(\u0026amp;keyword, \u0026#34;k\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;文章关键字\u0026#34;)\n\tflag.StringVar(\u0026amp;filename, \u0026#34;f\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;MD文件\u0026#34;)\n\tflag.IntVar(\u0026amp;islock, \u0026#34;l\u0026#34;, 0, \u0026#34;是否加锁\u0026#34;)\n\tflag.IntVar(\u0026amp;ispub, \u0026#34;p\u0026#34;, 0, \u0026#34;是否开放\u0026#34;)\n}\n\nfunc main(){\n    flag.Parse()\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003einit() 在 main 函数前调用。需要在 main 函数中调用 flag.Parse()，这一步非常关键。之后就可以使用变量了。\u003c/p\u003e","title":"Go 版本文章上传命令行工具"},{"content":"这个工具使用Rust开发，仅实现了上传文章功能，使用 GUI 桌面开发。\n该工具使用的库为 nwg，即 native-windows-gui 库，该库支持老的 Windows GUI。\n这应该算是一个半成品，这是 Rust 实现的一个桌面上传文章工具。它只有上传功能，之所以会这样，是因为后续的开发重心都放在go版本实现上。\n不过这也有一定的价值，首先就是它实现了图形桌面，这个可以学会如何实现一个窗口，显示输入框和按钮，如何布局。然后呢，可以在Rust中使用HTTP上传文件，这也是一个非常棒的技能点。\n以后有精力了，可以扩展完善一下。\n项目地址 https://gitee.com/littletow/upart-rs\n","permalink":"https://blog.91demo.top/wiki/rupart.html","summary":"\u003cp\u003e这个工具使用Rust开发，仅实现了上传文章功能，使用 GUI 桌面开发。\u003c/p\u003e\n\u003cp\u003e该工具使用的库为 nwg，即 native-windows-gui 库，该库支持老的 Windows GUI。\u003c/p\u003e\n\u003cp\u003e这应该算是一个半成品，这是 Rust 实现的一个桌面上传文章工具。它只有上传功能，之所以会这样，是因为后续的开发重心都放在go版本实现上。\u003c/p\u003e\n\u003cp\u003e不过这也有一定的价值，首先就是它实现了图形桌面，这个可以学会如何实现一个窗口，显示输入框和按钮，如何布局。然后呢，可以在Rust中使用HTTP上传文件，这也是一个非常棒的技能点。\u003c/p\u003e\n\u003cp\u003e以后有精力了，可以扩展完善一下。\u003c/p\u003e\n\u003ch2 id=\"项目地址\"\u003e项目地址\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://gitee.com/littletow/upart-rs\"\u003ehttps://gitee.com/littletow/upart-rs\u003c/a\u003e\u003c/p\u003e","title":"Rust 版本文章上传命令行工具"},{"content":"403 登录失败\n解决：等待 1 分钟，等超时之后重新连接即可。一般是切换 IP 时或者换软电话登录会碰到。\n401 登录失败\n解决：检查账户和密码是否正确？检查连接地址是否正确？检查 Asterisk 是否启动？检查防火墙端口是否打开？\n408 连接超时\n解决：一般是 Asterisk 服务不通，检查 Asterisk 服务是否启动，检查防火墙是否打开？\n可以拨通，没有声音？\n一般是 NAT 造成，配置这三个参数：rtp_symmetric，force_rport，rewrite_contact。\nSIP 客户端没有自动挂机？\n一般是没有设置 stun 造成的，在 SIP 客户端，设置 stun 即可。如果没有 stun server，可以设置这个stun.l.google.com:19302\nAGI 脚本返回状态 4，正常应该为 0？\n查看网上资料，是 AGI 脚本中调用 Hangup 导致，将脚本中的 Hangup 去掉，放在拨号计划配置文件中执行 Hangup，可以解决这个问题。\nAGI Script agidemo completed, returning 0 查看客户端是否在线？\npjsip show endpoints 要求最大一个客户端在线，请在aors配置中使用配置\nmax_contacts 设置为 1 MicroSIP客户端配置后还显示IDLE？\nSIP SERVER字段必须填写，和域名地址保持一致。这样状态IDLE会变为ONline。\nMicroSIP客户端连接后，Asterisk不显示在线？\n如果是服务器在公网，客户端在局域网的网络结构：\n我的Asterisk显示的客户端Contact是局域网地址，针对这种情况。\n1，临时解决方法：请在账户配置中，勾选IP REWRITE或ICE，具体选择哪个看服务端的配置。他们分别表示IP地址重写和启用更智能的 NAT 穿透技术。如果需要向好友显示在线状态，请勾选Publish Presence。\n2，更推荐的解决方法：检查Asterisk的PJSIP配置，需要添加 rewrite_contact=yes这是解决 NAT 的“银弹”。它告诉 Asterisk：“忽略报文里写的内网 IP，直接回复给数据包发过来的那个公网地址和端口。”\n设置 rtp_symmetric=yes：确保语音流（RTP）也走同样的路径，防止通话时单通（没声音）。\n","permalink":"https://blog.91demo.top/wiki/errors.html","summary":"\u003cp\u003e403 登录失败\u003c/p\u003e\n\u003cp\u003e解决：等待 1 分钟，等超时之后重新连接即可。一般是切换 IP 时或者换软电话登录会碰到。\u003c/p\u003e\n\u003cp\u003e401 登录失败\u003c/p\u003e\n\u003cp\u003e解决：检查账户和密码是否正确？检查连接地址是否正确？检查 Asterisk 是否启动？检查防火墙端口是否打开？\u003c/p\u003e\n\u003cp\u003e408 连接超时\u003c/p\u003e\n\u003cp\u003e解决：一般是 Asterisk 服务不通，检查 Asterisk 服务是否启动，检查防火墙是否打开？\u003c/p\u003e\n\u003cp\u003e可以拨通，没有声音？\u003c/p\u003e\n\u003cp\u003e一般是 NAT 造成，配置这三个参数：rtp_symmetric，force_rport，rewrite_contact。\u003c/p\u003e\n\u003cp\u003eSIP 客户端没有自动挂机？\u003c/p\u003e\n\u003cp\u003e一般是没有设置 stun 造成的，在 SIP 客户端，设置 stun 即可。如果没有 stun server，可以设置这个\u003ccode\u003estun.l.google.com:19302\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eAGI 脚本返回状态 4，正常应该为 0？\u003c/p\u003e\n\u003cp\u003e查看网上资料，是 AGI 脚本中调用 Hangup 导致，将脚本中的 Hangup 去掉，放在拨号计划配置文件中执行 Hangup，可以解决这个问题。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eAGI Script agidemo completed, returning 0\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e查看客户端是否在线？\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epjsip show endpoints\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e要求最大一个客户端在线，请在aors配置中使用配置\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003emax_contacts 设置为 1\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eMicroSIP客户端配置后还显示IDLE？\u003c/p\u003e\n\u003cp\u003eSIP SERVER字段必须填写，和域名地址保持一致。这样状态IDLE会变为ONline。\u003c/p\u003e\n\u003cp\u003eMicroSIP客户端连接后，Asterisk不显示在线？\u003c/p\u003e\n\u003cp\u003e如果是服务器在公网，客户端在局域网的网络结构：\u003cbr\u003e\n我的Asterisk显示的客户端Contact是局域网地址，针对这种情况。\u003cbr\u003e\n1，临时解决方法：请在账户配置中，勾选IP REWRITE或ICE，具体选择哪个看服务端的配置。他们分别表示IP地址重写和启用更智能的 NAT 穿透技术。如果需要向好友显示在线状态，请勾选Publish Presence。\u003cbr\u003e\n2，更推荐的解决方法：检查Asterisk的PJSIP配置，需要添加 \u003ccode\u003erewrite_contact=yes\u003c/code\u003e这是解决 NAT 的“银弹”。它告诉 Asterisk：“忽略报文里写的内网 IP，直接回复给数据包发过来的那个公网地址和端口。”\u003cbr\u003e\n设置 \u003ccode\u003ertp_symmetric=yes\u003c/code\u003e：确保语音流（RTP）也走同样的路径，防止通话时单通（没声音）。\u003c/p\u003e","title":"语音验证码实现中常见错误"},{"content":"音频文件处理 我们使用手机上的录音机来录制音频文件。\nAndroid 录音机录制的音频文件格式为 mp3，如果是 amr 格式，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。\nIOS 录音机录制的音频文件格式为 m4a，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。\n我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能后续会集成到豆子工具中。\nmp3 转 g711a 命令：\nffmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw 使用 ffplay 播放测试\nffplay -i test.alaw -f alaw -ac 1 -ar 8000 将制作好的音频文件存放在 asterisk sounds 目录，就可以在拨号计划中使用 Playback 应用调用它了。\n配置实时数据库 今天讲解 Asterisk 如何实时将 SIP 用户写入 sqlite3 数据库。\n先定义数据库表结构，我当前使用的 PJSIP 协议。 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 (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), connected_line_method varchar(10) check(connected_line_method in (\u0026#39;invite\u0026#39;,\u0026#39;reinvite\u0026#39;,\u0026#39;update\u0026#39;)), direct_media_method varchar(10) check(direct_media_method in (\u0026#39;invite\u0026#39;,\u0026#39;reinvite\u0026#39;,\u0026#39;update\u0026#39;)), direct_media_glare_mitigation varchar(20) check(direct_media_glare_mitigation in (\u0026#39;none\u0026#39;,\u0026#39;outgoing\u0026#39;,\u0026#39;incoming\u0026#39;)), disable_direct_media_on_nat varchar(5) check(disable_direct_media_on_nat in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), dtmf_mode varchar(20) check(dtmf_mode in (\u0026#39;rfc4733\u0026#39;,\u0026#39;inband\u0026#39;,\u0026#39;info\u0026#39;)), external_media_address VARCHAR(40), force_rport varchar(5) check(force_rport in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), ice_support varchar(5) check(ice_support in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), identify_by varchar(10) check(identify_by in (\u0026#39;username\u0026#39;)), mailboxes VARCHAR(40), moh_suggest VARCHAR(40), outbound_auth VARCHAR(40), outbound_proxy VARCHAR(40), rewrite_contact varchar(5) check(rewrite_contact in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), rtp_ipv6 varchar(5) check(rtp_ipv6 in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), rtp_symmetric varchar(5) check(rtp_symmetric in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), send_diversion varchar(5) check(send_diversion in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), send_pai varchar(5) check(send_pai in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), send_rpid varchar(5) check(send_rpid in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), timers_min_se INTEGER, timers varchar(20) check(timers in (\u0026#39;forced\u0026#39;,\u0026#39;no\u0026#39;,\u0026#39;required\u0026#39;,\u0026#39;yes\u0026#39;)), timers_sess_expires INTEGER, callerid VARCHAR(40), callerid_privacy varchar(40) check(callerid_privacy in (\u0026#39;allowed_not_screened\u0026#39;,\u0026#39;allowed_passed_screened\u0026#39;,\u0026#39;allowed_failed_screened\u0026#39;,\u0026#39;allowed\u0026#39;,\u0026#39;prohib_not_screened\u0026#39;,\u0026#39;prohib_passed_screened\u0026#39;,\u0026#39;prohib_failed_screened\u0026#39;,\u0026#39;prohib\u0026#39;,\u0026#39;unavailable\u0026#39;)), callerid_tag VARCHAR(40), `100rel` varchar(20) check(`100rel` in (\u0026#39;no\u0026#39;,\u0026#39;required\u0026#39;,\u0026#39;yes\u0026#39;)), aggregate_mwi varchar(5) check(aggregate_mwi in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), trust_id_inbound varchar(5) check(trust_id_inbound in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), trust_id_outbound varchar(5) check(trust_id_outbound in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), use_ptime varchar(5) check(use_ptime in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), use_avpf varchar(5) check(use_avpf in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), media_encryption varchar(10) check(media_encryption in (\u0026#39;no\u0026#39;,\u0026#39;sdes\u0026#39;,\u0026#39;dtls\u0026#39;)), inband_progress varchar(5) check(inband_progress in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#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 (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), t38_udptl varchar(5) check(t38_udptl in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), t38_udptl_ec varchar(20) check(t38_udptl_ec in (\u0026#39;none\u0026#39;,\u0026#39;fec\u0026#39;,\u0026#39;redundancy\u0026#39;)), t38_udptl_maxdatagram INTEGER, t38_udptl_nat varchar(5) check(t38_udptl_nat in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), t38_udptl_ipv6 varchar(5) check(t38_udptl_ipv6 in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), tone_zone VARCHAR(40), language VARCHAR(40), one_touch_recording varchar(5) check(one_touch_recording in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), record_on_feature VARCHAR(40), record_off_feature VARCHAR(40), rtp_engine VARCHAR(40), allow_transfer varchar(5) check(allow_transfer in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), allow_subscribe varchar(5) check(allow_subscribe in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#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 (\u0026#39;active\u0026#39;,\u0026#39;passive\u0026#39;,\u0026#39;actpass\u0026#39;)), srtp_tag_32 varchar(5) check(srtp_tag_32 in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#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 (\u0026#39;md5\u0026#39;,\u0026#39;userpass\u0026#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 (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), qualify_frequency INTEGER, authenticate_qualify varchar(5) check(authenticate_qualify in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), UNIQUE (id) ); CREATE INDEX ps_aors_id ON ps_aors (id); PJSIP 测试数据：\ninsert into ps_endpoints(id,context,disallow,allow,auth,aors) values (\u0026#39;9000\u0026#39;,\u0026#39;vcode\u0026#39;,\u0026#39;all\u0026#39;,\u0026#39;ulaw,alaw,gsm\u0026#39;,\u0026#39;9000\u0026#39;,\u0026#39;9000\u0026#39;); insert into ps_aors(id,max_contacts) values(\u0026#39;9000\u0026#39;,1); insert into ps_auths(id,auth_type,password,username) values(\u0026#39;9000\u0026#39;,\u0026#39;userpass\u0026#39;,\u0026#39;123321\u0026#39;,\u0026#39;9000\u0026#39;); 注意：数据库表中的字段和配置文件中的字段是一致的。\n配置 Asterisk 在 res_config_sqlite3.conf 中添加如下内容：\n[asterisk] dbfile =\u0026gt; /var/lib/asterisk/realtime.sqlite3 在 extconfig.conf 文件中添加如下内容：\nps_endpoints =\u0026gt; sqlite3,asterisk ps_auths =\u0026gt; sqlite3,asterisk ps_aors =\u0026gt; sqlite3,asterisk 在 sorcery.conf 文件中添加如下内容：\n[res_pjsip] endpoint=realtime,ps_endpoints auth=realtime,ps_auths aor=realtime,ps_aors 使用 sqlite3 命令创建 realtime.sqlite3 数据库，并创建上面的三张 PJSIP 表，然后插入记录。\n最后在软电话上测试是否可以注册成功。\n虽然，我使用的 sqlite3 存储 sip 用户信息，但我推荐你使用 mysql 或者 postgresql。\n它的好处是可以对接第三方应用时，实时添加操作数据库，实时添加用户。而 sqlite3 会报错，提示数据库已锁定。\n虽然我通过同步数据库文件解决，但它不是一个实时的方案。它在并发操作这块还是有些欠缺。\n如果您有什么问题，欢迎关注公众号【技术源泉】私信我。\n以下是备忘录，在 Asterisk 新版本中已不推荐使用。\n如果使用的 SIP，请使用如下方法：\n1，调整配置文件\n修改 extconfig.conf，添加如下内容：\nsippeers=\u0026gt;sqlite,general,sippeers 第一个 sippeers 是固定名称，最后一个 sippeers 是表名。\n2，在数据库中添加表\n例如，数据文件存储在/var/lib/asterisk/realtime.sqlite3\n这个是 SIP 用户\nCREATE TABLE sippeers ( id INTEGER NOT NULL primary key autoincrement, name VARCHAR(40) NOT NULL, ipaddr VARCHAR(45), port INTEGER, regseconds INTEGER, defaultuser VARCHAR(40), fullcontact VARCHAR(80), regserver VARCHAR(20), useragent VARCHAR(20), lastms INTEGER, host VARCHAR(40), type varchar(10) check(type in (\u0026#39;friend\u0026#39;,\u0026#39;user\u0026#39;,\u0026#39;peer\u0026#39;)), context VARCHAR(40), permit VARCHAR(95), deny VARCHAR(95), secret VARCHAR(40), md5secret VARCHAR(40), remotesecret VARCHAR(40), transport varchar(20) check(transport in (\u0026#39;udp\u0026#39;,\u0026#39;tcp\u0026#39;,\u0026#39;tls\u0026#39;,\u0026#39;ws\u0026#39;,\u0026#39;wss\u0026#39;,\u0026#39;udp,tcp\u0026#39;,\u0026#39;tcp,udp\u0026#39;)), dtmfmode varchar(20) check(dtmfmode in (\u0026#39;rfc2833\u0026#39;,\u0026#39;info\u0026#39;,\u0026#39;shortinfo\u0026#39;,\u0026#39;inband\u0026#39;,\u0026#39;auto\u0026#39;)), directmedia varchar(20) check(directmedia in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;,\u0026#39;nonat\u0026#39;,\u0026#39;update\u0026#39;)), nat VARCHAR(29), callgroup VARCHAR(40), pickupgroup VARCHAR(40), language VARCHAR(40), disallow VARCHAR(200), allow VARCHAR(200), insecure VARCHAR(40), trustrpid varchar(10) check(trustrpid in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), progressinband varchar(10) check(progressinband in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;,\u0026#39;never\u0026#39;)), promiscredir varchar(5) check(promiscredir in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), useclientcode varchar(5) check(useclientcode in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), accountcode VARCHAR(40), setvar VARCHAR(200), callerid VARCHAR(40), amaflags VARCHAR(40), callcounter varchar(5) check(callcounter in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), busylevel INTEGER, allowoverlap varchar(5) check(allowoverlap in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), allowsubscribe varchar(5) check(allowsubscribe in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), videosupport varchar(5) check(videosupport in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), maxcallbitrate INTEGER, rfc2833compensate varchar(5) check(rfc2833compensate in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), mailbox VARCHAR(40), `session-timers` varchar(20) check(`session-timers` in (\u0026#39;accept\u0026#39;,\u0026#39;refuse\u0026#39;,\u0026#39;originate\u0026#39;)), `session-expires` INTEGER, `session-minse` INTEGER, `session-refresher` varchar(5) check(`session-refresher` in (\u0026#39;uac\u0026#39;,\u0026#39;uas\u0026#39;)), t38pt_usertpsource VARCHAR(40), regexten VARCHAR(40), fromdomain VARCHAR(40), fromuser VARCHAR(40), `qualify` VARCHAR(40), defaultip VARCHAR(45), rtptimeout INTEGER, rtpholdtimeout INTEGER, sendrpid varchar(5) check(sendrpid in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), outboundproxy VARCHAR(40), callbackextension VARCHAR(40), timert1 INTEGER, timerb INTEGER, qualifyfreq INTEGER, constantssrc varchar(5) check(constantssrc in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), contactpermit VARCHAR(95), contactdeny VARCHAR(95), usereqphone varchar(5) check(usereqphone in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), textsupport varchar(5) check(textsupport in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), faxdetect varchar(5) check(faxdetect in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), buggymwi varchar(5) check(buggymwi in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), auth VARCHAR(40), fullname VARCHAR(40), trunkname VARCHAR(40), cid_number VARCHAR(40), callingpres varchar(50) check(callingpres in (\u0026#39;allowed_not_screened\u0026#39;,\u0026#39;allowed_passed_screen\u0026#39;,\u0026#39;allowed_failed_screen\u0026#39;,\u0026#39;allowed\u0026#39;,\u0026#39;prohib_not_screened\u0026#39;,\u0026#39;prohib_passed_screen\u0026#39;,\u0026#39;prohib_failed_screen\u0026#39;,\u0026#39;prohib\u0026#39;)), mohinterpret VARCHAR(40), mohsuggest VARCHAR(40), parkinglot VARCHAR(40), hasvoicemail varchar(5) check(hasvoicemail in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), subscribemwi varchar(5) check(subscribemwi in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), vmexten VARCHAR(40), autoframing varchar(5) check(autoframing in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), rtpkeepalive INTEGER, `call-limit` INTEGER, g726nonstandard varchar(5) check(g726nonstandard in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), ignoresdpversion varchar(5) check(ignoresdpversion in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), allowtransfer varchar(5) check(allowtransfer in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), dynamic varchar(5) check(dynamic in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), path VARCHAR(256), supportpath varchar(5) check(supportpath in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)), UNIQUE (name) ); CREATE INDEX sippeers_name ON sippeers (name); CREATE INDEX sippeers_name_host ON sippeers (name, host); CREATE INDEX sippeers_ipaddr_port ON sippeers (ipaddr, port); CREATE INDEX sippeers_host_port ON sippeers (host, port); Asterisk AGI 调用 Asterisk 支持 AGI 接口，它支持多种语言，并提供多种语言 SDK，例如 Ruby，Java，PHP，Python，C++，.Net，nodejs，Go。\n我使用的 Golang 语言，使用这个github.com/CyCoreSystems/agi AGI 库。\n使用 go 编写的 agi 脚本\npackage main import ( \u0026#34;github.com/CyCoreSystems/agi\u0026#34; ) func main() { a := agi.NewStdio() // 设置通道变量 err := a.Set(\u0026#34;MYVAR\u0026#34;, \u0026#34;foo\u0026#34;) if err != nil { panic(\u0026#34;failed to set variable MYVAR\u0026#34;) } m := a.Variables // 获取AGI参数 callerid, ok := m[\u0026#34;agi_callerid\u0026#34;] if !ok { a.Verbose(\u0026#34;failed to get callerid\u0026#34;, 1) panic(\u0026#34;failed to get callerid\u0026#34;) } // 打印日志 a.Verbose(callerid, 4) // 播放音频文件 a.StreamFile(\u0026#34;bean\u0026#34;, \u0026#34;\u0026#34;, 0) a.StreamFile(\u0026#34;thanks\u0026#34;, \u0026#34;\u0026#34;, 0) } 我们需要将编译之后的脚本文件放在/var/lib/asterisk/agi-bin/目录中。\n并执行chmod +x agidemo添加执行权限。\n","permalink":"https://blog.91demo.top/go/vcode.html","summary":"\u003ch2 id=\"音频文件处理\"\u003e音频文件处理\u003c/h2\u003e\n\u003cp\u003e我们使用手机上的录音机来录制音频文件。\u003cbr\u003e\nAndroid 录音机录制的音频文件格式为 mp3，如果是 amr 格式，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。\u003cbr\u003e\nIOS 录音机录制的音频文件格式为 m4a，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。\u003c/p\u003e\n\u003cp\u003e我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能后续会集成到豆子工具中。\u003c/p\u003e\n\u003cp\u003emp3 转 g711a 命令：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e使用 ffplay 播放测试\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003effplay -i test.alaw -f alaw -ac 1 -ar 8000\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e将制作好的音频文件存放在 asterisk sounds 目录，就可以在拨号计划中使用 Playback 应用调用它了。\u003c/p\u003e\n\u003ch2 id=\"配置实时数据库\"\u003e配置实时数据库\u003c/h2\u003e\n\u003cp\u003e今天讲解 Asterisk 如何实时将 SIP 用户写入 sqlite3 数据库。\u003c/p\u003e\n\u003ch3 id=\"先定义数据库表结构我当前使用的-pjsip-协议\"\u003e先定义数据库表结构，我当前使用的 PJSIP 协议。\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\nCREATE TABLE ps_endpoints (\n    id VARCHAR(40) NOT NULL,\n    transport VARCHAR(40),\n    aors VARCHAR(200),\n    auth VARCHAR(40),\n    context VARCHAR(40),\n    disallow VARCHAR(200),\n    allow VARCHAR(200),\n    direct_media varchar(5) check(direct_media in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    connected_line_method varchar(10) check(connected_line_method in (\u0026#39;invite\u0026#39;,\u0026#39;reinvite\u0026#39;,\u0026#39;update\u0026#39;)),\n    direct_media_method varchar(10) check(direct_media_method in (\u0026#39;invite\u0026#39;,\u0026#39;reinvite\u0026#39;,\u0026#39;update\u0026#39;)),\n    direct_media_glare_mitigation varchar(20) check(direct_media_glare_mitigation in (\u0026#39;none\u0026#39;,\u0026#39;outgoing\u0026#39;,\u0026#39;incoming\u0026#39;)),\n    disable_direct_media_on_nat varchar(5) check(disable_direct_media_on_nat in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    dtmf_mode varchar(20) check(dtmf_mode in (\u0026#39;rfc4733\u0026#39;,\u0026#39;inband\u0026#39;,\u0026#39;info\u0026#39;)),\n    external_media_address VARCHAR(40),\n    force_rport varchar(5) check(force_rport in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    ice_support varchar(5) check(ice_support in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    identify_by varchar(10) check(identify_by in (\u0026#39;username\u0026#39;)),\n    mailboxes VARCHAR(40),\n    moh_suggest VARCHAR(40),\n    outbound_auth VARCHAR(40),\n    outbound_proxy VARCHAR(40),\n    rewrite_contact varchar(5) check(rewrite_contact in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    rtp_ipv6 varchar(5) check(rtp_ipv6 in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    rtp_symmetric varchar(5) check(rtp_symmetric in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    send_diversion varchar(5) check(send_diversion in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    send_pai varchar(5) check(send_pai in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    send_rpid varchar(5) check(send_rpid in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    timers_min_se INTEGER,\n    timers varchar(20) check(timers in (\u0026#39;forced\u0026#39;,\u0026#39;no\u0026#39;,\u0026#39;required\u0026#39;,\u0026#39;yes\u0026#39;)),\n    timers_sess_expires INTEGER,\n    callerid VARCHAR(40),\n    callerid_privacy varchar(40) check(callerid_privacy in (\u0026#39;allowed_not_screened\u0026#39;,\u0026#39;allowed_passed_screened\u0026#39;,\u0026#39;allowed_failed_screened\u0026#39;,\u0026#39;allowed\u0026#39;,\u0026#39;prohib_not_screened\u0026#39;,\u0026#39;prohib_passed_screened\u0026#39;,\u0026#39;prohib_failed_screened\u0026#39;,\u0026#39;prohib\u0026#39;,\u0026#39;unavailable\u0026#39;)),\n    callerid_tag VARCHAR(40),\n    `100rel` varchar(20) check(`100rel` in (\u0026#39;no\u0026#39;,\u0026#39;required\u0026#39;,\u0026#39;yes\u0026#39;)),\n    aggregate_mwi varchar(5) check(aggregate_mwi in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    trust_id_inbound varchar(5) check(trust_id_inbound in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    trust_id_outbound varchar(5) check(trust_id_outbound in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    use_ptime varchar(5) check(use_ptime in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    use_avpf varchar(5) check(use_avpf in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    media_encryption varchar(10) check(media_encryption in (\u0026#39;no\u0026#39;,\u0026#39;sdes\u0026#39;,\u0026#39;dtls\u0026#39;)),\n    inband_progress varchar(5) check(inband_progress in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    call_group VARCHAR(40),\n    pickup_group VARCHAR(40),\n    named_call_group VARCHAR(40),\n    named_pickup_group VARCHAR(40),\n    device_state_busy_at INTEGER,\n    fax_detect varchar(5) check(fax_detect in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    t38_udptl varchar(5) check(t38_udptl in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    t38_udptl_ec varchar(20) check(t38_udptl_ec in (\u0026#39;none\u0026#39;,\u0026#39;fec\u0026#39;,\u0026#39;redundancy\u0026#39;)),\n    t38_udptl_maxdatagram INTEGER,\n    t38_udptl_nat varchar(5) check(t38_udptl_nat in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    t38_udptl_ipv6 varchar(5) check(t38_udptl_ipv6 in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    tone_zone VARCHAR(40),\n    language VARCHAR(40),\n    one_touch_recording varchar(5) check(one_touch_recording in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    record_on_feature VARCHAR(40),\n    record_off_feature VARCHAR(40),\n    rtp_engine VARCHAR(40),\n    allow_transfer varchar(5) check(allow_transfer in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    allow_subscribe varchar(5) check(allow_subscribe in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    sdp_owner VARCHAR(40),\n    sdp_session VARCHAR(40),\n    tos_audio INTEGER,\n    tos_video INTEGER,\n    cos_audio INTEGER,\n    cos_video INTEGER,\n    sub_min_expiry INTEGER,\n    from_domain VARCHAR(40),\n    from_user VARCHAR(40),\n    mwi_fromuser VARCHAR(40),\n    dtls_verify VARCHAR(40),\n    dtls_rekey VARCHAR(40),\n    dtls_cert_file VARCHAR(200),\n    dtls_private_key VARCHAR(200),\n    dtls_cipher VARCHAR(200),\n    dtls_ca_file VARCHAR(200),\n    dtls_ca_path VARCHAR(200),\n    dtls_setup varchar(20) check(dtls_setup in (\u0026#39;active\u0026#39;,\u0026#39;passive\u0026#39;,\u0026#39;actpass\u0026#39;)),\n    srtp_tag_32 varchar(5) check(srtp_tag_32 in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    UNIQUE (id)\n);\n\nCREATE INDEX ps_endpoints_id ON ps_endpoints (id);\n\n\nCREATE TABLE ps_auths (\n    id VARCHAR(40) NOT NULL,\n    auth_type varchar(10) check(auth_type in (\u0026#39;md5\u0026#39;,\u0026#39;userpass\u0026#39;)),\n    nonce_lifetime INTEGER,\n    md5_cred VARCHAR(40),\n    password VARCHAR(80),\n    realm VARCHAR(40),\n    username VARCHAR(40),\n    UNIQUE (id)\n);\n\nCREATE INDEX ps_auths_id ON ps_auths (id);\n\nCREATE TABLE ps_aors (\n    id VARCHAR(40) NOT NULL,\n    contact VARCHAR(40),\n    default_expiration INTEGER,\n    mailboxes VARCHAR(80),\n    max_contacts INTEGER,\n    minimum_expiration INTEGER,\n    remove_existing varchar(5) check(remove_existing in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    qualify_frequency INTEGER,\n    authenticate_qualify varchar(5) check(authenticate_qualify in (\u0026#39;yes\u0026#39;,\u0026#39;no\u0026#39;)),\n    UNIQUE (id)\n);\n\nCREATE INDEX ps_aors_id ON ps_aors (id);\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003ePJSIP 测试数据：\u003c/p\u003e","title":"私有化语音验证码方案：基于 Asterisk 与 Go 的 SIP 通信及 AGI 脚本实战"},{"content":"用户通过 VOIP 电话连接到 Asterisk，拨打固定的数字 8000，Asterisk 将调用 AGI 接口获取验证码，然后将获取到的验证码播报给用户，播报完毕后自动挂断。\n我们根据自助语音验证码原理，将自助验证码实现分解为以下几个技术要点，只要我们解决以下技术要点，就可以实现自助语音验证码。\n技术要点：\n1，如何播放音频文件？音频文件从哪里来？如何播放动态数据？\n2，如何存储用户信息？如何使用数据库存储用户信息？\n3，如何获取验证码？\n下面内容是我对各个技术点的对应解决方案。\n1，Asterisk 自带 Playback 应用，可以通过它播放音频文件。音频文件需要我们提前录制好，并且转换为对应的音频格式，最简单的方法就是使用手机上的录音机。当在拨号计划中多次执行 Playback 应用，Asterisk 会将音频流自动连接起来。所以我们可以使用循环多次执行 Playback 应用即可。\n2，Asterisk 使用配置文件写入 SIP 用户信息，但当写入新的 SIP 用户信息后，需要重新加载配置文件。为了方便和第三方对接，我们推荐使用数据库。Asterisk 支持数据库，并且可以实时获取用户信息。\n3，Asterisk 支持 AGI 接口，我们可以使用 AGI 获取第三方应用的验证码，获取后和提前录制好的文件结合起来进行播放。\n当掌握了这些技术点后，我们就可以灵活应用到其它解决方案。\n","permalink":"https://blog.91demo.top/wiki/implast.html","summary":"\u003cp\u003e用户通过 VOIP 电话连接到 Asterisk，拨打固定的数字 8000，Asterisk 将调用 AGI 接口获取验证码，然后将获取到的验证码播报给用户，播报完毕后自动挂断。\u003cbr\u003e\n我们根据自助语音验证码原理，将自助验证码实现分解为以下几个技术要点，只要我们解决以下技术要点，就可以实现自助语音验证码。\u003c/p\u003e\n\u003cp\u003e技术要点：\u003c/p\u003e\n\u003cp\u003e1，如何播放音频文件？音频文件从哪里来？如何播放动态数据？\u003c/p\u003e\n\u003cp\u003e2，如何存储用户信息？如何使用数据库存储用户信息？\u003c/p\u003e\n\u003cp\u003e3，如何获取验证码？\u003c/p\u003e\n\u003cp\u003e下面内容是我对各个技术点的对应解决方案。\u003c/p\u003e\n\u003cp\u003e1，Asterisk 自带 Playback 应用，可以通过它播放音频文件。音频文件需要我们提前录制好，并且转换为对应的音频格式，最简单的方法就是使用手机上的录音机。当在拨号计划中多次执行 Playback 应用，Asterisk 会将音频流自动连接起来。所以我们可以使用循环多次执行 Playback 应用即可。\u003c/p\u003e\n\u003cp\u003e2，Asterisk 使用配置文件写入 SIP 用户信息，但当写入新的 SIP 用户信息后，需要重新加载配置文件。为了方便和第三方对接，我们推荐使用数据库。Asterisk 支持数据库，并且可以实时获取用户信息。\u003c/p\u003e\n\u003cp\u003e3，Asterisk 支持 AGI 接口，我们可以使用 AGI 获取第三方应用的验证码，获取后和提前录制好的文件结合起来进行播放。\u003c/p\u003e\n\u003cp\u003e当掌握了这些技术点后，我们就可以灵活应用到其它解决方案。\u003c/p\u003e","title":"如何实现自助语音验证码？"},{"content":"在沉浸于 Wails 3 开发 Mole 客户端的过程中，我不禁想起了我在 2023 年 11 月完成的一个小项目——mp-qrcode-gen。\n虽然现在微信小程序后台已经集成了生成小程序码的功能，但在 2023 年，开发者想要快速、批量或者自定义参数生成小程序码，往往需要自己写脚本。于是，我用 Rust 编写了这款极致轻量的桌面工具。\n为什么在 2023 年做这个工具？ 当时，自己需要根据页面去生成一些小程序码，线下张贴使用。每次使用服务端修改代码感觉非常不便利。于是做了一个这样的生成小程序码的工具。\n当完成后，我还发了视频号进行了推广，发现很多人还下载使用了。其实运营人员和开发者在准备线下物料（如海报）时，通常会面临一个痛点：\n接口调用门槛：微信官方提供的是 API 接口，非技术人员无法直接使用。 参数复杂：小程序码分为“受限”和“不受限”两种，参数限制各异，手动拼凑 URL 极易出错。 批量需求：线下场景往往需要针对不同页面、不同场景值生成大量图片，网页端操作效率低下。 技术选型：Rust + native-windows-gui 在那个时期，我并没有选择 Electron 这种庞然大物，而是选择了Rust，保证了极高的运行效率和内存安全。和native-windows-gui (NWG)，这是一个非常纯粹的 Windows 原生 GUI 库。它不包含浏览器内核，直接调用 Windows API，生成的 .exe 文件体积非常小。选择Rust还有一个原因就是我在学习它，需要一个项目练手。\n核心功能拆解 做的工具界面非常简单直观，配置完 AppID 和 AppSecret 后，就可以使用它生成小程序码了。点击生成按钮后，工具会直接调用微信接口并处理返回的二进制流，将其保存为本地图片文件。无需打开浏览器，所见即所得。\n根据微信官方的接口，我实现了两种模式，分别为受限模式和不受限模式。\n1. 数量受限模式 (API A/B) 特点：支持输入完整的、带有超长 URL 参数的页面路径。 场景：适用于对码量要求不高（总计 10 万个以内），但需要精准携带复杂参数的场景。 2. 数量不受限模式 (API C) 特点：页面路径较短，但支持独立的 Scene（场景值）字段。 场景：这是线下物料最常用的模式，可以无限次生成，通过场景值来区分不同的线下投放点。 开源和思考 虽然几个月后，微信官方后台也逐步完善了类似的功能，但 mp-qrcode-gen 的意义在于：在需求未被完全满足的真空期，个人开发者可以用技术手段快速提供解决方案。\n即便在今天看来，这个工具的交互逻辑依然清晰：\nAppID 鉴权 -\u0026gt; Token 自动管理 -\u0026gt; 参数校验 -\u0026gt; 二进制流转图片。 这款工具目前依然在 Github 上开源。对于想要学习 Rust 桌面端开发 或者 微信 API 调用逻辑 的朋友，这是一个非常简洁的入门参考。\n项目地址：https://github.com/littletow/mp-qrcode-gen ","permalink":"https://blog.91demo.top/rust/mpcode-intro.html","summary":"\u003cp\u003e在沉浸于 Wails 3 开发 Mole 客户端的过程中，我不禁想起了我在 2023 年 11 月完成的一个小项目——\u003cstrong\u003emp-qrcode-gen\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e虽然现在微信小程序后台已经集成了生成小程序码的功能，但在 2023 年，开发者想要快速、批量或者自定义参数生成小程序码，往往需要自己写脚本。于是，我用 Rust 编写了这款极致轻量的桌面工具。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"生成小程序码流程图\" loading=\"lazy\" src=\"/images/mpcode-rs/mpcode-gen.webp\"\u003e\u003c/p\u003e\n\u003ch2 id=\"为什么在-2023-年做这个工具\"\u003e为什么在 2023 年做这个工具？\u003c/h2\u003e\n\u003cp\u003e当时，自己需要根据页面去生成一些小程序码，线下张贴使用。每次使用服务端修改代码感觉非常不便利。于是做了一个这样的生成小程序码的工具。\u003c/p\u003e\n\u003cp\u003e当完成后，我还发了视频号进行了推广，发现很多人还下载使用了。其实运营人员和开发者在准备线下物料（如海报）时，通常会面临一个痛点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e接口调用门槛\u003c/strong\u003e：微信官方提供的是 API 接口，非技术人员无法直接使用。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e参数复杂\u003c/strong\u003e：小程序码分为“受限”和“不受限”两种，参数限制各异，手动拼凑 URL 极易出错。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e批量需求\u003c/strong\u003e：线下场景往往需要针对不同页面、不同场景值生成大量图片，网页端操作效率低下。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"技术选型rust--native-windows-gui\"\u003e技术选型：Rust + native-windows-gui\u003c/h2\u003e\n\u003cp\u003e在那个时期，我并没有选择 Electron 这种庞然大物，而是选择了\u003cstrong\u003eRust\u003c/strong\u003e，保证了极高的运行效率和内存安全。和\u003cstrong\u003enative-windows-gui (NWG)\u003c/strong\u003e，这是一个非常纯粹的 Windows 原生 GUI 库。它不包含浏览器内核，直接调用 Windows API，生成的 \u003ccode\u003e.exe\u003c/code\u003e 文件体积非常小。选择Rust还有一个原因就是我在学习它，需要一个项目练手。\u003c/p\u003e\n\u003ch2 id=\"核心功能拆解\"\u003e核心功能拆解\u003c/h2\u003e\n\u003cp\u003e做的工具界面非常简单直观，配置完 \u003ccode\u003eAppID\u003c/code\u003e 和 \u003ccode\u003eAppSecret\u003c/code\u003e 后，就可以使用它生成小程序码了。点击生成按钮后，工具会直接调用微信接口并处理返回的二进制流，将其保存为本地图片文件。无需打开浏览器，所见即所得。\u003c/p\u003e\n\u003cp\u003e根据微信官方的接口，我实现了两种模式，分别为受限模式和不受限模式。\u003c/p\u003e\n\u003ch3 id=\"1-数量受限模式-api-ab\"\u003e1. 数量受限模式 (API A/B)\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e特点\u003c/strong\u003e：支持输入完整的、带有超长 URL 参数的页面路径。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e场景\u003c/strong\u003e：适用于对码量要求不高（总计 10 万个以内），但需要精准携带复杂参数的场景。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-数量不受限模式-api-c\"\u003e2. 数量不受限模式 (API C)\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e特点\u003c/strong\u003e：页面路径较短，但支持独立的 \u003ccode\u003eScene\u003c/code\u003e（场景值）字段。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e场景\u003c/strong\u003e：这是线下物料最常用的模式，可以无限次生成，通过场景值来区分不同的线下投放点。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"开源和思考\"\u003e开源和思考\u003c/h2\u003e\n\u003cp\u003e虽然几个月后，微信官方后台也逐步完善了类似的功能，但 \u003cstrong\u003emp-qrcode-gen\u003c/strong\u003e 的意义在于：\u003cstrong\u003e在需求未被完全满足的真空期，个人开发者可以用技术手段快速提供解决方案。\u003c/strong\u003e\u003c/p\u003e","title":"使用Rust基于 Native-Windows-GUI 构建的轻量小程序码生成工具"},{"content":"在自学 Rust 的那段日子里，我除了开发开源项目，还为日常工作量身定制了一些不公开的内部工具。其中最令我印象深刻的，是一个看似简单的 APK 文件上传小工具。\n虽然它只是一个 Rust + native-windows-gui (NWG) 编写的单一界面应用，但它解决的两个核心生产问题，至今仍对我有着深远的影响。\n工具画像：极简与高效 这个工具的界面极其克制：\n交互区：一个醒目的文件拖拽控件，文案提示“请把 APK 文件拖拽到这里”。 表单区：版本号、Version Code、更新描述、是否强制升级（开关）。 执行区：一个大大的“上传”按钮。 它的核心逻辑非常纯粹：校验必填项 -\u0026gt; 调用内部上传 API -\u0026gt; 上传文件 -\u0026gt; 返回成功或失败的对话框。\n故事一：消失的 60 秒与隐藏的超时限制 工具上线初期，一切运行平稳。由于办公室带宽充足，APK 的上传时间通常维持在 40 秒左右。\n事故发生：\nAPK 在集成广告后，经常会发生失败的情况，客户端弹出“超时”的报错。\n排查过程：\n我首先检查了后端服务，发现后端接口完全没有超时限制，服务器日志显示连接是被客户端主动断开的。 多次重复上传文件使用有线和无线对比，经排查是文件变大，由于公司无线网络波动，上传速度时好时坏，就会偶尔出现失败的情况。对比排查，发现失败的情况上传时间都超过了 60 秒。 回到 Rust 代码中，我发现当时为了追求简洁，直接使用了 HTTP 客户端的默认配置。而这个默认配置的 Timeout 为 60 秒。 解决方案：\n在那个瞬间我意识到，工具不能只考虑“理想状态”。我手动将本地超时时间放宽至 5 分钟，并增加了状态提示。上传时不会再出现超时的情况了。\n教训：永远不要依赖默认的超时设置，尤其是在处理文件上传这类长耗时操作时，本地的容错范围必须根据业务场景精细化配置。 故事二：包名校验——把人为失误挡在门外 这是这个工具最有价值的一次功能进化。\n事故发生：\n有一次，同事不小心将应用 A 的打包产物当作应用 B 上传到了官网，导致用户下载后发现软件张冠李戴。这属于严重的生产事故。\n排查过程：\n这个客户端软件调用内部API后，会自动将上传的文件名修改为固定的软件下载文件名，显示在网站上。经测试，如果将应用A的打包产物使用应用B上传，在官网就会出现相同的情况。解决这个方法也非常简单，就是上传的时候小心细致一点，应用A使用A客户端软件，应用B使用B客户端软件。但是人总会犯错，尤其是在面对多个长相相似的 .apk 文件时。通过肉眼观察文件名来区分应用，是极其不可靠的。\n解决方案：\n所以，我在想是否有能够唯一识别软件而不依赖文件名呢？经沟通可以确定包名是唯一的，所以可以通过包名比对限制上传错误的情况。要获取包名，必须解析APK文件中的XML。所以我在工具中引入了 APK 自动解析功能：\n独立客户端：为每个应用分发独立的上传客户端，这是以前的模式不用更改，仅需要在客户端配置中硬编码该应用正确的 Package Name。 预检机制：当用户拖入 APK 文件后，工具在点击上传前，会先在本地利用 Rust 库读取 APK 内部的 AndroidManifest.xml。 强制比对：将解析出的包名与预设包名对比。一旦发现不一致，立即报错弹窗并锁定上传按钮。 价值：这一小步逻辑改进，彻底杜绝了“传错包”这种人为低级但不能杜绝的失误。 思考：工具的意义不仅是效率，更是兜底 这个 APK 上传工具虽然代码量不大，且因为涉及公司隐私未曾公开，但它教会了我作为一名独立开发者最重要的两个核心理念：\n容错性设计：网络是不可靠的，要有预见性地处理异常。 防御性编程：不要相信人的操作，要用代码为业务流程设置“防呆开关”。 这也是为什么我后来在开发 Mole (frp 客户端) 时，如此注重逻辑闭环和状态校验的原因。每一个看似不起眼的练手工具，都是在为未来的稳定系统积累血泪经验。\n","permalink":"https://blog.91demo.top/rust/apkup-rs.html","summary":"\u003cp\u003e在自学 Rust 的那段日子里，我除了开发开源项目，还为日常工作量身定制了一些不公开的内部工具。其中最令我印象深刻的，是一个看似简单的 \u003cstrong\u003eAPK 文件上传小工具\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e虽然它只是一个 Rust + \u003ccode\u003enative-windows-gui\u003c/code\u003e (NWG) 编写的单一界面应用，但它解决的两个核心生产问题，至今仍对我有着深远的影响。\u003c/p\u003e\n\u003ch2 id=\"工具画像极简与高效\"\u003e工具画像：极简与高效\u003c/h2\u003e\n\u003cp\u003e这个工具的界面极其克制：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e交互区\u003c/strong\u003e：一个醒目的文件拖拽控件，文案提示“请把 APK 文件拖拽到这里”。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e表单区\u003c/strong\u003e：版本号、Version Code、更新描述、是否强制升级（开关）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e执行区\u003c/strong\u003e：一个大大的“上传”按钮。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e它的核心逻辑非常纯粹：校验必填项 -\u0026gt; 调用内部上传 API -\u0026gt; 上传文件 -\u0026gt; 返回成功或失败的对话框。\u003c/p\u003e\n\u003ch2 id=\"故事一消失的-60-秒与隐藏的超时限制\"\u003e故事一：消失的 60 秒与隐藏的超时限制\u003c/h2\u003e\n\u003cp\u003e工具上线初期，一切运行平稳。由于办公室带宽充足，APK 的上传时间通常维持在 40 秒左右。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e事故发生\u003c/strong\u003e：\u003cbr\u003e\nAPK 在集成广告后，经常会发生失败的情况，客户端弹出“超时”的报错。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查过程\u003c/strong\u003e：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e我首先检查了后端服务，发现后端接口完全没有超时限制，服务器日志显示连接是被客户端主动断开的。\u003c/li\u003e\n\u003cli\u003e多次重复上传文件使用有线和无线对比，经排查是文件变大，由于公司无线网络波动，上传速度时好时坏，就会偶尔出现失败的情况。对比排查，发现失败的情况上传时间都超过了 60 秒。\u003c/li\u003e\n\u003cli\u003e回到 Rust 代码中，我发现当时为了追求简洁，直接使用了 HTTP 客户端的默认配置。而这个默认配置的 \u003cstrong\u003eTimeout 为 60 秒\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e解决方案\u003c/strong\u003e：\u003cbr\u003e\n在那个瞬间我意识到，工具不能只考虑“理想状态”。我手动将本地超时时间放宽至 5 分钟，并增加了状态提示。上传时不会再出现超时的情况了。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e教训\u003c/strong\u003e：\u003cstrong\u003e永远不要依赖默认的超时设置\u003c/strong\u003e，尤其是在处理文件上传这类长耗时操作时，本地的容错范围必须根据业务场景精细化配置。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"故事二包名校验把人为失误挡在门外\"\u003e故事二：包名校验——把人为失误挡在门外\u003c/h2\u003e\n\u003cp\u003e这是这个工具最有价值的一次功能进化。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e事故发生\u003c/strong\u003e：\u003cbr\u003e\n有一次，同事不小心将应用 A 的打包产物当作应用 B 上传到了官网，导致用户下载后发现软件张冠李戴。这属于严重的生产事故。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e排查过程\u003c/strong\u003e：\u003cbr\u003e\n这个客户端软件调用内部API后，会自动将上传的文件名修改为固定的软件下载文件名，显示在网站上。经测试，如果将应用A的打包产物使用应用B上传，在官网就会出现相同的情况。解决这个方法也非常简单，就是上传的时候小心细致一点，应用A使用A客户端软件，应用B使用B客户端软件。但是人总会犯错，尤其是在面对多个长相相似的 \u003ccode\u003e.apk\u003c/code\u003e 文件时。通过肉眼观察文件名来区分应用，是极其不可靠的。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e解决方案\u003c/strong\u003e：\u003cbr\u003e\n所以，我在想是否有能够唯一识别软件而不依赖文件名呢？经沟通可以确定包名是唯一的，所以可以通过包名比对限制上传错误的情况。要获取包名，必须解析APK文件中的XML。所以我在工具中引入了 \u003cstrong\u003eAPK 自动解析功能\u003c/strong\u003e：\u003c/p\u003e","title":"构建高可靠 APK 上传工具，彻底终结网络超时与手误引发的生产事故"},{"content":"启动项目 在学习了微信小程序之后，我使用wxml页面可以做出唐诗的内容了。结合wxss，我可以定义好看的布局。在小程序需要备案时，因为唐诗需要资质，而我个人无法满足这些条件，所以我需要开发新的项目。恰好最近在为我的技术笔记发愁。我希望有一个可以展示我笔记内容的工具。而visit 就是一个用于展示和搜索文章的小程序工具。\n项目需求 我的最初想法非常简单，1，可以更新文章内容而不需要升级小程序。2，需要在小程序端进行展示。3，能够根据标题或者关键字进行搜索文章内容。\n技术选型 小程序使用原生开发 界面 UI 使用 WeUI 内容渲染使用Markdown 知识储备 需要你了解微信小程序开发基础知识，Markdown内容编写，以及towxml库。\n完成首页页面 首页页面非常简单，一个标题描述，用来解释小程序干什么？一个搜索框用来搜索内容，三个快捷按钮用于快速查找内容。\n写首页的时候，需要用到的技术要点：\n搜索框、按钮、列表、文本 界面交互 网络请求、数据存储 在微信开发者工具，app.json 文件中配置首页，配置后会自动生成首页模板页面。\n\u0026#34;pages\u0026#34;: [ \u0026#34;pages/index/index\u0026#34; ], 在 pages/index/index.wxml 文件中，编写首页内容。包含标题，描述，搜索框，以及快捷按钮。\n\u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;page__hd\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;page__title\u0026#34;\u0026gt;使用说明\u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;page__desc\u0026#34;\u0026gt;收集了很多经典的代码片段和库，是学习编程的好工具。可通过关键字查找内容，用于解决开发中遇到的问题。\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;page__bd page__bd_spacing\u0026#34;\u0026gt; \u0026lt;!-- 搜索框--\u0026gt; \u0026lt;view class=\u0026#34;weui-search-bar {{inputShowed ? \u0026#39;weui-search-bar_focusing\u0026#39; : \u0026#39;\u0026#39;}}\u0026#34; id=\u0026#34;searchBar\u0026#34;\u0026gt; \u0026lt;form class=\u0026#34;weui-search-bar__form\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-search-bar__box\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;weui-icon-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; confirm-type=\u0026#34;search\u0026#34; class=\u0026#34;weui-search-bar__input\u0026#34; placeholder=\u0026#34;请输入您要查找的内容\u0026#34; value=\u0026#34;{{inputVal}}\u0026#34; focus=\u0026#34;{{inputShowed}}\u0026#34; bindinput=\u0026#34;inputTyping\u0026#34; bindconfirm=\u0026#34;search\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;label class=\u0026#34;weui-search-bar__label\u0026#34; bindtap=\u0026#34;showInput\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;weui-icon-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;span class=\u0026#34;weui-search-bar__text\u0026#34;\u0026gt;搜索\u0026lt;/span\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;view class=\u0026#34;weui-search-bar__cancel-btn\u0026#34; bindtap=\u0026#34;hideInput\u0026#34;\u0026gt;取消\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 查询快捷按钮--\u0026gt; \u0026lt;view class=\u0026#34;weui-flex\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;btnHot\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;最火\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;btnNew\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;最新\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;btnCold\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;最冷\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!-- 内容--\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; 当使用模拟器预览时，发现首页内容是正确的，但是样式非常丑， 我使用 WeUI 来美化一下，使用weui是因为虽然它不是最美，但它的兼容性和稳定性最好，如果自己写wxss，需要考虑各种机型和设备。\n从 Github 搜索 weui-wxss，下载后，将 weui.xss 中的内容复制到项目中的 app.wxss 中。复制后，这个 weui 样式就在整个项目中生效。如果要某个页面单独调整样式，可以在页面中 wxss 中单独修改样式。\n首页数据交互 在画完页面之后，需要进行互动，小程序中和数据进行交互需要写JS文件。在JS文件中设置数据和方法，可以和页面进行互动。\n根据首页页面实际情况，搜索框，快捷按钮都需要互动。先说搜索框，搜索框需要记录用户输入的内容，当按钮点击搜索，或者回车时，还需要根据用户输入的内容，去后台请求数据。\n在 index.js 文件中，我们添加数据字段：\ndata: { artList: [], inputShowed: false, inputVal: \u0026#34;\u0026#34;, keyword: \u0026#34;\u0026#34;, }, 它们分别表示输入值，关键字，文章内容。还需要添加方法：\nshowInput: function () { this.setData({ inputShowed: true }); }, hideInput: function () { this.setData({ inputVal: \u0026#34;\u0026#34;, inputShowed: false, keyword: \u0026#34;\u0026#34;, artList: [], }); }, inputTyping: function (e) { this.setData({ inputVal: e.detail.value }); }, search(e) { var keyword = this.data.inputVal.toLowerCase() this.setData({ keyword: keyword, op: 1, }) this.searchArt(1, keyword) }, 其中，inputTyping 和 search 方法是记录搜索内容，并向后台请求数据，其它两个方法是为了美化搜索框的效果。\n当后台返回数据后，会赋值给 artList，它是一个数组列表，我们循环读取渲染它的内容。在 index.wxml 中的代码如下：\n\u0026lt;!-- 内容--\u0026gt; \u0026lt;!--循环输出--\u0026gt; \u0026lt;view class=\u0026#34;weui-cells\u0026#34;\u0026gt; \u0026lt;block wx:for=\u0026#34;{{artList}}\u0026#34; wx:key=\u0026#34;uuid\u0026#34; \u0026gt; \u0026lt;view aria-labelledby=\u0026#34;js_cell_l1_bd \u0026#34; aria-describedby=\u0026#34;js_cell_l1_note\u0026#34; class=\u0026#34;weui-cell weui-cell_access\u0026#34; hover-class=\u0026#34;weui-cell_active\u0026#34; bindtap=\u0026#34;jump\u0026#34; data-idx=\u0026#34;{{index}}\u0026#34; data-guid=\u0026#34;{{item.uuid}}\u0026#34; data-stars=\u0026#34;{{item.views}}\u0026#34;\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__bd\u0026#34; id=\u0026#34;js_cell_l1_bd\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;view\u0026gt;{{item.title}}\u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;view class=\u0026#34;weui-cell__ft weui-cell__ft_in-access\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;text wx:if=\u0026#34;{{ item.lockState == 1 }}\u0026#34; id=\u0026#34;js_cell_l1_note\u0026#34; aria-label=\u0026#34;，锁\u0026#34; class=\u0026#34;weui-badge weui-badge_dot\u0026#34;\u0026gt;锁\u0026lt;/text\u0026gt; \u0026lt;text wx:elif=\u0026#34;{{ item.createTime \u0026gt; yestTime }}\u0026#34; id=\u0026#34;js_cell_l1_note\u0026#34; aria-label=\u0026#34;，新\u0026#34; class=\u0026#34;weui-badge weui-badge_dot\u0026#34;\u0026gt;新\u0026lt;/text\u0026gt; \u0026lt;!-- \u0026lt;text wx:elif=\u0026#34;{{ item.updateTime \u0026gt; item.createdTime }}\u0026#34; id=\u0026#34;js_cell_l1_note\u0026#34; aria-label=\u0026#34;，有更新\u0026#34; class=\u0026#34;weui-badge weui-badge_dot\u0026#34;\u0026gt;修\u0026lt;/text\u0026gt; --\u0026gt; \u0026lt;text wx:elif=\u0026#34;{{ item.views \u0026gt; 50 }}\u0026#34; id=\u0026#34;js_cell_l1_note\u0026#34; aria-label=\u0026#34;，火\u0026#34; class=\u0026#34;weui-badge weui-badge_dot\u0026#34;\u0026gt;热\u0026lt;/text\u0026gt; \u0026lt;text wx:elif=\u0026#34;{{ item.views \u0026gt; 500 }}\u0026#34; id=\u0026#34;js_cell_l1_note\u0026#34; aria-label=\u0026#34;，非常火\u0026#34; class=\u0026#34;weui-badge weui-badge_dot\u0026#34;\u0026gt;爆\u0026lt;/text\u0026gt; \u0026lt;text wx:elif=\u0026#34;{{ item.views \u0026gt; 5000 }}\u0026#34; id=\u0026#34;js_cell_l1_note\u0026#34; aria-label=\u0026#34;，超级火\u0026#34; class=\u0026#34;weui-badge weui-badge_dot\u0026#34;\u0026gt;燃\u0026lt;/text\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;/block\u0026gt; \u0026lt;/view\u0026gt; \u0026lt;!--循环输出--\u0026gt; 为了更好的体验，可在列表中根据浏览次数，显示不同的修饰词。让用户更好的了解到那篇文章质量更高，更受欢迎。点击列表后，会跳转到文章详情。通过 jump 方法实现。当在视图上需要更多的参数时，可以使用 data-格式，这样会传给 js 具体值，在请求后台数据章节会用到。\n首页中还有 3 个快捷按钮，分别为最新、最火、最冷。根据文章的浏览量来排序展示。按钮本身有点击属性，当点击按钮时，将根据按钮设置的参数去后台查询对应的数据。这三个按钮都使用同一个查询方法，为了区分三个按钮，可以使用 view 的 id 属性。代码如下：\nbindBtn: function (e) { let btnId = e.target.id; let qtype = 1; switch (btnId) { case \u0026#34;btnHot\u0026#34;: // 按浏览次数倒序排序，再按创建升序 qtype = 1; break; case \u0026#34;btnNew\u0026#34;: // 按创建倒序排序 qtype = 2; break; case \u0026#34;btnCold\u0026#34;: // 按浏览次数升序排序，再按创建升序 qtype = 3; break; default: break; } this.setData({ artList: [], qtype: qtype, op: 2, }) this.getArtList(1, qtype) }, 使用网络请求后台数据 如果只有静态页面，会缺少一些灵气。现在网络无处不在，掌握网络请求，是一项必须的技能。\n小程序 HTTP 网络开发非常简单。小程序官方提供了 HTTP 的三个请求 API，分别是网络请求 request，上传文件 uploadfile，下载文件 downloadfile。\n可使用这三个 API，去实现我们的需求。在使用本地开发者工具开发小程序时，默认不要勾选检验域名，方便我们调试。当上线的时候，我们需要将后台的域名配置到小程序后台域名白名单中。\n核心是请求文章列表。我设计了两个接口，一个接口根据关键字查询文章列表，一个接口根据快接按钮属性查询文章列表。\n1，根据按钮属性查询文章列表代码如下：\n// 根据类型查询 getArtList: function (pageNo, qtype) { const that = this that.loading = true wx.showLoading({ title: \u0026#39;加载中...\u0026#39;, mask: true, }) if (pageNo === 1) { that.setData({ artList: [], }) } httpGet(\u0026#39;/artl\u0026#39;, { \u0026#39;pageNo\u0026#39;: pageNo, \u0026#39;qtype\u0026#39;: qtype, }).then((res) =\u0026gt; { that.loading = false wx.hideLoading() const result = res.data; if (result.code == 1) { const articles = result.data; that.setData({ page: pageNo, //当前的页号 pages: result.count, //总页数 artList: that.data.artList.concat(articles) }) } else { wx.showToast({ title: \u0026#39;没有找到记录\u0026#39;, }) } }).catch((err) =\u0026gt; { that.loading = false wx.hideLoading() wx.showToast({ title: \u0026#39;网络异常请重试\u0026#39;, }) }) }, 2，根据关键字查询文章列表代码如下：\nsearchArt: function (pageNo, keyword) { const that = this that.loading = true wx.showLoading({ title: \u0026#39;加载中...\u0026#39;, mask: true, }) if (pageNo === 1) { that.setData({ artList: [], }) } httpGet(\u0026#39;/sart\u0026#39;, { \u0026#39;pageNo\u0026#39;: pageNo, \u0026#39;keyword\u0026#39;: keyword, }).then((res) =\u0026gt; { that.loading = false wx.hideLoading() const result = res.data; if (result.code == 1) { const articles = result.data; that.setData({ page: pageNo, //当前的页号 pages: result.count, //总页数 artList: that.data.artList.concat(articles) }) } else { wx.showToast({ title: \u0026#39;没有找到记录\u0026#39;, }) } }).catch((err) =\u0026gt; { that.loading = false wx.hideLoading() wx.showToast({ title: \u0026#39;网络异常请重试\u0026#39;, }) }) }, 两个方法大体框架差不多，后台接口名不一样。在这里，我多说两句，这两个接口可以合并成一个接口，有好处也有坏处。好处是减少接口的数量，坏处是减少了灵活性，耦合性太强，修改一个接口内容会影响到另一个接口。所以大家可以根据实际需求来设计。我经历的好多接口都是合并之后，在新增需求后又再次拆开，然后继续优化时会再次合并。\n网络请求这块知识点不多，大多时间都是和后台的调试。这次没有用到文件上传和文件下载。这两个 API 也非常的简单。我的另一个小程序【豆子工具】中有用到，可以看看它的项目。\n当获取到数据后，赋值给 artList，小程序会自动渲染数据到页面。另外两个数据 page 和 pages 是为了分页使用。\nthat.setData({ page: pageNo, //当前的页号 pages: result.count, //总页数 artList: that.data.artList.concat(articles) }) 当数据渲染到首页后，我们就可以看到文章列表，当点击文章列表中的某个文章项时，会跳转到文章详情。这次我们使用小程序解析 Markdown 格式内容。\n解析 Markdown 文件 当我们使用网络请求后台数据后，赋值给 artList，这样首页页面就展示了文章列表。当我们点击文章列表中的某项时，会再次请求后台，获取文章的详情。文章的详情是 Markdown 数据。\n在小程序中，无法直接展示 Markdown 数据，我们需要将 Markdown 转换为 WXML，网上的大神很多，有多款 Markdown 转 WXML 库，我使用的是 towxml 库。我使用Markdown主要是因为更新内容不用升级小程序，这是动态渲染的最大优点。当然它还有其它一些优势。\n1，按照 towxml 库的说明文档，我们将编译后的 towxml 复制到我们的项目中，然后在 app.js 文件中引入这个组件。\nApp({ towxml: require(\u0026#39;./towxml/index\u0026#39;), 2，按照首页的步骤，我们添加文章详情页面。\n\u0026#34;pages/article/index\u0026#34; 在 index.wxml 文件中，我们添加页面布局，这个很简单，显示渲染后的内容即可。\n\u0026lt;!--pages/article/index.wxml--\u0026gt; \u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt; \u0026lt;!--使用towxml--\u0026gt; \u0026lt;towxml nodes=\u0026#34;{{article}}\u0026#34; /\u0026gt; \u0026lt;/view\u0026gt; 3，注意，我们还需要在 index.json 中添加组件引入。\n{ \u0026#34;usingComponents\u0026#34;: { \u0026#34;towxml\u0026#34;: \u0026#34;../../towxml/towxml\u0026#34; } } 4，在 js 文件中，我们需要调用 Markdown 数据。\nonLoad(options) { const _ts = this; wx.showLoading({ title: \u0026#39;加载中\u0026#39;, }) httpGet(\u0026#39;/artd\u0026#39;, { uuid: options.guid, }).then((res) =\u0026gt; { const result = res.data; if (result.code == 1) { let content = result.data; let obj = app.towxml(content, \u0026#39;markdown\u0026#39;, { theme: \u0026#39;light\u0026#39;, events: { tap: (e) =\u0026gt; { console.log(\u0026#39;tap\u0026#39;, e); } } }); _ts.setData({ article: obj, }); wx.hideLoading({ success: (res) =\u0026gt; { }, }) } else { wx.hideLoading() } }).catch((err) =\u0026gt; { console.log(err); wx.hideLoading() wx.showToast({ title: \u0026#39;网络异常请重试\u0026#39;, }) }) }, 我们在 onLoad 中加载函数，这样页面启动就会自动网络请求，然后渲染 Markdown 数据。在请求数据时，我们注意到 guid，这个参数是上一个页面即首页，点击文章列表项带过来的数据。它就是上节中，在 view 中设置 data-属性，可以将页面参数传给 JS。\n首页跳转到详情页方法如下：\njump: function (e) { const guid = e.currentTarget.dataset.guid // 调整到文章页面 wx.navigateTo({ url: \u0026#39;../article/index?guid=\u0026#39; + guid, }) }, 现在，关于文章展示和搜索的功能就完成了。\n","permalink":"https://blog.91demo.top/wiki/v1.html","summary":"\u003ch2 id=\"启动项目\"\u003e启动项目\u003c/h2\u003e\n\u003cp\u003e在学习了微信小程序之后，我使用wxml页面可以做出唐诗的内容了。结合wxss，我可以定义好看的布局。在小程序需要备案时，因为唐诗需要资质，而我个人无法满足这些条件，所以我需要开发新的项目。恰好最近在为我的技术笔记发愁。我希望有一个可以展示我笔记内容的工具。而visit 就是一个用于展示和搜索文章的小程序工具。\u003c/p\u003e\n\u003ch3 id=\"项目需求\"\u003e项目需求\u003c/h3\u003e\n\u003cp\u003e我的最初想法非常简单，1，可以更新文章内容而不需要升级小程序。2，需要在小程序端进行展示。3，能够根据标题或者关键字进行搜索文章内容。\u003c/p\u003e\n\u003ch3 id=\"技术选型\"\u003e技术选型\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e小程序使用原生开发\u003c/li\u003e\n\u003cli\u003e界面 UI 使用 WeUI\u003c/li\u003e\n\u003cli\u003e内容渲染使用Markdown\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"知识储备\"\u003e知识储备\u003c/h3\u003e\n\u003cp\u003e需要你了解微信小程序开发基础知识，Markdown内容编写，以及towxml库。\u003c/p\u003e\n\u003ch2 id=\"完成首页页面\"\u003e完成首页页面\u003c/h2\u003e\n\u003cp\u003e首页页面非常简单，一个标题描述，用来解释小程序干什么？一个搜索框用来搜索内容，三个快捷按钮用于快速查找内容。\u003c/p\u003e\n\u003cp\u003e写首页的时候，需要用到的技术要点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e搜索框、按钮、列表、文本\u003c/li\u003e\n\u003cli\u003e界面交互\u003c/li\u003e\n\u003cli\u003e网络请求、数据存储\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在微信开发者工具，app.json 文件中配置首页，配置后会自动生成首页模板页面。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026#34;pages\u0026#34;: [\n    \u0026#34;pages/index/index\u0026#34;\n  ],\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在 pages/index/index.wxml 文件中，编写首页内容。包含标题，描述，搜索框，以及快捷按钮。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026lt;view class=\u0026#34;page\u0026#34;\u0026gt;\n  \u0026lt;view class=\u0026#34;page__hd\u0026#34;\u0026gt;\n    \u0026lt;view class=\u0026#34;page__title\u0026#34;\u0026gt;使用说明\u0026lt;/view\u0026gt;\n    \u0026lt;view class=\u0026#34;page__desc\u0026#34;\u0026gt;收集了很多经典的代码片段和库，是学习编程的好工具。可通过关键字查找内容，用于解决开发中遇到的问题。\u0026lt;/view\u0026gt;\n  \u0026lt;/view\u0026gt;\n  \u0026lt;view class=\u0026#34;page__bd page__bd_spacing\u0026#34;\u0026gt;\n    \u0026lt;!-- 搜索框--\u0026gt;\n    \u0026lt;view class=\u0026#34;weui-search-bar {{inputShowed ? \u0026#39;weui-search-bar_focusing\u0026#39; : \u0026#39;\u0026#39;}}\u0026#34; id=\u0026#34;searchBar\u0026#34;\u0026gt;\n      \u0026lt;form class=\u0026#34;weui-search-bar__form\u0026#34;\u0026gt;\n        \u0026lt;view class=\u0026#34;weui-search-bar__box\u0026#34;\u0026gt;\n          \u0026lt;i class=\u0026#34;weui-icon-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\n          \u0026lt;input type=\u0026#34;text\u0026#34; confirm-type=\u0026#34;search\u0026#34; class=\u0026#34;weui-search-bar__input\u0026#34; placeholder=\u0026#34;请输入您要查找的内容\u0026#34; value=\u0026#34;{{inputVal}}\u0026#34; focus=\u0026#34;{{inputShowed}}\u0026#34; bindinput=\u0026#34;inputTyping\u0026#34; bindconfirm=\u0026#34;search\u0026#34; /\u0026gt;\n        \u0026lt;/view\u0026gt;\n        \u0026lt;label class=\u0026#34;weui-search-bar__label\u0026#34; bindtap=\u0026#34;showInput\u0026#34;\u0026gt;\n          \u0026lt;i class=\u0026#34;weui-icon-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\n          \u0026lt;span class=\u0026#34;weui-search-bar__text\u0026#34;\u0026gt;搜索\u0026lt;/span\u0026gt;\n        \u0026lt;/label\u0026gt;\n      \u0026lt;/form\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-search-bar__cancel-btn\u0026#34; bindtap=\u0026#34;hideInput\u0026#34;\u0026gt;取消\u0026lt;/view\u0026gt;\n    \u0026lt;/view\u0026gt;\n\n    \u0026lt;!-- 查询快捷按钮--\u0026gt;\n    \u0026lt;view class=\u0026#34;weui-flex\u0026#34;\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt;\n        \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;btnHot\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;最火\u0026lt;/view\u0026gt;\n      \u0026lt;/view\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt;\n        \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;btnNew\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;最新\u0026lt;/view\u0026gt;\n      \u0026lt;/view\u0026gt;\n      \u0026lt;view class=\u0026#34;weui-flex__item\u0026#34;\u0026gt;\n        \u0026lt;view class=\u0026#34;placeholder\u0026#34; id=\u0026#34;btnCold\u0026#34; bindtap=\u0026#34;bindBtn\u0026#34; hover-class=\u0026#34;placeholder-hover\u0026#34;\u0026gt;最冷\u0026lt;/view\u0026gt;\n      \u0026lt;/view\u0026gt;\n    \u0026lt;/view\u0026gt;\n    \u0026lt;!-- 内容--\u0026gt;\n  \u0026lt;/view\u0026gt;\n\u0026lt;/view\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e当使用模拟器预览时，发现首页内容是正确的，但是样式非常丑， 我使用 WeUI 来美化一下，使用weui是因为虽然它不是最美，但它的兼容性和稳定性最好，如果自己写wxss，需要考虑各种机型和设备。\u003c/p\u003e","title":"豆子碎片小程序项目第一版"},{"content":"记录曾经存在的功能，列表形式展示。\n音频转 MP3 可以将 IOS 的录音文件 m4a 格式转换为 mp3 格式，在老设备播放，或者其它特殊应用场景。\n使用方法：搜索豆子工具小程序，点击音频格式转换。\n推荐理由：方便用户在手机上操作，可以转换音频，还可以试听效果。\n获取 AST 账户 AST 账户用于连接 Asterisk，是自助语音验证码重要的一个环节。\n你可以搜索【豆子工具】小程序，获取自己的 AST 账户。\n注意：AST 账户获取后并不是马上生效，它需要到次日方可使用。这是因为它在凌晨会进行一次同步，会同步到 Asterisk 数据库。\n后续我们会讲下如何实现自助语音验证码。\n获取识别码 识别码用于开放接口认证。专属 ID 码为自定义识别码，方便用户记忆。\n目前开放接口为：\n获取本机公网 IP 获取 IP 地址的归属地 你可以搜索【豆子工具】小程序，获取自己的识别码。\n然后使用你喜爱的编程语言调用开放接口。\n开放接口 API 请查看【豆子笔记】网站。\n获取本机局域网 IP 状态：上架\n上架原因：有适用场景，可用于获取手机的局域网 IP，排查问题时使用。\n实现原理：调用小程序的本地获取 IP API。\n获取网络打印机地址 状态：上架\n上架原因：有适用场景，可用于获取局域网的网络打印机 IP 地址。\n实现原理：调用小程序的 mdns API。\n获取验证码 验证码用于访问豆子笔记网站认证使用。它是一次性码，用完即作废，5 分钟内有效。\n豆子笔记网址：www.91demo.top\n除了在豆子工具小程序获取验证码外，还有一个自助语音验证码，使用 Asterisk 实现。我们会在后续的章节进行讲解。\n获取豆子点数 观看激励广告可获得豆子点数。\n豆子点数用于使用小工具。添加豆子点数机制，是为了更好的促进豆子工具良性的发展。\n除了观看激励广告获取豆子点数外，还可以使用九宫格大转盘抽取豆子点数，\n报 IP 地址归属地也可以获取豆子点数奖励。\n如果我的工具对你有帮助，欢迎你浏览收藏，多多的观看广告。\nURL 编解码 假如你碰到这样一个情况，A 用户给你发来了一段日志，而你恰好不方便操作电脑（你在地铁上），\n你在手机上仔细看了日志的内容，你现在知道了问题的大概原因，只需要确认某个 URL 参数是否正确？\n现在这个数据是 URL 编码的，你想知道原始数据，如果你手边有个可以 URL 解码的工具，\n你确认后就可以马上回复对方，解决这个问题。\n你可以搜索【豆子工具】小程序，使用 URL 编解码这个小工具。\n这个小工具除了解码外，还可以编码，例如，用于 Mongodb 的密码。\n九宫格大转盘 状态：下架\n上架原因：有适用场景，可用于展示广告，获取豆子点数。\n实现原理：使用 小程序 画布 API。\n时间戳日期转换 假如你碰到这样一个情况，A 用户给你发来了一段日志，而你恰好不方便操作电脑（你在地铁上），\n你在手机上仔细看了日志的内容，你现在知道了问题的大概原因，只需要确认某个日期是否正确？\n现在这个数据是时间戳，如果你手边有个可以转成日期的工具，\n你确认后就可以马上回复对方，解决这个问题。\n你可以搜索【豆子工具】小程序，使用时间戳计算这个小工具。\n这个小工具可用于排查问题，或时间戳日期转换。\nMD5 哈希加密 假如你碰到这样一个情况，A 用户给你发来了一段日志，而你恰好不方便操作电脑（你在地铁上），\n你在手机上仔细看了日志的内容，你现在知道了问题的大概原因，只需要确认某个数据是否匹配？\n你知道这个数据经过了 MD5 加密，如果你手边有个可以 MD5 加密的工具，\n你确认后就可以马上回复对方，解决这个问题。\n你可以搜索【豆子工具】小程序，使用 MD5 哈希加密小工具。\n这个小工具除了支持 MD5 外，还支持 SHA1,SHA256。可用于排查问题，或加密参数。\n调试本地上传文件服务 状态：上架\n上架原因：有适用场景，可用于本地调试。\n实现原理：使用 小程序 上传文件 API。\n调试本地下载服务 状态：上架\n上架原因：有适用场景，可用于本地调试。\n实现原理：使用 小程序下载文件 API。\n调试本地 UDP 服务 状态：上架\n上架原因：有适用场景，可用于本地调试。\n实现原理：使用 小程序 UDP API。\n调试本地 TCP 服务 状态：上架\n上架原因：有适用场景，可用于本地调试。\n实现原理：使用 小程序 TCP API。\n调试本地 Websocket 服务 状态：上架\n上架原因：有适用场景，可用于本地调试。\n实现原理：使用 小程序 Websocket API。\n调试本地 HTTP 服务 状态：上架\n上架原因：有适用场景，可用于本地调试。\n实现原理：使用 小程序网络请求 API。\n跳转到另一个小程序 状态：上架\n上架原因：有适用场景，可用于我的小程序引流。\n实现原理：使用 button 调用跳转小程序 API。\n","permalink":"https://blog.91demo.top/wiki/wfuncs.html","summary":"\u003cp\u003e记录曾经存在的功能，列表形式展示。\u003c/p\u003e\n\u003ch2 id=\"音频转-mp3\"\u003e音频转 MP3\u003c/h2\u003e\n\u003cp\u003e可以将 IOS 的录音文件 m4a 格式转换为 mp3 格式，在老设备播放，或者其它特殊应用场景。\u003c/p\u003e\n\u003cp\u003e使用方法：搜索豆子工具小程序，点击音频格式转换。\u003c/p\u003e\n\u003cp\u003e推荐理由：方便用户在手机上操作，可以转换音频，还可以试听效果。\u003c/p\u003e\n\u003ch2 id=\"获取-ast-账户\"\u003e获取 AST 账户\u003c/h2\u003e\n\u003cp\u003eAST 账户用于连接 Asterisk，是自助语音验证码重要的一个环节。\u003c/p\u003e\n\u003cp\u003e你可以搜索【豆子工具】小程序，获取自己的 AST 账户。\u003c/p\u003e\n\u003cp\u003e注意：AST 账户获取后并不是马上生效，它需要到次日方可使用。这是因为它在凌晨会进行一次同步，会同步到 Asterisk 数据库。\u003c/p\u003e\n\u003cp\u003e后续我们会讲下如何实现自助语音验证码。\u003c/p\u003e\n\u003ch2 id=\"获取识别码\"\u003e获取识别码\u003c/h2\u003e\n\u003cp\u003e识别码用于开放接口认证。专属 ID 码为自定义识别码，方便用户记忆。\u003c/p\u003e\n\u003cp\u003e目前开放接口为：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e获取本机公网 IP\u003c/li\u003e\n\u003cli\u003e获取 IP 地址的归属地\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e你可以搜索【豆子工具】小程序，获取自己的识别码。\u003c/p\u003e\n\u003cp\u003e然后使用你喜爱的编程语言调用开放接口。\u003c/p\u003e\n\u003cp\u003e开放接口 API 请查看【豆子笔记】网站。\u003c/p\u003e\n\u003ch2 id=\"获取本机局域网-ip\"\u003e获取本机局域网 IP\u003c/h2\u003e\n\u003cp\u003e状态：上架\u003c/p\u003e\n\u003cp\u003e上架原因：有适用场景，可用于获取手机的局域网 IP，排查问题时使用。\u003c/p\u003e\n\u003cp\u003e实现原理：调用小程序的本地获取 IP API。\u003c/p\u003e\n\u003ch2 id=\"获取网络打印机地址\"\u003e获取网络打印机地址\u003c/h2\u003e\n\u003cp\u003e状态：上架\u003c/p\u003e\n\u003cp\u003e上架原因：有适用场景，可用于获取局域网的网络打印机 IP 地址。\u003c/p\u003e\n\u003cp\u003e实现原理：调用小程序的 mdns API。\u003c/p\u003e\n\u003ch2 id=\"获取验证码\"\u003e获取验证码\u003c/h2\u003e\n\u003cp\u003e验证码用于访问豆子笔记网站认证使用。它是一次性码，用完即作废，5 分钟内有效。\u003c/p\u003e\n\u003cp\u003e豆子笔记网址：www.91demo.top\u003c/p\u003e\n\u003cp\u003e除了在豆子工具小程序获取验证码外，还有一个自助语音验证码，使用 Asterisk 实现。我们会在后续的章节进行讲解。\u003c/p\u003e\n\u003ch2 id=\"获取豆子点数\"\u003e获取豆子点数\u003c/h2\u003e\n\u003cp\u003e观看激励广告可获得豆子点数。\u003c/p\u003e\n\u003cp\u003e豆子点数用于使用小工具。添加豆子点数机制，是为了更好的促进豆子工具良性的发展。\u003c/p\u003e\n\u003cp\u003e除了观看激励广告获取豆子点数外，还可以使用九宫格大转盘抽取豆子点数，\u003cbr\u003e\n报 IP 地址归属地也可以获取豆子点数奖励。\u003c/p\u003e","title":"Wander 项目功能列表状态--列表形式"},{"content":"虽然现在的 App 普遍支持手机号或微信一键登录，但对于开发者来说，GitHub、服务器、各类海外服务的登录依然离不开“邮箱+密码”的传统模式。\n我曾经历过一段漫长的“密码管理进化史”：\nExcel 时代：最早用一个加密的 Excel 表格记录，在电脑端还凑合。 Flutter App 时代：为了手机查看方便，我曾写过一个简单的 Flutter App，支持指纹查看，但功能简陋，连“修改”功能都没有。 直到有一次，GitHub 要求我重新登录，由于我频繁重置密码且新旧密码不能重复，导致我彻底记混了。在另一台电脑前尝试了无数次失败后，我意识到：我需要一个足够安全、随时可用、功能完整的个人密码管理工具。\n于是，“豆子工具”里的小程序版密码本诞生了。\n1. 设计核心：绝对的隐私与自由 在设计之初，我就定下了两个原则：不联网、重加密。\n纯离线运行：为了打消用户（包括我自己）对隐私的顾虑，我砍掉了所有联网备份功能。所有的密码数据仅存储在手机本地，备份只能通过聊天文件导出。 三重加密体系：这是我花费心血最多的地方。主密码（Master Password）不存储在设备上。 第一步：用主密码解密 RSA 私钥。 第二步：用私钥解密 AES 密钥。 第三步：用 AES 密钥解密具体的 JSON 序列化记录。\n这种混合加密机制确保了即使手机丢失，只要主密码不泄露，数据依然是安全的。 2. 攻克开发中的“大山” 这个功能的开发过程远比我想象中痛苦。尤其是在小程序环境下处理文件缓存、二进制流转换和加解密逻辑，经常一个 Bug 就要调试好几天。\n中途我好几次想过放弃，觉得用 Excel 也可以凑合。但每次想到反复重置密码的痛苦，还是咬牙坚持了下来。为了稳定性，我将原本不稳定的二进制存储改为了 UTF-8 编码的 JSON 序列化，最终实现了丝滑的操作体验。\n3. 功能完善：不仅仅是“存一下” 相比之前的 Flutter 版本，这次我补全了所有短板：\n增删改查：支持搜索功能，输入标题就能快速定位账号。 备份恢复：加入了完整的备份机制，更换手机时可以轻松迁移数据。 批量操作：支持导入和导出，方便从其他平台平替过来。 密码提示：为了防范“忘记主密码”这个终极灾难，我加入了密码提示功能（建议设一个只有自己懂的暗语）。 4. 字段灵活，满足多样需求 每一条记录都包含了：标题、URL、用户名、密码、备注。\n无论是一个服务器的 SSH 密码，还是一个冷门网站的登录信息，都能井井有条地分类存放。\n总结 这个密码本工具是我最用心、也觉得最实用的作品之一。它没有花哨的云同步，却给了我最踏实的安全感。\n最后再次提醒： 由于是全离线存储，请务必定期通过“导出备份”功能保存你的数据，并牢记你的主密码。\n","permalink":"https://blog.91demo.top/wiki/tool-pwbook.html","summary":"\u003cp\u003e虽然现在的 App 普遍支持手机号或微信一键登录，但对于开发者来说，GitHub、服务器、各类海外服务的登录依然离不开“邮箱+密码”的传统模式。\u003c/p\u003e\n\u003cp\u003e我曾经历过一段漫长的“密码管理进化史”：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eExcel 时代\u003c/strong\u003e：最早用一个加密的 Excel 表格记录，在电脑端还凑合。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFlutter App 时代\u003c/strong\u003e：为了手机查看方便，我曾写过一个简单的 Flutter App，支持指纹查看，但功能简陋，连“修改”功能都没有。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e直到有一次，GitHub 要求我重新登录，由于我频繁重置密码且新旧密码不能重复，导致我彻底记混了。在另一台电脑前尝试了无数次失败后，我意识到：\u003cstrong\u003e我需要一个足够安全、随时可用、功能完整的个人密码管理工具。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e于是，“豆子工具”里的\u003cstrong\u003e小程序版密码本\u003c/strong\u003e诞生了。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"我的密码本\" loading=\"lazy\" src=\"/images/wander/tool-pwbook.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-设计核心绝对的隐私与自由\"\u003e1. 设计核心：绝对的隐私与自由\u003c/h3\u003e\n\u003cp\u003e在设计之初，我就定下了两个原则：\u003cstrong\u003e不联网、重加密。\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e纯离线运行\u003c/strong\u003e：为了打消用户（包括我自己）对隐私的顾虑，我砍掉了所有联网备份功能。所有的密码数据仅存储在手机本地，备份只能通过聊天文件导出。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e三重加密体系\u003c/strong\u003e：这是我花费心血最多的地方。主密码（Master Password）不存储在设备上。\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e第一步\u003c/strong\u003e：用主密码解密 \u003cstrong\u003eRSA 私钥\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e第二步\u003c/strong\u003e：用私钥解密 \u003cstrong\u003eAES 密钥\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e第三步\u003c/strong\u003e：用 AES 密钥解密具体的 \u003cstrong\u003eJSON 序列化记录\u003c/strong\u003e。\u003cbr\u003e\n这种混合加密机制确保了即使手机丢失，只要主密码不泄露，数据依然是安全的。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-攻克开发中的大山\"\u003e2. 攻克开发中的“大山”\u003c/h3\u003e\n\u003cp\u003e这个功能的开发过程远比我想象中痛苦。尤其是在小程序环境下处理\u003cstrong\u003e文件缓存、二进制流转换和加解密逻辑\u003c/strong\u003e，经常一个 Bug 就要调试好几天。\u003c/p\u003e\n\u003cp\u003e中途我好几次想过放弃，觉得用 Excel 也可以凑合。但每次想到反复重置密码的痛苦，还是咬牙坚持了下来。为了稳定性，我将原本不稳定的二进制存储改为了 UTF-8 编码的 JSON 序列化，最终实现了丝滑的操作体验。\u003c/p\u003e\n\u003ch3 id=\"3-功能完善不仅仅是存一下\"\u003e3. 功能完善：不仅仅是“存一下”\u003c/h3\u003e\n\u003cp\u003e相比之前的 Flutter 版本，这次我补全了所有短板：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e增删改查\u003c/strong\u003e：支持搜索功能，输入标题就能快速定位账号。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e备份恢复\u003c/strong\u003e：加入了完整的备份机制，更换手机时可以轻松迁移数据。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e批量操作\u003c/strong\u003e：支持导入和导出，方便从其他平台平替过来。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e密码提示\u003c/strong\u003e：为了防范“忘记主密码”这个终极灾难，我加入了密码提示功能（建议设一个只有自己懂的暗语）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"4-字段灵活满足多样需求\"\u003e4. 字段灵活，满足多样需求\u003c/h3\u003e\n\u003cp\u003e每一条记录都包含了：\u003cstrong\u003e标题、URL、用户名、密码、备注\u003c/strong\u003e。\u003cbr\u003e\n无论是一个服务器的 SSH 密码，还是一个冷门网站的登录信息，都能井井有条地分类存放。\u003c/p\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e这个密码本工具是我最用心、也觉得最实用的作品之一。它没有花哨的云同步，却给了我最踏实的安全感。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e最后再次提醒：\u003c/strong\u003e 由于是全离线存储，请务必定期通过“导出备份”功能保存你的数据，并牢记你的主密码。\u003c/p\u003e","title":"实战笔记：为了管好那堆记不住的密码，我给自己写了个全加密密码本"},{"content":"在运维工作中，有一类事故极其低级却又杀伤力巨大：SSL 证书过期。\n痛点：被动挨打的“救火”模式 由于业务需要，我手里管理着大量客户的域名。每个客户购买证书的渠道各异（阿里云、腾讯云或其他厂商），证书下来后通过微信或邮件发给运维，再手动配置到服务器上。\n这套流程在客户少的时候还算正常。但随着客户增多，问题出现了：证书到期时间不一，全靠人工记忆。\n总会有那么一两次，因为忙碌或者交接疏忽，某个域名证书悄悄过期了。直到客户反馈 APP 无法访问、浏览器弹出红色的安全告警，我们才急忙去“救火”。这种被动的局面不仅影响专业度，也给客户带来了实际损失。\n方案：主动出击的自动化巡检 为了彻底根治这个“心病”，我决定做一个自动化的域名证书检测工具。\n我的设想很简单：变“人找信息”为“信息找人”。\n1. 实现原理 配置简单化：将所有需要监测的域名汇总成一个 txt 文件，每行一个域名，管理起来极其方便。 核心引擎（Go）：使用 Go 语言开发后端服务，利用 cron 库开启定时任务，设定每天固定时间（如凌晨 4 点）执行一次巡检。 检测逻辑：程序自动循环读取域名列表，通过 TLS 握手获取证书的有效载荷，计算当前的剩余天数。 精准预警：我设定了一个“7天阈值”。一旦发现有域名将在 7 天内过期，程序会立即将这些域名汇总。 2. 消息触达：为什么选择机器人？ 在小程序中，邮箱属于敏感隐私资料，审核往往比较严格。为了避开这个麻烦，同时也为了让通知更具实时性，我选择了钉钉机器人和企业微信机器人。\n管理员或运营人员只需将机器人的 Webhook 地址配置好，每天早上一上班，就能在手机上收到一份清晰的到期清单。\n价值：买到了最宝贵的“时间” 这个功能上线后，我们最直接的收获就是**“沟通时间”**：\n提前预判：有了 7 天的缓冲期，运营人员可以气定神闲地与客户沟通续费。 提前操作：运维人员有了充裕的时间更换新证书，彻底杜绝了“半夜修证书”的尴尬。 总结 很多时候，自动化并不是为了追求多么高大上的技术，而是为了把人从那种机械、高风险的记忆工作中解放出来。\n这个小工具的逻辑虽然简单，但它是我内容生态中非常重要的一环。它验证了：只要抓住了痛点，简单的技术组合也能产生巨大的生产力。\n","permalink":"https://blog.91demo.top/wiki/tool-chkdomain.html","summary":"\u003cp\u003e在运维工作中，有一类事故极其低级却又杀伤力巨大：\u003cstrong\u003eSSL 证书过期。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"痛点被动挨打的救火模式\"\u003e痛点：被动挨打的“救火”模式\u003c/h3\u003e\n\u003cp\u003e由于业务需要，我手里管理着大量客户的域名。每个客户购买证书的渠道各异（阿里云、腾讯云或其他厂商），证书下来后通过微信或邮件发给运维，再手动配置到服务器上。\u003c/p\u003e\n\u003cp\u003e这套流程在客户少的时候还算正常。但随着客户增多，问题出现了：\u003cstrong\u003e证书到期时间不一，全靠人工记忆。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e总会有那么一两次，因为忙碌或者交接疏忽，某个域名证书悄悄过期了。直到客户反馈 APP 无法访问、浏览器弹出红色的安全告警，我们才急忙去“救火”。这种被动的局面不仅影响专业度，也给客户带来了实际损失。\u003c/p\u003e\n\u003ch3 id=\"方案主动出击的自动化巡检\"\u003e方案：主动出击的自动化巡检\u003c/h3\u003e\n\u003cp\u003e为了彻底根治这个“心病”，我决定做一个自动化的域名证书检测工具。\u003c/p\u003e\n\u003cp\u003e我的设想很简单：\u003cstrong\u003e变“人找信息”为“信息找人”。\u003c/strong\u003e\u003c/p\u003e\n\u003ch4 id=\"1-实现原理\"\u003e1. 实现原理\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e配置简单化\u003c/strong\u003e：将所有需要监测的域名汇总成一个 \u003ccode\u003etxt\u003c/code\u003e 文件，每行一个域名，管理起来极其方便。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心引擎（Go）\u003c/strong\u003e：使用 Go 语言开发后端服务，利用 \u003ccode\u003ecron\u003c/code\u003e 库开启定时任务，设定每天固定时间（如凌晨 4 点）执行一次巡检。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e检测逻辑\u003c/strong\u003e：程序自动循环读取域名列表，通过 TLS 握手获取证书的有效载荷，计算当前的剩余天数。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e精准预警\u003c/strong\u003e：我设定了一个“7天阈值”。一旦发现有域名将在 7 天内过期，程序会立即将这些域名汇总。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"2-消息触达为什么选择机器人\"\u003e2. 消息触达：为什么选择机器人？\u003c/h4\u003e\n\u003cp\u003e在小程序中，邮箱属于敏感隐私资料，审核往往比较严格。为了避开这个麻烦，同时也为了让通知更具实时性，我选择了\u003cstrong\u003e钉钉机器人\u003c/strong\u003e和\u003cstrong\u003e企业微信机器人\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e管理员或运营人员只需将机器人的 \u003ccode\u003eWebhook\u003c/code\u003e 地址配置好，每天早上一上班，就能在手机上收到一份清晰的到期清单。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"域名证书检测\" loading=\"lazy\" src=\"/images/wander/tool-chkdomain.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"价值买到了最宝贵的时间\"\u003e价值：买到了最宝贵的“时间”\u003c/h3\u003e\n\u003cp\u003e这个功能上线后，我们最直接的收获就是**“沟通时间”**：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e提前预判\u003c/strong\u003e：有了 7 天的缓冲期，运营人员可以气定神闲地与客户沟通续费。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e提前操作\u003c/strong\u003e：运维人员有了充裕的时间更换新证书，彻底杜绝了“半夜修证书”的尴尬。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e很多时候，自动化并不是为了追求多么高大上的技术，而是为了把人从那种机械、高风险的记忆工作中解放出来。\u003c/p\u003e\n\u003cp\u003e这个小工具的逻辑虽然简单，但它是我内容生态中非常重要的一环。它验证了：\u003cstrong\u003e只要抓住了痛点，简单的技术组合也能产生巨大的生产力。\u003c/strong\u003e\u003c/p\u003e","title":"实战笔记：为了不再漏掉任何一个域名到期提醒，我做了个自动化检测工具"},{"content":"在软件发布流程中，最尴尬的事情莫过于：宣传图已经发出去了，用户扫码后却发现链接打不开，或者下载了一个旧版本的安装包。\n我就亲身经历过这么一次“翻车”事故。\n事故现场：一个 URL 引起的麻烦 我曾经碰到这样一个情况，网站上附带有二维码，用于下载安卓 APP。某天，用户反馈扫码下载的安卓 APP 没有新开发的功能。经过排查才发现是二维码的问题。\n使用网站点击下载的用户正常，使用手机扫二维码下载的安卓 APP 没有新功能。原因为扫码下载还是使用的老链接地址，使用的上一个版本。这个时候，测试如果不细心的话，就不会发现这个问题。如果你手边有个可以识别二维码内容的小工具，那么你大概率不会在这种问题上栽跟头。\n虽然这只是一个低级错误，但它让我意识到：在二维码挂载后、正式发布前，必须有一个极简的“内容鉴定”环节。\n痛点：如何快速、无痛地校验？ 通常我们校验二维码，习惯性地直接拿手机扫一下。但如果二维码指向的是一个大容量的 APP 下载包或复杂的跳转链接，扫码后手机会自动触发下载或进入复杂的网页路径。\n我其实并不想真的下载并安装测试（那是后续的 QA 环节），我只是想一眼看到二维码里到底写了什么字符，确定那个 URL 是不是我想要的那一个。\n实现：借力小程序的原生能力 既然发现了痛点，解决起来就非常顺手了。得益于微信生态对扫码的深度支持，我在“豆子工具”里实现了一个“二维码识别”功能：\n核心逻辑：直接调用小程序的 wx.scanCode 接口。 功能表现：不仅支持扫描实物二维码，还支持从手机相册里直接读取生成的二维码图片。 结果反馈：系统不会触发自动跳转，而是将二维码包含的原始文本、URL、一维码内容直接以纯文本的形式展示在屏幕上。 当你碰到陌生的二维码时，忍不住想看，但又担心是病毒时或者有害网站时，不妨先使用该工具扫描二维码。如果二维码内容是网站链接，它不会跳转到网站，只会显示网址，非常安全。\n价值：多看一眼，少出一次错 现在，每当我生成一个新的二维码，无论是用于软件下载、文档分享还是活动跳转，我都会习惯性地用自己的工具“扫一下”：\n确认 URL 参数是否完整。 确认是否有肉眼难以察觉的拼写错误。 确认二维码的类型（一维码还是二维码）是否符合预期。 总结 这个功能的代码实现极其简单，几乎就是调用一个 API 的事。但它的价值在于改变了工作流——在发布前的最后关头，提供了一个低成本的“人工校验点”。\n很多时候，工具的作用不仅仅是自动化，更是为了在我们疏忽大意时，拉我们一把。\n","permalink":"https://blog.91demo.top/wiki/tool-scan.html","summary":"\u003cp\u003e在软件发布流程中，最尴尬的事情莫过于：宣传图已经发出去了，用户扫码后却发现链接打不开，或者下载了一个旧版本的安装包。\u003c/p\u003e\n\u003cp\u003e我就亲身经历过这么一次“翻车”事故。\u003c/p\u003e\n\u003ch3 id=\"事故现场一个-url-引起的麻烦\"\u003e事故现场：一个 URL 引起的麻烦\u003c/h3\u003e\n\u003cp\u003e我曾经碰到这样一个情况，网站上附带有二维码，用于下载安卓 APP。某天，用户反馈扫码下载的安卓 APP 没有新开发的功能。经过排查才发现是二维码的问题。\u003c/p\u003e\n\u003cp\u003e使用网站点击下载的用户正常，使用手机扫二维码下载的安卓 APP 没有新功能。原因为扫码下载还是使用的老链接地址，使用的上一个版本。这个时候，测试如果不细心的话，就不会发现这个问题。如果你手边有个可以识别二维码内容的小工具，那么你大概率不会在这种问题上栽跟头。\u003c/p\u003e\n\u003cp\u003e虽然这只是一个低级错误，但它让我意识到：\u003cstrong\u003e在二维码挂载后、正式发布前，必须有一个极简的“内容鉴定”环节。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"痛点如何快速无痛地校验\"\u003e痛点：如何快速、无痛地校验？\u003c/h3\u003e\n\u003cp\u003e通常我们校验二维码，习惯性地直接拿手机扫一下。但如果二维码指向的是一个大容量的 APP 下载包或复杂的跳转链接，扫码后手机会自动触发下载或进入复杂的网页路径。\u003c/p\u003e\n\u003cp\u003e我其实并不想真的下载并安装测试（那是后续的 QA 环节），我只是想\u003cstrong\u003e一眼看到二维码里到底写了什么字符\u003c/strong\u003e，确定那个 URL 是不是我想要的那一个。\u003c/p\u003e\n\u003ch3 id=\"实现借力小程序的原生能力\"\u003e实现：借力小程序的原生能力\u003c/h3\u003e\n\u003cp\u003e既然发现了痛点，解决起来就非常顺手了。得益于微信生态对扫码的深度支持，我在“豆子工具”里实现了一个“二维码识别”功能：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e核心逻辑\u003c/strong\u003e：直接调用小程序的 \u003ccode\u003ewx.scanCode\u003c/code\u003e 接口。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e功能表现\u003c/strong\u003e：不仅支持扫描实物二维码，还支持从手机相册里直接读取生成的二维码图片。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e结果反馈\u003c/strong\u003e：系统不会触发自动跳转，而是将二维码包含的原始文本、URL、一维码内容直接以\u003cstrong\u003e纯文本\u003c/strong\u003e的形式展示在屏幕上。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e当你碰到陌生的二维码时，忍不住想看，但又担心是病毒时或者有害网站时，不妨先使用该工具扫描二维码。如果二维码内容是网站链接，它不会跳转到网站，只会显示网址，非常安全。\u003c/p\u003e\n\u003ch3 id=\"价值多看一眼少出一次错\"\u003e价值：多看一眼，少出一次错\u003c/h3\u003e\n\u003cp\u003e现在，每当我生成一个新的二维码，无论是用于软件下载、文档分享还是活动跳转，我都会习惯性地用自己的工具“扫一下”：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e确认 URL 参数是否完整。\u003c/li\u003e\n\u003cli\u003e确认是否有肉眼难以察觉的拼写错误。\u003c/li\u003e\n\u003cli\u003e确认二维码的类型（一维码还是二维码）是否符合预期。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"识别二维码\" loading=\"lazy\" src=\"/images/wander/tool-scan.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e这个功能的代码实现极其简单，几乎就是调用一个 API 的事。但它的价值在于\u003cstrong\u003e改变了工作流\u003c/strong\u003e——在发布前的最后关头，提供了一个低成本的“人工校验点”。\u003c/p\u003e\n\u003cp\u003e很多时候，工具的作用不仅仅是自动化，更是为了在我们疏忽大意时，拉我们一把。\u003c/p\u003e","title":"实战笔记：为了不再发错下载链接，我给工具箱加了“扫码鉴定”"},{"content":"我从事于 IT 工作，经常会碰到各种奇葩的问题。其中的一项就是服务无法访问，当碰到这些情况，如何快速的排查问题？如果手边有电脑，那还好办，直接打开电脑找问题即可。如果手边没有电脑，我们怎么快速的定位问题？是服务挂掉了？还是服务器宕机了？\n我的基本思路是这样的，先查看服务器是否宕机，如果服务器没有宕机，那么查看服务是否挂掉？如果服务没有挂掉，那么很大可能是数据造成的问题，或者程序 Bug。\n某次我急需确认服务器上的一个端口是否正常开启，习惯性地打开 Windows 命令行准备敲下 telnet 命令。结果弹出的却是：“telnet 不是内部或外部命令”。这是让我抓狂的一刻。\n由于系统刚重装，这个组件还没勾选。当我从控制面板找到它、安装、甚至可能还要重启系统时，这套复杂的操作让我陷入了反思：为了检测一个端口，至于这么麻烦吗？\n更进一步想，如果我手头没有电脑，只有一部手机，我该如何快速判断服务器防火墙是不是忘了开？\n痛点：环境依赖与生态限制 在实现这个功能之前，我有过两个思考维度：\n环境依赖：无论是 Windows 的 Telnet 还是 Linux 的 nc，都依赖当前操作系统的环境配置。 小程序限制：微信小程序虽然有网络请求能力，但它必须配置服务器域名白名单。如果你想直接用小程序前端去探测一个随机的 IP 或端口，微信的底层安全机制是不允许的。 方案：Go 后端代劳，小程序只做“遥控器” 为了绕过这些限制，我在“豆子工具”里实现了一个远程端口检测功能。它的逻辑非常直接且高效：\n前端交互：用户只需在小程序里输入目标服务器的 IP/域名 和 端口号。 后端核心：数据提交到我用 Go 编写的后端服务。 模拟连接：后端服务代替用户执行 TCP 连接探测。Go 语言的 net.DialTimeout 在这里非常实用，可以精准控制探测时间，避免由于网络超时导致的页面死等。 结果反馈：后端将“连接成功”或“连接失败”的结果返回给小程序展示。 那么如何操作呢？\n1，访问服务器的 SSH 端口，查看是否通畅？如果程序返回开启，那么进行下一步。\n2，访问服务端口，查看是否挂掉？如果程序返回开启，那么进行下一步。\n3，查看其他用户是否这样的情况？如果是，大概率是程序 Bug，如果不是，大概率给这个用户的数据相关。\n价值：随时随地的“运维眼” 自从这个功能上线后，我解决了很多尴尬的场景：\n排查防火墙：在云厂商后台改了安全组策略后，掏出手机点一下，秒知是否生效。 现场交付：在客户现场没有电脑时，快速确认后端服务是否已经拉起。 零部署成本：再也不用担心当前电脑有没有装 Telnet 或 Netcat。 总结 很多时候，工具的意义并不在于它用了多么高深的算法，而在于它是否能在你最需要的时候，以最简单的方式解决那个微小却刺手的痛点。\n这个小功能的背后，其实是 Go 语言并发网络模型的一个微小缩影。\n","permalink":"https://blog.91demo.top/wiki/tool-chkport.html","summary":"\u003cp\u003e我从事于 IT 工作，经常会碰到各种奇葩的问题。其中的一项就是服务无法访问，当碰到这些情况，如何快速的排查问题？如果手边有电脑，那还好办，直接打开电脑找问题即可。如果手边没有电脑，我们怎么快速的定位问题？是服务挂掉了？还是服务器宕机了？\u003c/p\u003e\n\u003cp\u003e我的基本思路是这样的，先查看服务器是否宕机，如果服务器没有宕机，那么查看服务是否挂掉？如果服务没有挂掉，那么很大可能是数据造成的问题，或者程序 Bug。\u003c/p\u003e\n\u003cp\u003e某次我急需确认服务器上的一个端口是否正常开启，习惯性地打开 Windows 命令行准备敲下 \u003ccode\u003etelnet\u003c/code\u003e 命令。结果弹出的却是：“telnet 不是内部或外部命令”。这是让我抓狂的一刻。\u003c/p\u003e\n\u003cp\u003e由于系统刚重装，这个组件还没勾选。当我从控制面板找到它、安装、甚至可能还要重启系统时，这套复杂的操作让我陷入了反思：\u003cstrong\u003e为了检测一个端口，至于这么麻烦吗？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e更进一步想，如果我手头没有电脑，只有一部手机，我该如何快速判断服务器防火墙是不是忘了开？\u003c/p\u003e\n\u003ch3 id=\"痛点环境依赖与生态限制\"\u003e痛点：环境依赖与生态限制\u003c/h3\u003e\n\u003cp\u003e在实现这个功能之前，我有过两个思考维度：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e环境依赖\u003c/strong\u003e：无论是 Windows 的 Telnet 还是 Linux 的 \u003ccode\u003enc\u003c/code\u003e，都依赖当前操作系统的环境配置。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e小程序限制\u003c/strong\u003e：微信小程序虽然有网络请求能力，但它必须配置服务器域名白名单。如果你想直接用小程序前端去探测一个随机的 IP 或端口，微信的底层安全机制是不允许的。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"方案go-后端代劳小程序只做遥控器\"\u003e方案：Go 后端代劳，小程序只做“遥控器”\u003c/h3\u003e\n\u003cp\u003e为了绕过这些限制，我在“豆子工具”里实现了一个\u003cstrong\u003e远程端口检测\u003c/strong\u003e功能。它的逻辑非常直接且高效：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e前端交互\u003c/strong\u003e：用户只需在小程序里输入目标服务器的 \u003cstrong\u003eIP/域名\u003c/strong\u003e 和 \u003cstrong\u003e端口号\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e后端核心\u003c/strong\u003e：数据提交到我用 \u003cstrong\u003eGo\u003c/strong\u003e 编写的后端服务。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e模拟连接\u003c/strong\u003e：后端服务代替用户执行 TCP 连接探测。Go 语言的 \u003ccode\u003enet.DialTimeout\u003c/code\u003e 在这里非常实用，可以精准控制探测时间，避免由于网络超时导致的页面死等。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e结果反馈\u003c/strong\u003e：后端将“连接成功”或“连接失败”的结果返回给小程序展示。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"服务端口检测\" loading=\"lazy\" src=\"/images/wander/tool-chkport.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e那么如何操作呢？\u003c/p\u003e\n\u003cp\u003e1，访问服务器的 SSH 端口，查看是否通畅？如果程序返回开启，那么进行下一步。\u003c/p\u003e\n\u003cp\u003e2，访问服务端口，查看是否挂掉？如果程序返回开启，那么进行下一步。\u003c/p\u003e\n\u003cp\u003e3，查看其他用户是否这样的情况？如果是，大概率是程序 Bug，如果不是，大概率给这个用户的数据相关。\u003c/p\u003e\n\u003ch3 id=\"价值随时随地的运维眼\"\u003e价值：随时随地的“运维眼”\u003c/h3\u003e\n\u003cp\u003e自从这个功能上线后，我解决了很多尴尬的场景：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e排查防火墙\u003c/strong\u003e：在云厂商后台改了安全组策略后，掏出手机点一下，秒知是否生效。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e现场交付\u003c/strong\u003e：在客户现场没有电脑时，快速确认后端服务是否已经拉起。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e零部署成本\u003c/strong\u003e：再也不用担心当前电脑有没有装 Telnet 或 Netcat。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e很多时候，工具的意义并不在于它用了多么高深的算法，而在于它是否能在你最需要的时候，以最简单的方式解决那个微小却刺手的痛点。\u003c/p\u003e\n\u003cp\u003e这个小功能的背后，其实是 \u003cstrong\u003eGo 语言并发网络模型\u003c/strong\u003e的一个微小缩影。\u003c/p\u003e","title":"实战笔记：当我想查端口却没装 Telnet 时，我决定自己写个工具"},{"content":"还记得我之前提到的吗？2018 年，“豆子工具”的初版只有一个功能：点一下按钮，生成一个随机数。\n那时候的代码逻辑极其简单，甚至算不上一个“工具”。但随着这几年自己在开发、运维过程中不断遇到“需要一个复杂密码”、“需要一个 16 位纯金钥”或者“需要一个全大写 ID”等实际场景，我发现简单的随机数其实并不简单。\n痛点：单一随机数的局限 早期的随机数功能非常死板，生成的格式往往不是我想要的。每次生成后，我可能还需要手动去改长度、改大小写，甚至手动补上特殊字符。\n作为一个开发者，如果一个工具不能让我“一键到位”，那就是不合格的。\n进化：高度自定义的“融合模式” 于是，我把这些年所有关于“随机”的需求全部揉碎、重组，将它升级成了一个全能的生成器。\n现在的随机数功能，不再是盲目地随机，而是基于规则的精准生成。我为其设计了一套组合逻辑：\n长度自定义：不再局限于几位数字，用户可以根据需求自由输入长度。 字符集自由组合： 纯数字：适用于验证码类场景。 纯字母：适用于临时 ID 或变量命名。 字母+数字：兼顾强度与易读性。 含特殊字符：专门为高强度随机密码设计。 结果格式化： 支持结果一键转大写或转小写。这在配置某些特定系统的 API Key 时非常有用，省去了手动切换输入法的麻烦。 思考：小功能里的“产品观” 虽然随机数在技术实现上只是简单的字符数组随机采样，但从产品角度看，它体现了一种**“减少用户操作”**的原则。\n把纯数字、字母、符号和格式化选项融合在一起，本质上是把原本需要用户在脑子里构思、在手里修改的过程，变成了一个简单的“勾选”动作。\n以前，我曾把这个功能做的非常复杂，可以生成Excel文件并进行下载，但是当我上线后，我发现这个功能用的人很少，而我自己也没有这样的场景。我用的最多的就是需要一个临时密码，用来分享时会用到。所以就砍掉了这个功能。\n当简化之后，它有几个好处：1，增加了稳定性，因为去掉了导出Excel的功能。2，界面更简单，减少了很多输入的操作，方便使用。\n总结 现在，每当我需要为一个新数据库设置密码，或者为测试环境生成一批随机标识时，我都会习惯性地打开这个功能。\n从 2018 年那个只有一个按钮的“玩具”，到如今能够满足生产环境需求的“工具”，这种一点一滴的打磨，也许正是独立开发者的乐趣所在。\n","permalink":"https://blog.91demo.top/wiki/tool-random.html","summary":"\u003cp\u003e还记得我之前提到的吗？2018 年，“豆子工具”的初版只有一个功能：点一下按钮，生成一个随机数。\u003c/p\u003e\n\u003cp\u003e那时候的代码逻辑极其简单，甚至算不上一个“工具”。但随着这几年自己在开发、运维过程中不断遇到“需要一个复杂密码”、“需要一个 16 位纯金钥”或者“需要一个全大写 ID”等实际场景，我发现\u003cstrong\u003e简单的随机数其实并不简单\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"痛点单一随机数的局限\"\u003e痛点：单一随机数的局限\u003c/h3\u003e\n\u003cp\u003e早期的随机数功能非常死板，生成的格式往往不是我想要的。每次生成后，我可能还需要手动去改长度、改大小写，甚至手动补上特殊字符。\u003c/p\u003e\n\u003cp\u003e作为一个开发者，如果一个工具不能让我“一键到位”，那就是不合格的。\u003c/p\u003e\n\u003ch3 id=\"进化高度自定义的融合模式\"\u003e进化：高度自定义的“融合模式”\u003c/h3\u003e\n\u003cp\u003e于是，我把这些年所有关于“随机”的需求全部揉碎、重组，将它升级成了一个全能的生成器。\u003c/p\u003e\n\u003cp\u003e现在的随机数功能，不再是盲目地随机，而是\u003cstrong\u003e基于规则的精准生成\u003c/strong\u003e。我为其设计了一套组合逻辑：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e长度自定义\u003c/strong\u003e：不再局限于几位数字，用户可以根据需求自由输入长度。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e字符集自由组合\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e纯数字\u003c/strong\u003e：适用于验证码类场景。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e纯字母\u003c/strong\u003e：适用于临时 ID 或变量命名。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e字母+数字\u003c/strong\u003e：兼顾强度与易读性。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e含特殊字符\u003c/strong\u003e：专门为高强度随机密码设计。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e结果格式化\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e支持结果一键\u003cstrong\u003e转大写\u003c/strong\u003e或\u003cstrong\u003e转小写\u003c/strong\u003e。这在配置某些特定系统的 API Key 时非常有用，省去了手动切换输入法的麻烦。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"思考小功能里的产品观\"\u003e思考：小功能里的“产品观”\u003c/h3\u003e\n\u003cp\u003e虽然随机数在技术实现上只是简单的字符数组随机采样，但从产品角度看，它体现了一种**“减少用户操作”**的原则。\u003c/p\u003e\n\u003cp\u003e把纯数字、字母、符号和格式化选项融合在一起，本质上是把原本需要用户在脑子里构思、在手里修改的过程，变成了一个简单的“勾选”动作。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"生成随机数\" loading=\"lazy\" src=\"/images/wander/tool-random.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e以前，我曾把这个功能做的非常复杂，可以生成Excel文件并进行下载，但是当我上线后，我发现这个功能用的人很少，而我自己也没有这样的场景。我用的最多的就是需要一个临时密码，用来分享时会用到。所以就砍掉了这个功能。\u003c/p\u003e\n\u003cp\u003e当简化之后，它有几个好处：1，增加了稳定性，因为去掉了导出Excel的功能。2，界面更简单，减少了很多输入的操作，方便使用。\u003c/p\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e现在，每当我需要为一个新数据库设置密码，或者为测试环境生成一批随机标识时，我都会习惯性地打开这个功能。\u003c/p\u003e\n\u003cp\u003e从 2018 年那个只有一个按钮的“玩具”，到如今能够满足生产环境需求的“工具”，这种一点一滴的打磨，也许正是独立开发者的乐趣所在。\u003c/p\u003e","title":"实战笔记：从一个按钮到全能生成器，随机数功能的“进化论”"},{"content":"在“豆子工具”的所有功能中，局域网调试工具（TCP/UDP/WebSocket） 并不是受众最广的，但它却是我最引以为傲的一个。\n因为它让我真切地体会到了一句话：知识就是力量，知识就是金钱。\n缘起：把调试器揣进口袋 做嵌入式开发或者网络协议调试的人都知道，以往测试局域网通信，必须背着电脑，接上串口线或网线，蹲在机柜旁守着。我当时就在想：既然微信小程序已经开放了网络通信的能力，为什么我不能做一个随身携带的调试器呢？\n于是，我深度调用了小程序的 wx.createTCPSocket、wx.createUDPSocket 和 WebSocket API，在“豆子工具”里构建了一个完整的网络测试模块。它支持：\n十六进制（Hex）与 ASCII 码切换 实时数据日志展示 局域网内稳定的数据收发 变现：从技术分享到“第一桶金” 工具做成后，我并没有藏着掖着，而是把实现原理和核心逻辑整理成文，发布到了微信开发者社区。\n没过多久，一位用户通过那篇文章联系到了我。他在工业自动化领域遇到了一个难题：需要一套能够定制化采集设备数据的微信小程序工具，而协议正是基于 Modbus/TCP。\n由于我的“豆子工具”已经打好了坚实的底层基础，我对原有的 TCP 调试模块进行了针对性的修改，迅速就适配出了满足他实际业务场景的采集方案。\n虽然这笔订单带来的“第一桶金”数额并不算惊人，但它对我的意义非凡。它验证了一个逻辑：当你把一个细分领域的工具做到极致，并愿意分享出去时，价值自然会找上门来。\n思考：底层逻辑的价值 很多开发者纠结于学习各种眼花缭乱的新框架，但其实最耐打的永远是底层逻辑。\n通过开发这个调试工具，我深度钻研了 TCP 的三次握手、UDP 的无连接特性以及 Modbus 这种工业标准协议。正因为有了这些底层的积淀，当机会来临时，我才能在短短几天内就把“调试工具”变成“生产力工具”。\n总结 现在，这个局域网调试模块依然是“豆子工具”里的核心功能。每当我在现场用手机直接调通一台设备时，我都会想起那个联系我的用户。\n知识不仅能改变认知，真的能产生价值。\n","permalink":"https://blog.91demo.top/wiki/tool-tcp.html","summary":"\u003cp\u003e在“豆子工具”的所有功能中，\u003cstrong\u003e局域网调试工具（TCP/UDP/WebSocket）\u003c/strong\u003e 并不是受众最广的，但它却是我最引以为傲的一个。\u003c/p\u003e\n\u003cp\u003e因为它让我真切地体会到了一句话：\u003cstrong\u003e知识就是力量，知识就是金钱。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"缘起把调试器揣进口袋\"\u003e缘起：把调试器揣进口袋\u003c/h3\u003e\n\u003cp\u003e做嵌入式开发或者网络协议调试的人都知道，以往测试局域网通信，必须背着电脑，接上串口线或网线，蹲在机柜旁守着。我当时就在想：既然微信小程序已经开放了网络通信的能力，为什么我不能做一个随身携带的调试器呢？\u003c/p\u003e\n\u003cp\u003e于是，我深度调用了小程序的 \u003ccode\u003ewx.createTCPSocket\u003c/code\u003e、\u003ccode\u003ewx.createUDPSocket\u003c/code\u003e 和 \u003ccode\u003eWebSocket\u003c/code\u003e API，在“豆子工具”里构建了一个完整的网络测试模块。它支持：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e十六进制（Hex）与 ASCII 码切换\u003c/li\u003e\n\u003cli\u003e实时数据日志展示\u003c/li\u003e\n\u003cli\u003e局域网内稳定的数据收发\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"TCP调试工具\" loading=\"lazy\" src=\"/images/wander/tool-tcp.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"变现从技术分享到第一桶金\"\u003e变现：从技术分享到“第一桶金”\u003c/h3\u003e\n\u003cp\u003e工具做成后，我并没有藏着掖着，而是把实现原理和核心逻辑整理成文，发布到了\u003cstrong\u003e微信开发者社区\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e没过多久，一位用户通过那篇文章联系到了我。他在工业自动化领域遇到了一个难题：需要一套能够定制化采集设备数据的微信小程序工具，而协议正是基于 \u003cstrong\u003eModbus/TCP\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e由于我的“豆子工具”已经打好了坚实的底层基础，我对原有的 TCP 调试模块进行了针对性的修改，迅速就适配出了满足他实际业务场景的采集方案。\u003c/p\u003e\n\u003cp\u003e虽然这笔订单带来的“第一桶金”数额并不算惊人，但它对我的意义非凡。它验证了一个逻辑：\u003cstrong\u003e当你把一个细分领域的工具做到极致，并愿意分享出去时，价值自然会找上门来。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"思考底层逻辑的价值\"\u003e思考：底层逻辑的价值\u003c/h3\u003e\n\u003cp\u003e很多开发者纠结于学习各种眼花缭乱的新框架，但其实最耐打的永远是底层逻辑。\u003c/p\u003e\n\u003cp\u003e通过开发这个调试工具，我深度钻研了 TCP 的三次握手、UDP 的无连接特性以及 Modbus 这种工业标准协议。正因为有了这些底层的积淀，当机会来临时，我才能在短短几天内就把“调试工具”变成“生产力工具”。\u003c/p\u003e\n\u003ch3 id=\"总结\"\u003e总结\u003c/h3\u003e\n\u003cp\u003e现在，这个局域网调试模块依然是“豆子工具”里的核心功能。每当我在现场用手机直接调通一台设备时，我都会想起那个联系我的用户。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e知识不仅能改变认知，真的能产生价值。\u003c/strong\u003e\u003c/p\u003e","title":"实战笔记：知识真的就是金钱，聊聊我的局域网调试工具"},{"content":"在“流量贵如油”的今天，作为一名自己买服务器、撸代码的站长，如何给服务器“减负”是每天都要思考的必修课。\n痛点：博客网站的流量杀手 我有一个运行多年的个人博客。在分析服务器日志时发现，最大的流量开销并不是文字，而是文章里那些高清的图片。\n虽然 JPG 和 PNG 已经很普及，但随着屏幕分辨率越来越高，原图动辄几 MB，对于按带宽付费或者流量计费的服务器来说，这都是白花花的银子。\n方案：Webp 格式的“降维打击” WebP是一种现代图片格式，由Google开发，主要目的是优化网页加载速度。它在2010年发布，并且可以提供比传统JPEG、PNG和GIF格式更高效的压缩和更好的图像质量。\n它的特性如下：\n高压缩效率：无损压缩情况下，WebP无损压缩后的图片文件大小通常比PNG小20-30%，同时可以完整地恢复原始图像数据。有损压缩情况下，WebP有损压缩后的图片质量与JPEG相似，但文件大小可以缩减25-34%，从而能在不牺牲太多图像质量的情况下减小文件体积。\n支持透明度：WebP格式支持8位透明度通道（Alpha Channel），这允许图片在有损压缩的同时也能够处理透明背景，这是JPEG格式不具备的能力。\n动画支持：WebP格式可以存储动画，功能类似于GIF，但是以更小的文件体积和更好的压缩效率。\n广泛的颜色表示：WebP支持丰富的颜色表示方式，包括4:2:0和4:4:4色度采样。\n兼容性和支持：WebP格式得到了许多现代浏览器如Chrome、Firefox、Edge等的支持。对于不支持WebP的浏览器，需要使用其他格式的图片作为替代。\n文件扩展名是.webp。在Internet媒体类型（MIME type）中，WebP的类型是image/webp。\n现在主流浏览器（Chrome, Safari, Edge 等）已经全面支持 Webp 格式。相比于传统的 JPG 和 PNG，Webp 可以在保证肉眼看不出画质损失的前提下，将文件体积压缩 30% 到 70%。\n这是一个非常惊人的数据。这意味着原本 100GB 的图床流量，换成 Webp 后可能只需要 30GB。\n为了方便处理博客素材，我决定把这个需求也集成到“豆子工具”里。\n比如我的Hugo网站，在写文章用户图片时，我经常会截屏作为我的图片源，或者拍照作为我的图片源。这个在微信上传时自动转为JPG格式。虽然进行了压缩，但是体积还是有几百KB甚至达到1MB多。\n我的网站流量非常精贵，所以此时使用webp的优势就体现出来了。我会把图片发送到我的电脑，然后使用电脑上的小程序打开进行格式转换。比如我的一个660KB的JPG图片，转换成webp格式后，只有115KB。\n当然，如果你需要在手机上进行格式转换也可以，但是不要使用微信同步到电脑，因为它会进行格式转换，重新转为JPG。\n推荐在电脑上使用电脑版的小程序直接进行图片格式转换，非常方便快捷。\n实现：基于 Google 官方工具链 图片转换的实现原理与我之前的“音频转换”方案异曲同工，主打一个稳定与高效：\n核心引擎：在服务端安装了 Google 官方出品的 webp 命令行工具链（cwebp）。 后端调度：依然由 Go 语言担任“指挥官”，接收小程序上传的 PNG/JPG 原图，调用外部 cwebp 进程进行压缩。 参数优化：在后端我预设了平衡性最好的质量参数，确保图片在压缩后的清晰度依然能够满足博客展示的需求。 闭环应用：现在我写每一篇博客前，都会先用小程序把配图过一遍，转成 Webp 后再上传。 成果：全站 Webp 化 目前，我的博客已经全面实现了图片 Webp 化。这不仅显著提升了网页的加载速度（SEO 评分都变高了），更重要的是，它帮我省下了实打实的流量费用。\n如果你也面临服务器带宽压力，或者想让自己的网站加载得飞快，建议尽早加入 Webp 的阵营。\n下一次，我打算聊聊小程序里那个实用的“网络调试工具”，它是如何帮我搞定局域网协议的。\n","permalink":"https://blog.91demo.top/wiki/image-convert.html","summary":"\u003cp\u003e在“流量贵如油”的今天，作为一名自己买服务器、撸代码的站长，如何给服务器“减负”是每天都要思考的必修课。\u003c/p\u003e\n\u003ch3 id=\"痛点博客网站的流量杀手\"\u003e痛点：博客网站的流量杀手\u003c/h3\u003e\n\u003cp\u003e我有一个运行多年的个人博客。在分析服务器日志时发现，最大的流量开销并不是文字，而是文章里那些高清的图片。\u003c/p\u003e\n\u003cp\u003e虽然 JPG 和 PNG 已经很普及，但随着屏幕分辨率越来越高，原图动辄几 MB，对于按带宽付费或者流量计费的服务器来说，这都是白花花的银子。\u003c/p\u003e\n\u003ch3 id=\"方案webp-格式的降维打击\"\u003e方案：Webp 格式的“降维打击”\u003c/h3\u003e\n\u003cp\u003eWebP是一种现代图片格式，由Google开发，主要目的是优化网页加载速度。它在2010年发布，并且可以提供比传统JPEG、PNG和GIF格式更高效的压缩和更好的图像质量。\u003c/p\u003e\n\u003cp\u003e它的特性如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e高压缩效率：无损压缩情况下，WebP无损压缩后的图片文件大小通常比PNG小20-30%，同时可以完整地恢复原始图像数据。有损压缩情况下，WebP有损压缩后的图片质量与JPEG相似，但文件大小可以缩减25-34%，从而能在不牺牲太多图像质量的情况下减小文件体积。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e支持透明度：WebP格式支持8位透明度通道（Alpha Channel），这允许图片在有损压缩的同时也能够处理透明背景，这是JPEG格式不具备的能力。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e动画支持：WebP格式可以存储动画，功能类似于GIF，但是以更小的文件体积和更好的压缩效率。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e广泛的颜色表示：WebP支持丰富的颜色表示方式，包括4:2:0和4:4:4色度采样。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e兼容性和支持：WebP格式得到了许多现代浏览器如Chrome、Firefox、Edge等的支持。对于不支持WebP的浏览器，需要使用其他格式的图片作为替代。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e文件扩展名是.webp。在Internet媒体类型（MIME type）中，WebP的类型是image/webp。\u003c/p\u003e\n\u003cp\u003e现在主流浏览器（Chrome, Safari, Edge 等）已经全面支持 \u003cstrong\u003eWebp\u003c/strong\u003e 格式。相比于传统的 JPG 和 PNG，Webp 可以在保证肉眼看不出画质损失的前提下，将文件体积\u003cstrong\u003e压缩 30% 到 70%\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e这是一个非常惊人的数据。这意味着原本 100GB 的图床流量，换成 Webp 后可能只需要 30GB。\u003c/p\u003e\n\u003cp\u003e为了方便处理博客素材，我决定把这个需求也集成到“豆子工具”里。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"图片格式转换\" loading=\"lazy\" src=\"/images/wander/image-convert.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e比如我的\u003ca href=\"https://91demo.top\"\u003eHugo网站\u003c/a\u003e，在写文章用户图片时，我经常会截屏作为我的图片源，或者拍照作为我的图片源。这个在微信上传时自动转为JPG格式。虽然进行了压缩，但是体积还是有几百KB甚至达到1MB多。\u003c/p\u003e\n\u003cp\u003e我的网站流量非常精贵，所以此时使用webp的优势就体现出来了。我会把图片发送到我的电脑，然后使用电脑上的小程序打开进行格式转换。比如我的一个660KB的JPG图片，转换成webp格式后，只有115KB。\u003c/p\u003e\n\u003cp\u003e当然，如果你需要在手机上进行格式转换也可以，但是不要使用微信同步到电脑，因为它会进行格式转换，重新转为JPG。\u003c/p\u003e\n\u003cp\u003e推荐在电脑上使用电脑版的小程序直接进行图片格式转换，非常方便快捷。\u003c/p\u003e\n\u003ch3 id=\"实现基于-google-官方工具链\"\u003e实现：基于 Google 官方工具链\u003c/h3\u003e\n\u003cp\u003e图片转换的实现原理与我之前的“音频转换”方案异曲同工，主打一个\u003cstrong\u003e稳定与高效\u003c/strong\u003e：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e核心引擎\u003c/strong\u003e：在服务端安装了 Google 官方出品的 \u003ccode\u003ewebp\u003c/code\u003e 命令行工具链（\u003ccode\u003ecwebp\u003c/code\u003e）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e后端调度\u003c/strong\u003e：依然由 \u003cstrong\u003eGo\u003c/strong\u003e 语言担任“指挥官”，接收小程序上传的 PNG/JPG 原图，调用外部 \u003ccode\u003ecwebp\u003c/code\u003e 进程进行压缩。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e参数优化\u003c/strong\u003e：在后端我预设了平衡性最好的质量参数，确保图片在压缩后的清晰度依然能够满足博客展示的需求。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e闭环应用\u003c/strong\u003e：现在我写每一篇博客前，都会先用小程序把配图过一遍，转成 Webp 后再上传。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"成果全站-webp-化\"\u003e成果：全站 Webp 化\u003c/h3\u003e\n\u003cp\u003e目前，我的博客已经全面实现了图片 Webp 化。这不仅显著提升了网页的加载速度（SEO 评分都变高了），更重要的是，它帮我省下了实打实的流量费用。\u003c/p\u003e","title":"实战笔记：为了省下服务器流量费，我给小程序加上了 Webp 转换"},{"content":"在“豆子工具”众多的功能里，音频转换（m4a 转 mp3） 是我使用频率最高、也最具有“个人救赎”色彩的一个。\n起因：被软件更新“背刺”后的郁闷 这个功能的由来非常接地气：有一段时间，我需要频繁地将苹果手机录音产生的 m4a 格式文件转换成 mp3，因为当时某个必须使用的业务软件只认 mp3。\n那时候我找遍了各种转换工具。最后发现某款主流音乐软件自带的转换功能挺好用。然而，好景不长，在一次软件自动更新后，这个功能竟然被砍掉了。我去搜老版本安装包，却发现根本找不到安全的下载路径。\n那种“被绑架”的无奈感，相信每个工具控都深有体会。\n进阶：大名鼎鼎的 ffmpeg 郁闷之后，我转向了技术人的终极方案——ffmpeg。\n命令行虽然硬核，但确实强大到无以复加。一条简单的指令就能解决所有问题：\nffmpeg -i input.m4a output.mp3\n用了很长一段时间的命令行后，新的问题又来了：我总不能随时随地都带着电脑吧？如果我在外面，急需用手机转一个文件发给客户怎么办？\n于是，我动了把 ffmpeg 搬进“豆子工具”的心思。\n实现：极简架构下的“随时随地” 在小程序里实现这个功能，原理其实并不复杂，核心在于后端调度：\n前端上传：小程序端选择 m4a 文件，上传至服务器。 后端处理：后端使用 Go 语言接收文件，通过 exec 模块调用服务器系统环境中的 ffmpeg 进程进行转换。 实时试听：为了保证体验，我在小程序里集成了一个音频播放器。转换前可以听一下是否选对了文件，转换后也可以即时试听确认效果。 即用即删：转换生成的文件在用户下载后会立即从服务器删除，既保护了隐私，也完全不占用宝贵的服务器存储空间。 感受：工具的本质是“自由” 自从这个功能上线后，我就彻底告别了 ffmpeg 命令行。\n最爽的一点是随时随地：无论是在地铁上还是在户外，掏出手机，几秒钟就能完成格式转换。这种把强大工具揣在兜里的感觉，就是开发者最大的浪漫。\n如果你也经常被各种音频格式限制折磨，或者对 Go 调用 ffmpeg 的具体代码实现感兴趣，欢迎在评论区交流。\n","permalink":"https://blog.91demo.top/wiki/audio-convert.html","summary":"\u003cp\u003e在“豆子工具”众多的功能里，\u003cstrong\u003e音频转换（m4a 转 mp3）\u003c/strong\u003e 是我使用频率最高、也最具有“个人救赎”色彩的一个。\u003c/p\u003e\n\u003ch3 id=\"起因被软件更新背刺后的郁闷\"\u003e起因：被软件更新“背刺”后的郁闷\u003c/h3\u003e\n\u003cp\u003e这个功能的由来非常接地气：有一段时间，我需要频繁地将苹果手机录音产生的 \u003ccode\u003em4a\u003c/code\u003e 格式文件转换成 \u003ccode\u003emp3\u003c/code\u003e，因为当时某个必须使用的业务软件只认 \u003ccode\u003emp3\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e那时候我找遍了各种转换工具。最后发现某款主流音乐软件自带的转换功能挺好用。然而，好景不长，在一次软件自动更新后，这个功能竟然被砍掉了。我去搜老版本安装包，却发现根本找不到安全的下载路径。\u003c/p\u003e\n\u003cp\u003e那种“被绑架”的无奈感，相信每个工具控都深有体会。\u003c/p\u003e\n\u003ch3 id=\"进阶大名鼎鼎的-ffmpeg\"\u003e进阶：大名鼎鼎的 ffmpeg\u003c/h3\u003e\n\u003cp\u003e郁闷之后，我转向了技术人的终极方案——\u003cstrong\u003effmpeg\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e命令行虽然硬核，但确实强大到无以复加。一条简单的指令就能解决所有问题：\u003cbr\u003e\n\u003ccode\u003effmpeg -i input.m4a output.mp3\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e用了很长一段时间的命令行后，新的问题又来了：我总不能随时随地都带着电脑吧？如果我在外面，急需用手机转一个文件发给客户怎么办？\u003c/p\u003e\n\u003cp\u003e于是，我动了把 ffmpeg 搬进“豆子工具”的心思。\u003c/p\u003e\n\u003ch3 id=\"实现极简架构下的随时随地\"\u003e实现：极简架构下的“随时随地”\u003c/h3\u003e\n\u003cp\u003e在小程序里实现这个功能，原理其实并不复杂，核心在于\u003cstrong\u003e后端调度\u003c/strong\u003e：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e前端上传\u003c/strong\u003e：小程序端选择 \u003ccode\u003em4a\u003c/code\u003e 文件，上传至服务器。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e后端处理\u003c/strong\u003e：后端使用 \u003cstrong\u003eGo\u003c/strong\u003e 语言接收文件，通过 \u003ccode\u003eexec\u003c/code\u003e 模块调用服务器系统环境中的 \u003ccode\u003effmpeg\u003c/code\u003e 进程进行转换。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e实时试听\u003c/strong\u003e：为了保证体验，我在小程序里集成了一个音频播放器。转换前可以听一下是否选对了文件，转换后也可以即时试听确认效果。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e即用即删\u003c/strong\u003e：转换生成的文件在用户下载后会立即从服务器删除，既保护了隐私，也完全不占用宝贵的服务器存储空间。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"感受工具的本质是自由\"\u003e感受：工具的本质是“自由”\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"音频格式转换\" loading=\"lazy\" src=\"/images/wander/audio-convert.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e自从这个功能上线后，我就彻底告别了 ffmpeg 命令行。\u003c/p\u003e\n\u003cp\u003e最爽的一点是\u003cstrong\u003e随时随地\u003c/strong\u003e：无论是在地铁上还是在户外，掏出手机，几秒钟就能完成格式转换。这种把强大工具揣在兜里的感觉，就是开发者最大的浪漫。\u003c/p\u003e\n\u003cp\u003e如果你也经常被各种音频格式限制折磨，或者对 Go 调用 ffmpeg 的具体代码实现感兴趣，欢迎在评论区交流。\u003c/p\u003e","title":"实战笔记：我把 FFmpeg 搬进小程序，搞定了音频格式转换"},{"content":"有些事，不做笔记真的意识不到已经过去了这么久。\n最近在整理博客内容时，我翻到了**“豆子工具”小程序**的最早版本记录。那是 2018 年，最初的它简陋得甚至有点滑稽：整个页面只有一个按钮，点一下，生成一个随机数。\n谁能想到，这颗“小豆子”一跑就是八年。\n八年，它长成了我最趁手的“瑞士军刀” 这八年里，我在这款小程序上倾注了太多的时间和精力。它更像是我个人开发者生涯的一个“活化石”，记录了我每一个阶段遇到的问题和想出的方案。\n我始终坚持一个原则：只做真正有用的。 现在的“豆子工具”已经从当年的随机数生成器，进化成了一个覆盖网络、多媒体、开发调试的全能工具箱：\n网络与运维必备：获取 WIFI 公网地址、云厂商安全组管理、查服务端口、域名证书检测。 多媒体处理：音频格式转换（m4a 转 mp3）、图片格式转换（png 转 webp）、二维码识别与生成。 开发调试利器：局域网调试（TCP/UDP/WebSocket）、ESP 网络配置、时间戳转换、Base64 与 URL 编解码。 安全与提醒：私密密码本、事件邮件通知、微信服务通知、语音验证码。 这中间还有很多功能，比如查看上报信息、特定的算法实现等。当然，也有不少功能因为受众太小或者微信审核的边界问题，遗憾地消失在了版本更迭中。\n它是工具，也是我的技术底座 很多人问过我，市面上工具软件那么多，为什么要自己写？\n答案很简单：自由。 我可以为了适配一个特殊的局域网协议去写一套调试代码，也可以为了保护自己的隐私数据写一个加密密码本。这八年积累下来的不只是功能，更是对 Go、Rust、前端交互以及各种底层协议的深度理解。\n关于未来的“深度复盘” 八年的积淀太厚，小程序那方寸之间已经装不下太多的原理说明。\n所以，我决定在我的新博客中，为这些工具开辟专门的专栏。我会挑选出其中含金量最高、逻辑最有趣的工具，拆解它们的实现原理。比如：\n多媒体黑盒：音频和图片格式转换在小程序前端是如何高效完成的？ 网络深水区：局域网内的协议调试有哪些不为人知的坑？ 消息推送系统：我是如何设计那套多端触发的事件通知系统的？ 这些内容我会以“实战笔记”的形式，在这里逐一连载。\n这颗“豆子”还会继续长下去，而关于它的故事，我们去博客细聊。\n","permalink":"https://blog.91demo.top/wiki/wander-intro.html","summary":"\u003cp\u003e有些事，不做笔记真的意识不到已经过去了这么久。\u003c/p\u003e\n\u003cp\u003e最近在整理博客内容时，我翻到了**“豆子工具”小程序**的最早版本记录。那是 2018 年，最初的它简陋得甚至有点滑稽：整个页面只有一个按钮，点一下，生成一个随机数。\u003c/p\u003e\n\u003cp\u003e谁能想到，这颗“小豆子”一跑就是八年。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"豆子工具\" loading=\"lazy\" src=\"/images/wander/wander-page.webp\"\u003e\u003c/p\u003e\n\u003ch3 id=\"八年它长成了我最趁手的瑞士军刀\"\u003e八年，它长成了我最趁手的“瑞士军刀”\u003c/h3\u003e\n\u003cp\u003e这八年里，我在这款小程序上倾注了太多的时间和精力。它更像是我个人开发者生涯的一个“活化石”，记录了我每一个阶段遇到的问题和想出的方案。\u003c/p\u003e\n\u003cp\u003e我始终坚持一个原则：\u003cstrong\u003e只做真正有用的。\u003c/strong\u003e 现在的“豆子工具”已经从当年的随机数生成器，进化成了一个覆盖网络、多媒体、开发调试的全能工具箱：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e网络与运维必备\u003c/strong\u003e：获取 WIFI 公网地址、云厂商安全组管理、查服务端口、域名证书检测。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e多媒体处理\u003c/strong\u003e：音频格式转换（m4a 转 mp3）、图片格式转换（png 转 webp）、二维码识别与生成。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e开发调试利器\u003c/strong\u003e：局域网调试（TCP/UDP/WebSocket）、ESP 网络配置、时间戳转换、Base64 与 URL 编解码。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e安全与提醒\u003c/strong\u003e：私密密码本、事件邮件通知、微信服务通知、语音验证码。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这中间还有很多功能，比如查看上报信息、特定的算法实现等。当然，也有不少功能因为受众太小或者微信审核的边界问题，遗憾地消失在了版本更迭中。\u003c/p\u003e\n\u003ch3 id=\"它是工具也是我的技术底座\"\u003e它是工具，也是我的技术底座\u003c/h3\u003e\n\u003cp\u003e很多人问过我，市面上工具软件那么多，为什么要自己写？\u003c/p\u003e\n\u003cp\u003e答案很简单：\u003cstrong\u003e自由。\u003c/strong\u003e 我可以为了适配一个特殊的局域网协议去写一套调试代码，也可以为了保护自己的隐私数据写一个加密密码本。这八年积累下来的不只是功能，更是对 Go、Rust、前端交互以及各种底层协议的深度理解。\u003c/p\u003e\n\u003ch3 id=\"关于未来的深度复盘\"\u003e关于未来的“深度复盘”\u003c/h3\u003e\n\u003cp\u003e八年的积淀太厚，小程序那方寸之间已经装不下太多的原理说明。\u003c/p\u003e\n\u003cp\u003e所以，我决定在我的新博客中，为这些工具开辟专门的专栏。我会挑选出其中含金量最高、逻辑最有趣的工具，拆解它们的\u003cstrong\u003e实现原理\u003c/strong\u003e。比如：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e多媒体黑盒\u003c/strong\u003e：音频和图片格式转换在小程序前端是如何高效完成的？\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e网络深水区\u003c/strong\u003e：局域网内的协议调试有哪些不为人知的坑？\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e消息推送系统\u003c/strong\u003e：我是如何设计那套多端触发的事件通知系统的？\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这些内容我会以“实战笔记”的形式，在这里逐一连载。\u003c/p\u003e\n\u003cp\u003e这颗“豆子”还会继续长下去，而关于它的故事，我们去博客细聊。\u003c/p\u003e","title":"八年了，从一个随机数按钮到我的技术底座：豆子工具复盘"}]