<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Go on 豆子技术站</title><link>https://blog.91demo.top/go/index.html</link><description>Recent content in Go on 豆子技术站</description><generator>Hugo -- 0.155.1</generator><language>zh-cn</language><lastBuildDate>Tue, 17 Mar 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.91demo.top/go/index.xml" rel="self" type="application/rss+xml"/><item><title>为豆子域名管家实现版本检测功能的技术实践</title><link>https://blog.91demo.top/go/btaddvercheck.html</link><pubDate>Tue, 17 Mar 2026 09:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/btaddvercheck.html</guid><description>&lt;p&gt;豆子域名管家是一款基于 Wails 3 开发的超轻量级单文件客户端。它不仅拥有 Naive UI 打造的精致界面，更集成了强大的域名监控能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全方位监测：主面板表格直观展示域名列表、SSL 证书剩余天数、Whois 到期时间。&lt;/li&gt;
&lt;li&gt;智能告警：支持自定义告警阈值，状态（正常/警告/错误）一目了然。&lt;/li&gt;
&lt;li&gt;多渠道通知：深度集成钉钉与企业微信，支持配置调度时间与通知频次，确保告警不漏报、不打扰。&lt;/li&gt;
&lt;li&gt;静默守护：支持随系统自动启动，后台默默守护您的域名安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="为什么需要版本提醒为什么不是自动更新"&gt;为什么需要版本提醒，为什么不是自动更新？&lt;/h2&gt;
&lt;p&gt;在开发 豆子域名管家 时，Wails 3 就提供了自动集成的更新方案，但经过深思熟虑，我选择了“版本提醒 + 手动覆盖”的策略：主要考虑到个人服务器的带宽限制，避免高并发下载造成服务器宕机。单文件二进制直接覆盖即可完成升级，无需复杂的安装程序。&lt;/p&gt;
&lt;p&gt;在之前的版本中，我主要通过公众号发布新版本并提供下载链接进行手动升级。它的问题是用户需要关注公众号并手动前往蓝奏云下载。为了进一步优化体验，我决定在新版本中引入版本对比提醒：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;红点微标提醒：客户端启动后会自动对比本地与云端版本号。若有新版，左下角版本号将出现灵动的红点徽标，提醒而不打扰。&lt;/li&gt;
&lt;li&gt;交互式遮罩弹窗：点击徽标即可弹出基于 Naive UI 开发的漂亮窗口，清晰展示：
&lt;ul&gt;
&lt;li&gt;当前版本 vs 最新版本&lt;/li&gt;
&lt;li&gt;详细的更新日志&lt;/li&gt;
&lt;li&gt;保姆级使用方法说明&lt;/li&gt;
&lt;li&gt;极简升级路径：点击“复制下载链接”，直接在浏览器中下载最新的二进制文件，覆盖旧文件即可完成升级。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="版本检测核心实现思路"&gt;版本检测核心实现思路&lt;/h2&gt;
&lt;p&gt;这不仅仅是一个简单的弹窗，背后凝聚了对安全和灵活性的思考。为了确保版本检测既高效又安全，我们采用了以下技术路径：&lt;/p&gt;
&lt;p&gt;1，我没有在前端 JS 中硬编码版本号。使用了Go 后端驱动，客户端通过 Wails 3 的原生桥接功能，调用后端 Go 方法获取本地编译时的版本标识。&lt;/p&gt;
&lt;p&gt;2，防爬虫机制：在向云端请求最新版本信息时，配置了特定的 Custom Header 校验，有效拦截恶意脚本和不必要的扫描，保护服务器资源。&lt;/p&gt;
&lt;p&gt;3，设计了灵活的云端数据结构。我们的版本接口不只是返回一个数字，而是返回一个包含三个核心维度的 JSON 对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最新版本号：用于精准比对。&lt;/li&gt;
&lt;li&gt;下载地址：支持动态变更下载镜像，防止因链接失效导致的无法更新。&lt;/li&gt;
&lt;li&gt;更新日志：让用户第一时间了解修复了哪些 Bug 或新增了哪些功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4，极致的 UI 交互（基于 Naive UI）&lt;br&gt;
当本地版本低于云端时，左下角会亮起精美的红点徽标。点击后的交互逻辑非常人性化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信息全透明：对比当前版本与新版本，展示完整的更新列表和使用指南。&lt;/li&gt;
&lt;li&gt;尊重用户选择：弹窗配备了清晰的“取消/稍后再说”按钮，绝不强制升级。&lt;/li&gt;
&lt;li&gt;快捷操作：提供“复制下载链接”按钮，点击后自动写入剪切板。用户只需打开浏览器粘贴，即可从蓝奏云等高速渠道下载，覆盖即升级。免去了前期的复杂步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="界面功能效果预览"&gt;界面功能效果预览&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;徽标提示&lt;br&gt;
&lt;img alt="alt 徽标提示" loading="lazy" src="https://blog.91demo.top/images/go/vercheck-tips.png"&gt;&lt;/li&gt;
&lt;li&gt;弹窗说明&lt;br&gt;
&lt;img alt="alt 弹窗说明" loading="lazy" src="https://blog.91demo.top/images/go/vercheck-window.png"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="工具如何开始使用"&gt;工具如何开始使用？&lt;/h2&gt;
&lt;p&gt;无论您是拥有几十个域名的运维大拿，还是只有几个小站的个人玩家，豆子域名管家都是您的不二之选。&lt;/p&gt;</description></item><item><title>为豆子域名管家实现Windows开机自启动功能的技术实践</title><link>https://blog.91demo.top/go/addautostart.html</link><pubDate>Sun, 01 Mar 2026 09:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/addautostart.html</guid><description>&lt;p&gt;在PC客户端开发中，开机自启动是提升用户体验的重要功能之一。豆子域名管家作为一款Windows平台下的域名管理工具，近期添加了随系统启动功能。本文将详细介绍从技术选型到最终实现的全过程，重点阐述在跨平台库适配失败后，如何针对Windows系统特性实现简洁可靠的自启动方案。&lt;/p&gt;
&lt;h2 id="一技术选型与挑战"&gt;一、技术选型与挑战&lt;/h2&gt;
&lt;h3 id="11-初始方案跨平台库的尝试"&gt;1.1 初始方案：跨平台库的尝试&lt;/h3&gt;
&lt;p&gt;项目初期采用了go-autostart这一流行的Go语言跨平台自启动库。该库设计优雅，理论上支持Windows、macOS、Linux三大主流操作系统。然而在实际集成到wails3项目中时，遇到了编译问题：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sslchecker
.\domainservice.go:1453:11: app.Enable undefined (type *autostart.App has no field or method Enable)
.\domainservice.go:1455:11: app.Disable undefined (type *autostart.App has no field or method Disable)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;经过排查，发现虽然能定位到源码，但由于系统配置或依赖管理问题，无法正确调用库方法。这种问题在Go模块化开发中并不少见，特别是涉及CGO或系统特定依赖时。&lt;/p&gt;
&lt;h3 id="12-平台限制的现实考量"&gt;1.2 平台限制的现实考量&lt;/h3&gt;
&lt;p&gt;进一步分析发现，即使解决编译问题，跨平台方案仍面临以下限制：&lt;/p&gt;
&lt;p&gt;macOS的签名要求：自macOS Catalina以来，苹果加强了应用安全策略。无签名的应用在开机自启动时会被Gatekeeper拦截，除非用户手动进入系统设置&amp;gt;安全性与隐私&amp;gt;通用中点击&amp;quot;仍要打开&amp;quot;。&lt;/p&gt;
&lt;p&gt;Linux的碎片化：不同桌面环境（GNOME、KDE、XFCE等）的自启动机制存在差异，需要适配多种配置方式。&lt;/p&gt;
&lt;p&gt;维护成本：跨平台库在提供便利的同时，也引入了额外的依赖和潜在的兼容性问题。&lt;/p&gt;
&lt;p&gt;考虑到豆子域名管家主要用户群体为Windows用户，且无macOS开发者证书，决定采用专注Windows的轻量化方案。&lt;/p&gt;
&lt;h2 id="二windows自启动实现方案"&gt;二、Windows自启动实现方案&lt;/h2&gt;
&lt;h3 id="21-技术原理"&gt;2.1 技术原理&lt;/h3&gt;
&lt;p&gt;Windows开机自启动主要通过注册表实现。当前用户的自启动项位于：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;系统级的自启动项（需要管理员权限）位于：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于大多数桌面应用，使用用户级注册表即可满足需求，且无需提权操作。&lt;/p&gt;
&lt;h3 id="22-核心实现代码"&gt;2.2 核心实现代码&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// auto_start_windows.go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// +build windows&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;package&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;errors&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;os&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;path/filepath&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;strings&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;golang.org/x/sys/windows/registry&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// AutoStartManager Windows自启动管理器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// NewAutoStartManager 创建自启动管理器实例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;NewAutoStartManager&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;) (&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;os&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Executable&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;errors&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;New&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;获取可执行文件路径失败&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 转换为绝对路径并处理空格&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;filepath&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Abs&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Contains&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34; &amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;absPath&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;`Software\Microsoft\Windows\CurrentVersion\Run`&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// IsAutoStartEnabled 检查是否已启用开机自启动&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;IsAutoStartEnabled&lt;/span&gt;() (&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;OpenKey&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;CURRENT_USER&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;QUERY_VALUE&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Close&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;GetStringValue&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;ErrNotExist&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 对比路径，处理可能的引号差异&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;current&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Trim&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;stored&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Trim&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;`&amp;#34;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;EqualFold&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;filepath&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Clean&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;current&lt;/span&gt;), &lt;span style="color:#a6e22e"&gt;filepath&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Clean&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;stored&lt;/span&gt;)), &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// SetAutoStart 设置或取消开机自启动&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AutoStartManager&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;SetAutoStart&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;enable&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;OpenKey&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;CURRENT_USER&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;regPath&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;QUERY_VALUE&lt;/span&gt;|&lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;SET_VALUE&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Close&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;enable&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;SetStringValue&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exePath&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; = &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;DeleteValue&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appName&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registry&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;ErrNotExist&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 键不存在不算错误&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="23-关键实现细节"&gt;2.3 关键实现细节&lt;/h3&gt;
&lt;p&gt;路径处理：使用os.Executable()获取可执行文件绝对路径，确保在不同工作目录下都能正确运行。&lt;/p&gt;</description></item><item><title>Wails 3 进阶实战：基于 Go 语言实现 frp 自动化管理客户端的代码深度解析</title><link>https://blog.91demo.top/go/mole-summary.html</link><pubDate>Wed, 11 Feb 2026 10:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/mole-summary.html</guid><description>&lt;h2 id="一-项目设计核心思想"&gt;一、 项目设计核心思想&lt;/h2&gt;
&lt;p&gt;本项目的核心定位是内网穿透的一键化管理。参考 ngrok 的服务模式，通过自建 frp 服务器 提供稳定中转，将复杂的配置封装在 Wails 客户端中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商业闭环：通过微信小程序激励视频获取连接权限（2小时有效期）。&lt;/li&gt;
&lt;li&gt;用户体验：一键连接、自动分配二级域名、配置持久化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二-前端架构原生-js-的三维交互"&gt;二、 前端架构：原生 JS 的三维交互&lt;/h2&gt;
&lt;p&gt;为了保持轻量，前端放弃了重量级框架，采用原生 JS 与 Wails 运行时通信。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制面板 (Dashboard)：状态驱动 UI。涉及扫码弹窗逻辑、广告验证状态机。&lt;/li&gt;
&lt;li&gt;配置页面 (Settings)：表单处理。重点在于 Local Port 的保存与通过 Wails Bind 将数据下发给 Go 后端。&lt;/li&gt;
&lt;li&gt;运行日志 (Logs)：虚拟黑屏终端。难点在于实时流式展示后端 frpc 吐出的日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三-后端核心技术要点"&gt;三、 后端核心技术要点&lt;/h2&gt;
&lt;p&gt;将按照应用生命周期逻辑，对以下模块进行深度归纳：&lt;br&gt;
a. 应用原生窗口定义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构化管理：AppManager 模式&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// AppManager 统一管理应用和窗口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AppManager&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;App&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;application&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;App&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;MainWindow&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;application&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Window&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;manager&lt;/span&gt; = &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;AppManager&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;采用了 AppManager 结构体来统一持有 App 实例和 MainWindow 实例。&lt;br&gt;
知识点：在 Wails 3 中，不再像 v2 那样通过上下文（ctx）传递，而是鼓励通过对象持有的方式管理窗口引用。这方便了后续在任何 Service 中通过 manager.MainWindow 直接操控窗口（如置顶、隐藏、发送事件）。&lt;/p&gt;</description></item><item><title>网络协议新纪元：基于 Go 语言实现支持 Ed25519 加密的 QUIC 高性能通信实战</title><link>https://blog.91demo.top/go/quicgo.html</link><pubDate>Tue, 10 Feb 2026 07:05:33 +0000</pubDate><guid>https://blog.91demo.top/go/quicgo.html</guid><description>&lt;h2 id="1-为什么是-quic从-tcp-的瓶颈说起"&gt;1. 为什么是 QUIC？从 TCP 的瓶颈说起&lt;/h2&gt;
&lt;p&gt;手机在 Wi-Fi 和 4G/5G 之间切换（即“切网”）导致 TCP 连接断开，是移动互联网开发中的经典痛点。&lt;/p&gt;
&lt;p&gt;TCP 连接是基于源 IP、源端口、目的 IP、目的端口这“四元组”来标识的。当你从 Wi-Fi 切换到 5G 时，手机的 IP 地址发生了变化，旧的四元组立即失效，TCP 必须重新进行三次握手建立新连接，正在传输的数据（如视频缓冲、下载）就会中断。&lt;/p&gt;
&lt;p&gt;QUIC 引入了 Connection ID 的概念。它不依赖于底层 IP 地址。只要 CID 不变，即便你的 IP 从 A 变成了 B，服务端依然能通过 CID 认出：“噢，你还是刚才那个客户端！”。这样对于业务层完全无感知，数据传输无缝继续。这就是所谓的 “连接迁移 (Connection Migration)”。&lt;/p&gt;
&lt;p&gt;除了切网，QUIC 在弱网（丢包率高、延迟高）下更强的原因在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改进的拥塞控制： QUIC 在应用层实现，可以更激进地进行丢包恢复。&lt;/li&gt;
&lt;li&gt;无队头阻塞： 在 TCP 中，丢一个包全家等死；在 QUIC 中，你刷朋友圈的图丢了一个包，不会影响你接收聊天消息的流。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-go-库quic-go-简介"&gt;2. go 库&lt;a href="https://github.com/quic-go/quic-go"&gt;quic-go&lt;/a&gt; 简介&lt;/h2&gt;
&lt;p&gt;这是Go的库，在 Go 语言世界里，quic-go 是事实上的标准实现。它不仅完整实现了 IETF QUIC 协议，还提供了类标准库 net 的简洁接口。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connection (连接)： 代表两个端点之间的 UDP 隧道。&lt;/li&gt;
&lt;li&gt;Stream (流)： 连接内部的逻辑通道，双向且独立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="3-证书准备使用-openssl-生成-ed25519-证书"&gt;3. 证书准备：使用 OpenSSL 生成 Ed25519 证书&lt;/h2&gt;
&lt;p&gt;为了极致的性能与安全，弃用了传统的 RSA，选择 Ed25519 算法。它的签名速度更快，密钥更短。&lt;/p&gt;</description></item><item><title>全栈实战：基于 Go + 小程序构建“口袋指令中心”，实现远程发布控制</title><link>https://blog.91demo.top/go/bagscript.html</link><pubDate>Wed, 04 Feb 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/bagscript.html</guid><description>这是一个非常典型的 “运维自动化 + 私有控制台” 的场景。通过微信小程序作为安全鉴权层，利用 Go 后端作为执行引擎，构建了一个属于自己的“口袋指令中心”。</description></item><item><title>Go 语言进阶实战：编写高性能 Windows DLL 动态链接库并供第三方多语言调用</title><link>https://blog.91demo.top/go/godll.html</link><pubDate>Thu, 29 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/godll.html</guid><description>&lt;p&gt;我使用go写了一个http转WebSocket服务，这是一个命令行程序，双击可以直接运行，今天，有个新的需求，需要将这个命令行程序转为dll，然后供第三方使用。&lt;/p&gt;
&lt;h2 id="1改造程序"&gt;1，改造程序&lt;/h2&gt;
&lt;p&gt;这个命令行程序很小，核心逻辑就是启动了一个HTTP服务，然后接收连接，然后通过WebSocket将内容转发出去。所以，在调整为dll时，仅仅需要导出两个函数即可。&lt;/p&gt;
&lt;p&gt;1，StartServer，启动http服务，监听端口地址。因为不能阻塞，所以需要在监听服务时使用go方法启动一个携程。&lt;br&gt;
2，StopServer，关闭http服务，需要提供给第三方，当它关闭时，需要调用它，释放监听地址等资源。&lt;/p&gt;
&lt;p&gt;下面开始改造代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;package main
import &amp;#34;C&amp;#34; // 必须导入 C 包
import (
// ... 原有导入 ...
)
// 保持原有的全局变量和函数逻辑 (例如transDataGet, connWs 等)
// 需要注意下面的//export这中间不能有空格，否则无法导出头文件
//export StartServer
func StartServer() {
// 将原本 main 函数里的逻辑放在这里
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// ... 注册路由 ...
r.Run(&amp;#34;127.0.0.1:9988&amp;#34;)
}
// 必须保留一个空的 main 函数
func main() {}
// 其它参考StartServer，如果需要导出，一定要添加//export
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="2编译命令"&gt;2，编译命令&lt;/h2&gt;
&lt;p&gt;我是在windows环境，需要安装cgo环境，我安装了Mingw-w64支持C编译环境，执行以下命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;go build -buildmode=c-shared -o trans.dll main.go
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行后会生成trans.h和trans.dll两个文件。&lt;/p&gt;
&lt;h2 id="3验证dll"&gt;3，验证dll&lt;/h2&gt;
&lt;p&gt;除了一些可以查看dll的工具外，还可以使用第三方语言进行测试。例如我这里使用Python。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import ctypes
import time
lib = ctypes.CDLL(&amp;#34;./trans.dll&amp;#34;)
lib.StartServer()
print(&amp;#34;服务已在后台启动...&amp;#34;)
time.sleep(10) # 模拟第三方程序运行
lib.StopServer()
print(&amp;#34;服务已关闭&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="4再次优化代码"&gt;4，再次优化代码&lt;/h2&gt;
&lt;p&gt;当实际测试的时候，我发现它会阻塞调用者，为了解决这个问题，我们需要再次改造程序。将启动服务改为非阻塞模式。将Gin的启动逻辑放入协程，并利用http.Server提供的Shutdown方法来实现优雅退出。&lt;/p&gt;</description></item><item><title>告别域名过期焦虑：基于 Go + Wails 3 开发“豆子域名管家”，实现批量监测与企微钉钉预警</title><link>https://blog.91demo.top/go/sslchecker.html</link><pubDate>Wed, 28 Jan 2026 18:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/sslchecker.html</guid><description>&lt;p&gt;域名过期导致业务中断、流量缺失、品牌受损的案例比比皆是。手动记录域名的到期时间？是否会忘记，漏看？&lt;/p&gt;
&lt;p&gt;我以前曾介绍过，我在微信小程序实现了域名证书监控功能。但是担心隐私，功能限制。这次我带来了豆子域名管家，本地化运行。&lt;/p&gt;
&lt;p&gt;经过数月的规划和开发测试，豆子域名管家终于可以使用了。这是一款完全本地运行、支持批量导入管理以及可以企微和钉钉通知的域名证书检测工具。旨在解决域名过期监控难题。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;传统方式&lt;/th&gt;
&lt;th&gt;豆子域名管家&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;运行方式&lt;/td&gt;
&lt;td&gt;依赖云端服务&lt;/td&gt;
&lt;td&gt;纯本地运行，数据不出本地&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量处理&lt;/td&gt;
&lt;td&gt;手动逐个查询&lt;/td&gt;
&lt;td&gt;一键导入，批量删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;通知渠道&lt;/td&gt;
&lt;td&gt;单一（邮件/短信）&lt;/td&gt;
&lt;td&gt;钉钉+企微双通道，支持Markdown格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义提醒&lt;/td&gt;
&lt;td&gt;固定时间提醒&lt;/td&gt;
&lt;td&gt;可配置通知时间，提前预警天数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统集成&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;系统托盘常驻，后台静默运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;隐私安全&lt;/td&gt;
&lt;td&gt;数据上传第三方&lt;/td&gt;
&lt;td&gt;域名数据和配置数据存本地&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;技术架构亮点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;工具基于Wails3框架构建，采用Vue3+NaiveUI前端技术栈，确保界面简洁美观，交互流畅，支持暗黑/浅色两种主题。&lt;/li&gt;
&lt;li&gt;本地证书检测引擎，直接调用系统网络库进行TLS检测，无需依赖外部API。&lt;/li&gt;
&lt;li&gt;多线程并发处理，使用go的特性批量进行域名检测。&lt;/li&gt;
&lt;li&gt;跨平台兼容：支持Windows、macOS、Linux系统。&lt;/li&gt;
&lt;li&gt;配置持久化，所有配置本地存储，重启后自动恢复。&lt;/li&gt;
&lt;li&gt;支持域名证书检测，域名到期时间检测。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;软件运行界面预览：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;控制面板&lt;br&gt;
&lt;img alt="控制面板" loading="lazy" src="https://blog.91demo.top/images/notes/b01panel.webp"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置页面&lt;br&gt;
&lt;img alt="配置页面" loading="lazy" src="https://blog.91demo.top/images/notes/b01config.webp"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;基本操作指南：&lt;br&gt;
1，导入域名我们需要把要监控的域名按行录入到txt文档中，可以在软件配置界面下载示例模板，当准备完成后，选择刚才的文件，然后验证。没有问题后，点击确认导入即可。&lt;br&gt;
2，配置机器人目前工具支持钉钉机器人和企业微信机器人，支持Markdown格式消息，可以查点击推送预览效果按钮查看推送效果。当输入机器人配置后，可以验证测试，当收到消息后说明配置成功，点击保存即可。可以同时配置企微和钉钉。这样会同时推送两份通知。&lt;br&gt;
3，配置推送通知策略目前工具扫描调度间隔固定24小时，可以配置通知时间，和告警天数间隔。在通知时间，系统会将当天扫描的结果报表推送到机器人。如果用户更新某些域名证书后，可以在监控面板进行手动刷新。想查看效果，可以把通知时间设置为当前时间加几分钟，当到达时间后，将推送报表。确认无误后，可以调整为真正的推送时间。&lt;br&gt;
4，系统托盘运行当完成域名导入和机器人以及通知策略配置后，可以关闭窗口，工具将自动缩放到系统托盘。如果需要退出，需要右键系统托盘退出。注意，如果退出工具，将不能监控域名，因为这是一个本地工具。所以需要工具长期运行。&lt;br&gt;
5，监控面板监控面板的仪表板显示你的域名配置项统计信息，表格显示监视的域名列表。域名可以搜索，刷新以及删除。域名按照过期、告警、正常顺序排序。请查看最前面域名并及时处理。&lt;/p&gt;
&lt;p&gt;工具按照自己的真实需求开发，如果你需要尝试，可以通过下方下载链接。目前仅提供了Windows版本。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://91demo.top/b011"&gt;https://91demo.top/b011&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果您有任何问题和建议，欢迎反馈和交流。&lt;/p&gt;</description></item><item><title>深度解析：基于 Asterisk 与 SIP 协议的私有化语音验证码呼叫原理</title><link>https://blog.91demo.top/go/vcode-call.html</link><pubDate>Tue, 13 Jan 2026 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/go/vcode-call.html</guid><description>基于 Go-ARI 与 Asterisk 实现自动化语音验证码外呼系统</description></item><item><title>深度实战：基于 Wails v3 与 Go 打造跨平台 FRP 桌面客户端 Mole-go 的技术架构与原理</title><link>https://blog.91demo.top/go/mole-intro.html</link><pubDate>Sat, 10 Jan 2026 06:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/mole-intro.html</guid><description>深入解析 Wails v3 框架下的桌面应用开发，探讨如何集成 FRP 内网穿透核心逻辑、实现 Go 与前端的高效交互及多平台打包实践。</description></item><item><title>基于 Wails 3 和微信小程序的一次frp 智能管理实战</title><link>https://blog.91demo.top/go/demo-intro.html</link><pubDate>Tue, 02 Dec 2025 08:00:00 +0800</pubDate><guid>https://blog.91demo.top/go/demo-intro.html</guid><description>记录FRP GUI客户端开发的完整流程，桌面工具如何和小程序生态进行集合完成多端通信。</description></item><item><title>跨平台桌面开发新选择：Wails 3 初体验及在 FRP 管理客户端中的选型实践</title><link>https://blog.91demo.top/go/wails3-demo.html</link><pubDate>Sat, 01 Nov 2025 07:34:14 +0000</pubDate><guid>https://blog.91demo.top/go/wails3-demo.html</guid><description>&lt;h2 id="为什么选择了wails-3-"&gt;为什么选择了Wails 3 ？&lt;/h2&gt;
&lt;p&gt;Wails 3 最大的改变在于它不再强绑定于某个特定的前端框架，且引入了&lt;strong&gt;多窗口支持&lt;/strong&gt;和&lt;strong&gt;更轻量级的 Runtime&lt;/strong&gt;。它允许你在不启动主窗口的情况下运行后端服务，这正是我们实现“系统托盘”和“后台演示服务”的基础。&lt;/p&gt;
&lt;p&gt;在 v2 中，我们习惯于自动生成的 &lt;code&gt;wailsjs&lt;/code&gt; 文件夹。但在 v3 中，这一逻辑被进一步标准化。&lt;/p&gt;
&lt;p&gt;当你运行开发指令时，Wails 会扫描你的 Go 结构体方法，并将其映射为前端可以调用的 JavaScript 函数。这个过程在 v3 中被称为 &lt;strong&gt;Generate&lt;/strong&gt; 过程。&lt;/p&gt;
&lt;h3 id="wails-3-通信灵魂"&gt;Wails 3 通信灵魂&lt;/h3&gt;
&lt;p&gt;我们在前端调用在后端定义的 HandleConnect 返回自定义结构体，这在 Wails 3 中是前后端通信的灵魂。&lt;/p&gt;
&lt;p&gt;在 Wails 3 开发中，最核心的动作就是：&lt;strong&gt;后端做功，前端表现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当你调用 &lt;code&gt;MoleService.HandleConnect()&lt;/code&gt; 时，Go 后端会产生一个结果。在本项目中，我们需要同时返回一个 &lt;code&gt;Code&lt;/code&gt;（状态码）和一个 &lt;code&gt;Content&lt;/code&gt;（数据内容）。&lt;/p&gt;
&lt;p&gt;为了实现这一点，我们定义了一个结构体：&lt;br&gt;
&lt;code&gt;type Response struct { Code int; Content string }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;虽然 Go 内部使用的是结构体，但前端 JavaScript 只能读懂 JSON 对象。Wails 3 内部会自动帮你完成这个“翻译”过程。&lt;/p&gt;
&lt;p&gt;但是，如果你想让前端看到的字段名是小写的（例如 &lt;code&gt;res.code&lt;/code&gt; 而不是 &lt;code&gt;res.Code&lt;/code&gt;），你必须在 Go 结构体定义时加上“注解”。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Response&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Code&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;code&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Content&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`json:&amp;#34;content&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;记住，所有通过 bindings 调用的 Go 方法，在前端返回的都是一个 Promise 对象。这意味着你必须使用 await 或者 .then() 来接收数据，否则你拿到的将是一个永不开启的“盲盒”。&lt;/p&gt;</description></item><item><title>私有化语音验证码方案：基于 Asterisk 与 Go 的 SIP 通信及 AGI 脚本实战</title><link>https://blog.91demo.top/go/vcode.html</link><pubDate>Sun, 21 Aug 2022 12:40:16 +0800</pubDate><guid>https://blog.91demo.top/go/vcode.html</guid><description>&lt;h2 id="音频文件处理"&gt;音频文件处理&lt;/h2&gt;
&lt;p&gt;我们使用手机上的录音机来录制音频文件。&lt;br&gt;
Android 录音机录制的音频文件格式为 mp3，如果是 amr 格式，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。&lt;br&gt;
IOS 录音机录制的音频文件格式为 m4a，请使用豆子工具音频格式转换功能，转成 mp3 格式文件。&lt;/p&gt;
&lt;p&gt;我们还需要使用 ffmpeg 将 mp3 文件转成 g711a 格式文件。这个 mp3 转 g711a 功能后续会集成到豆子工具中。&lt;/p&gt;
&lt;p&gt;mp3 转 g711a 命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffmpeg -i test.mp3 -acodec pcm_alaw -f alaw -ac 1 -ar 8000 -vn test.alaw
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用 ffplay 播放测试&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ffplay -i test.alaw -f alaw -ac 1 -ar 8000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将制作好的音频文件存放在 asterisk sounds 目录，就可以在拨号计划中使用 Playback 应用调用它了。&lt;/p&gt;
&lt;h2 id="配置实时数据库"&gt;配置实时数据库&lt;/h2&gt;
&lt;p&gt;今天讲解 Asterisk 如何实时将 SIP 用户写入 sqlite3 数据库。&lt;/p&gt;
&lt;h3 id="先定义数据库表结构我当前使用的-pjsip-协议"&gt;先定义数据库表结构，我当前使用的 PJSIP 协议。&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
CREATE TABLE ps_endpoints (
id VARCHAR(40) NOT NULL,
transport VARCHAR(40),
aors VARCHAR(200),
auth VARCHAR(40),
context VARCHAR(40),
disallow VARCHAR(200),
allow VARCHAR(200),
direct_media varchar(5) check(direct_media in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
connected_line_method varchar(10) check(connected_line_method in (&amp;#39;invite&amp;#39;,&amp;#39;reinvite&amp;#39;,&amp;#39;update&amp;#39;)),
direct_media_method varchar(10) check(direct_media_method in (&amp;#39;invite&amp;#39;,&amp;#39;reinvite&amp;#39;,&amp;#39;update&amp;#39;)),
direct_media_glare_mitigation varchar(20) check(direct_media_glare_mitigation in (&amp;#39;none&amp;#39;,&amp;#39;outgoing&amp;#39;,&amp;#39;incoming&amp;#39;)),
disable_direct_media_on_nat varchar(5) check(disable_direct_media_on_nat in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
dtmf_mode varchar(20) check(dtmf_mode in (&amp;#39;rfc4733&amp;#39;,&amp;#39;inband&amp;#39;,&amp;#39;info&amp;#39;)),
external_media_address VARCHAR(40),
force_rport varchar(5) check(force_rport in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
ice_support varchar(5) check(ice_support in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
identify_by varchar(10) check(identify_by in (&amp;#39;username&amp;#39;)),
mailboxes VARCHAR(40),
moh_suggest VARCHAR(40),
outbound_auth VARCHAR(40),
outbound_proxy VARCHAR(40),
rewrite_contact varchar(5) check(rewrite_contact in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
rtp_ipv6 varchar(5) check(rtp_ipv6 in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
rtp_symmetric varchar(5) check(rtp_symmetric in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
send_diversion varchar(5) check(send_diversion in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
send_pai varchar(5) check(send_pai in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
send_rpid varchar(5) check(send_rpid in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
timers_min_se INTEGER,
timers varchar(20) check(timers in (&amp;#39;forced&amp;#39;,&amp;#39;no&amp;#39;,&amp;#39;required&amp;#39;,&amp;#39;yes&amp;#39;)),
timers_sess_expires INTEGER,
callerid VARCHAR(40),
callerid_privacy varchar(40) check(callerid_privacy in (&amp;#39;allowed_not_screened&amp;#39;,&amp;#39;allowed_passed_screened&amp;#39;,&amp;#39;allowed_failed_screened&amp;#39;,&amp;#39;allowed&amp;#39;,&amp;#39;prohib_not_screened&amp;#39;,&amp;#39;prohib_passed_screened&amp;#39;,&amp;#39;prohib_failed_screened&amp;#39;,&amp;#39;prohib&amp;#39;,&amp;#39;unavailable&amp;#39;)),
callerid_tag VARCHAR(40),
`100rel` varchar(20) check(`100rel` in (&amp;#39;no&amp;#39;,&amp;#39;required&amp;#39;,&amp;#39;yes&amp;#39;)),
aggregate_mwi varchar(5) check(aggregate_mwi in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
trust_id_inbound varchar(5) check(trust_id_inbound in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
trust_id_outbound varchar(5) check(trust_id_outbound in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
use_ptime varchar(5) check(use_ptime in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
use_avpf varchar(5) check(use_avpf in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
media_encryption varchar(10) check(media_encryption in (&amp;#39;no&amp;#39;,&amp;#39;sdes&amp;#39;,&amp;#39;dtls&amp;#39;)),
inband_progress varchar(5) check(inband_progress in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
call_group VARCHAR(40),
pickup_group VARCHAR(40),
named_call_group VARCHAR(40),
named_pickup_group VARCHAR(40),
device_state_busy_at INTEGER,
fax_detect varchar(5) check(fax_detect in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
t38_udptl varchar(5) check(t38_udptl in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
t38_udptl_ec varchar(20) check(t38_udptl_ec in (&amp;#39;none&amp;#39;,&amp;#39;fec&amp;#39;,&amp;#39;redundancy&amp;#39;)),
t38_udptl_maxdatagram INTEGER,
t38_udptl_nat varchar(5) check(t38_udptl_nat in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
t38_udptl_ipv6 varchar(5) check(t38_udptl_ipv6 in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
tone_zone VARCHAR(40),
language VARCHAR(40),
one_touch_recording varchar(5) check(one_touch_recording in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
record_on_feature VARCHAR(40),
record_off_feature VARCHAR(40),
rtp_engine VARCHAR(40),
allow_transfer varchar(5) check(allow_transfer in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
allow_subscribe varchar(5) check(allow_subscribe in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
sdp_owner VARCHAR(40),
sdp_session VARCHAR(40),
tos_audio INTEGER,
tos_video INTEGER,
cos_audio INTEGER,
cos_video INTEGER,
sub_min_expiry INTEGER,
from_domain VARCHAR(40),
from_user VARCHAR(40),
mwi_fromuser VARCHAR(40),
dtls_verify VARCHAR(40),
dtls_rekey VARCHAR(40),
dtls_cert_file VARCHAR(200),
dtls_private_key VARCHAR(200),
dtls_cipher VARCHAR(200),
dtls_ca_file VARCHAR(200),
dtls_ca_path VARCHAR(200),
dtls_setup varchar(20) check(dtls_setup in (&amp;#39;active&amp;#39;,&amp;#39;passive&amp;#39;,&amp;#39;actpass&amp;#39;)),
srtp_tag_32 varchar(5) check(srtp_tag_32 in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
UNIQUE (id)
);
CREATE INDEX ps_endpoints_id ON ps_endpoints (id);
CREATE TABLE ps_auths (
id VARCHAR(40) NOT NULL,
auth_type varchar(10) check(auth_type in (&amp;#39;md5&amp;#39;,&amp;#39;userpass&amp;#39;)),
nonce_lifetime INTEGER,
md5_cred VARCHAR(40),
password VARCHAR(80),
realm VARCHAR(40),
username VARCHAR(40),
UNIQUE (id)
);
CREATE INDEX ps_auths_id ON ps_auths (id);
CREATE TABLE ps_aors (
id VARCHAR(40) NOT NULL,
contact VARCHAR(40),
default_expiration INTEGER,
mailboxes VARCHAR(80),
max_contacts INTEGER,
minimum_expiration INTEGER,
remove_existing varchar(5) check(remove_existing in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
qualify_frequency INTEGER,
authenticate_qualify varchar(5) check(authenticate_qualify in (&amp;#39;yes&amp;#39;,&amp;#39;no&amp;#39;)),
UNIQUE (id)
);
CREATE INDEX ps_aors_id ON ps_aors (id);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;PJSIP 测试数据：&lt;/p&gt;</description></item></channel></rss>