如何在 IT 运维中节省开支

当前,降本增效已成为各行各业的共识,如何在业务和支撑环节中有效节省开支,几乎是每个从业者必须面对的问题。本文将结合个人经验,记录一些在 IT 运维实践中节省成本的措施。 0x01 断舍离 定期梳理闲置的资源和服务,及时关闭不必要的项目。 例如: 对于因计划调整而搁置的项目,预留的资源应尽早下线; 对于因业务调整而不再需要的服务和资源,应及时停用; 在运行过程中发现长期闲置的资源,应及时释放。 0x02 高转低 根据实际使用情况,评估当前资源配置是否过高。若发现资源利用率较低,可以考虑将高配资源替换为低配资源,以节省费用。 例如,服务器和数据库实例等,可以根据 CPU、内存、存储等指标,在满足业务需求的前提下,选择更经济的规格。 此外,可以考虑通过性能优化,降低资源需求,从而使用更低配置的资源。 0x03 商转免 使用免费或开源软件替代付费软件 例如,一些存储中间件、消息中间件和监控系统等,可以考虑使用成熟的开源软件替代商业软件,以节省费用。 当然,在选择开源软件时,需考虑其功能和性能是否满足要求,同时也要评估运维和支持成本。 使用免费证书替代付费证书 对于一些小公司和个人网站,可以考虑使用 Let’s Encrypt 等免费证书颁发机构提供的 SSL 证书,以节省证书费用。详见我的另一篇文章:借助 Let’s Encrypt 节省 SSL 证书费用。 0x04 优策略 在采购硬件、软件和服务时,可以通过一些策略节省成本: 如果采购量较大,可以尝试与供应商谈判,争取更优惠的价格; 选择合适的时机进行采购,不急用的资源可以在促销季统一采购; 在商业服务的计费模式选择上,按量付费和包年包月各有优劣,结合实际使用情况选择最合适的方式,并按需调整。 0x05 常复盘 定期进行成本复盘,检查是否有可以优化的细节。 例如: 年代久远的视频文件,可以通过精简多清晰度副本来节省存储成本; 关注云平台的日常费用消耗,查看服务商是否推出新的资源包以抵扣费用; … 这是一个持续优化的过程,为公司节省的每一分钱,都可能是在寒冬中生存下去的机会。 您还有哪些有效的节省成本经验,期待评论交流!

2025/12/9
articleCard.readMore

借助 Let’s Encrypt 节省 SSL 证书费用

向服务商购买一张常见的 DV 通配符 SSL 证书,通常每年价格在数百至一千多元人民币不等;若名下有多个域名需要使用证书,总费用每年可能达到数千元。 在当前强调降本增效的环境下,若评估后认为免费证书能够满足需求,小公司和个人网站即可节省相应成本。 Let’s Encrypt 简介 Let’s Encrypt 是一家免费、开放、自动化的公益性证书颁发机构(CA),由互联网安全研究组(ISRG)运作,属于非营利组织。其目标是推广 HTTPS 的应用,为构建更安全、尊重隐私的互联网提供免费而便捷的支持。 操作方法 根据不同使用环境,Let’s Encrypt 提供多种验证与获取证书的方式。常用工具是 Certbot,详见文档:https://eff-certbot.readthedocs.io/en/stable/。 在部分环境中,可配置工具定期自动续期,减少维护工作。 由于服务器环境较为老旧,且需要将证书上传至阿里云并部署到多个云服务,本文暂采用“本地生成证书—手动上传与更新”的方式。 0x01 在本地生成证书 本文使用 Docker 运行 Certbot,参见文档:https://eff-certbot.readthedocs.io/en/stable/install.html#alternative-1-docker。 生成通配符证书的示例命令如下: docker run -it --rm --name certbot \ -v '/Users/mazhuang/some/path/letsencrypt:/etc/letsencrypt' \ certbot/certbot certonly \ --preferred-challenges dns \ --manual \ --server https://acme-v02.api.letsencrypt.org/directory \ --key-type rsa --rsa-key-size 2048 --preferred-challenges dns:使用 DNS 方式进行域名验证; --manual:以交互式方式进行询问与操作; --key-type rsa --rsa-key-size 2048:生成 2048 位 RSA 私钥(部分阿里云服务不支持默认的 ECC 证书)。 执行后会依次询问邮箱、协议授权、域名等信息,随后提示添加 DNS TXT 记录以完成域名所有权验证,按提示操作即可。 生成成功后,证书与私钥保存在挂载的本地目录中,例如上述命令中的 /Users/mazhuang/some/path/letsencrypt/archive/{domain name}。各文件的说明可参考:https://eff-certbot.readthedocs.io/en/stable/using.html#where-certs。 0x02 上传和部署证书 将证书上传到阿里云的数字证书管理服务。可使用其一键部署功能(付费),或在各云服务中手动选择使用该证书(免费),按需取用。 0x03 定期更新证书 Let’s Encrypt 颁发的证书有效期为 90 天,建议在到期前 30 天内更新。可重复步骤 0x01 生成新证书,然后上传并部署。 注意事项 部分极为老旧的平台有可能不支持 Let’s Encrypt 颁发的证书,建议评估后再决定是否使用,具体的兼容情况可以参考:https://letsencrypt.org/zh-cn/docs/certificate-compatibility/ 。 比如我这边就遇到了因为使用的是 JDK 8 的低于 141 的版本,部署完证书后,发现 xxl-job 定时任务执行器没有注册上,报错 sun.security.validator.ValidatorException: PKIX path building failed。 解决方法: 下载 ISRG Root X1 证书 在这里可以找到: https://letsencrypt.org/certificates/ cd /opt get https://letsencrypt.org/certs/isrgrootx1.pem 导入证书到 JDK 的 cacerts 中 keytool -trustcacerts -keystore "/opt/jdk/jre/lib/security/cacerts" -storepass changeit -noprompt -importcert -alias lets-encrypt-x1 -file "/opt/isrgrootx1.pem" 重启服务 小结 以上步骤简单、成本为零。对小公司和个人网站而言,是节省 SSL 证书费用的可行方案。 若环境允许,建议配置自动化续期,进一步降低维护成本,按需采用。 参考链接 https://letsencrypt.org/zh-cn/ https://eff-certbot.readthedocs.io/en/stable/

2025/11/27
articleCard.readMore

解决访问 https 网站时,后端重定向或获取 URL 变成 http 的问题

一种常见的服务部署架构是 Nginx 反向代理后端 Java 应用服务器,Nginx 监听 443 端口处理 https 请求,然后转发给后端服务器。 对应的 Nginx 配置大致如下: upstream www { server 192.168.1.101:8080 weight=100 max_fails=3 fail_timeout=10s; server 192.168.1.102:8080 weight=100 max_fails=3 fail_timeout=10s; } server { listen 443 ssl; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://www; } } 即:客户端与 Nginx 之间是 https,Nginx 与后端 Java 应用服务器之间是 http。 这样可能会遇到一些问题,如: HttpServletRequest.getRequestURL() 获取到的 URL 是 Nginx 与后端服务器之间的 http URL,比如 http://192.168.1.101:8080/xxx; HttpServletResponse.sendRedirect() 生成的重定向 URL 也是 http URL。 要解决这些问题,可以通过 Nginx 配置 + 少量后端代码修改来实现。 解决应用中获取到的 URL 的问题 用户实际访问的是 https://example.com/xxx,但是后端应用获取到的 URL 是 http://192.168.1.101:8080/xxx,如何让后端应用获取到正确的 URL 呢? 第一步,Nginx 可以通过 proxy_set_header Host 指令将客户端请求的 Host 头传递给后端服务器: location / { # ... proxy_set_header Host $host; } 这样,后端应用通过 HttpServletRequest.getRequestURL() 获取到的 URL 就是 http://example.com/xxx 了。 但此时,协议仍然不对,还是 http。 要给后端应用传递正确的协议,通常的做法是使用 X-Forwarded-Proto 头: location / { # ... proxy_set_header X-Forwarded-Proto $scheme; } 添加这个头之后并不会让 HttpServletRequest.getRequestURL() 直接返回 https URL,需要在后端应用中做一些处理。以 Java 应用为例,可以通过一个过滤器(Filter)来修改 request 的 scheme: import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class XForwardedProtoFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; String xForwardedProto = httpRequest.getHeader("X-Forwarded-Proto"); if (StringUtils.isNotBlank(xForwardedProto) && !xForwardedProto.equalsIgnoreCase(httpRequest.getScheme()) && xForwardedProto.equalsIgnoreCase("https")) { httpRequest = new HttpServletRequestWrapper(httpRequest) { @Override public String getScheme() { return xForwardedProto; } @Override public StringBuffer getRequestURL() { StringBuffer requestURL = super.getRequestURL(); if (requestURL != null && requestURL.length() > 0) { int index = requestURL.indexOf("://"); if (index > 0) { requestURL.replace(0, index, xForwardedProto); } } return requestURL; } }; } chain.doFilter(httpRequest, response); } else { chain.doFilter(request, response); } } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } } 至此,后端应用通过 HttpServletRequest.getRequestURL() 获取到的 URL 就是 https://example.com/xxx 了。 解决重定向 URL 的问题 后端应用通过 HttpServletResponse.sendRedirect() 生成的重定向 URL 也是 http URL,如何让它变成 https 呢? 这个问题可以通过 Nginx 的另一指令 proxy_redirect 来解决,该指令用于修改从后端服务器返回的 Location 和 Refresh 响应头。 location / { # ... proxy_redirect http:// $scheme://; } 这样,当后端应用返回一个重定向响应时,Nginx 会将 Location 头中的 http:// 替换为 $scheme://,即 https://。 进一步思考:当 Nginx 前面还有负载均衡器时 在很多情况下,Nginx 前面可能还有商用负载均衡器(如 AWS ELB、阿里云 SLB 等),这时需要考虑负载均衡器与 Nginx 之间的协议问题。 如果负载均衡器与 Nginx 之间是 http,而 Nginx 与后端应用之间是 http,那么就需要在负载均衡器和 Nginx 之间添加 X-Forwarded-Proto 头,以便 Nginx 能够正确地识别原始请求的协议。 主流的负载均衡器配置项里应该都有添加 X-Forwarded-Proto 头的选项开关,比如阿里云: 需要注意的是这样配置后,Nginx 配置也需要做相应的调整,将 $scheme 替换为 $http_x_forwarded_proto: (此种场景 $scheme 为负载均衡器与 Nginx 之间的协议 http,$http_x_forwarded_proto 为负载均衡器通过 Header 透传过来的前端访问协议 https。) location / { # ... proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_redirect http:// $http_x_forwarded_proto://; } 参考链接 Nginx 官方文档 - proxy_set_header Nginx 官方文档 - proxy_redirect Nginx 官方文档 - Embedded Variables

2025/11/14
articleCard.readMore

清除 GitHub 上的幽灵通知

最近我的 GitHub 页面右上角一直有个小蓝点,就像这样: 这是有未读通知的指示,但点进去却什么也看不到。 这种「幽灵通知」已经干扰了我的正常使用体验。这天实在忍无可忍,正打算给 GitHub 提交一个工单时,在官方开设的讨论区里发现了一个讨论贴,我在里面找到了有效的临时解决办法,特此记录下来以备后用。 讨论贴链接:https://github.com/orgs/community/discussions/6874 方案一 我使用的是讨论里提到的 利用浏览器开发者工具的解决方案。 在浏览器打开通知列表页面,点击左侧标记有数字的 Filters 或者 Repositories,比如我上文贴的图里的 Participating、Mentioned,或者 outcaster552/gitcoinpromosender、gitcionoda/org、yycombinator/-co、paradigm-ventures/paradigm 等等。 打开浏览器的开发者工具(F12),切换到 Console 标签页,粘贴以下代码并回车执行: document.querySelector('.js-notifications-mark-all-actions').removeAttribute('hidden'); document.querySelector('.js-notifications-mark-all-actions form[action="/notifications/beta/archive"] button').removeAttribute('disabled'); 这时页面上会出现一个 Done 按钮,点击它即可清除对应的通知。 方案二 如果以上办法没有解决问题,还可以试一下贴子里 被标记为答案的回复 是一个借助 curl 命令的解决方案。 curl -X PUT -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token $TOKEN" \ https://api.github.com/notifications \ -d '{"last_read_at":"2025-10-15T10:00:00Z"}' 其中 $TOKEN 需要替换成你自己的 GitHub 个人访问令牌(Personal Access Token),可以在 https://github.com/settings/tokens/new 创建,注意 Select scopes 里需要勾选 notifications。命令里的 last_read_at 字段的值可以按需修改为当前时间。 结语 这种幽灵通知,看情况应该来自于某些居心不良的开发者,在他们的仓库里恶意 at 大量的用户来引流,然后这些用户就会收到通知。但如果这些仓库后来因为违规被删除,那么这些通知就会变成幽灵通知,无法被正常清除。 看讨论的时间线,这个问题已经存在至少四年了,GitHub 官方似乎并没有打算修复它。希望本文能帮到和我有同样困扰的朋友。

2025/10/15
articleCard.readMore

Java|FreeMarker 复用 layout

项目里的页面一多,重复的页面布局就不可避免地冒了出来,作为程序员,消除重复,义不容辞。那么,今天就来聊聊如何在 FreeMarker 中复用页面 layout,让代码更优雅、更易维护。 常规做法:include FreeMarker 提供了 include 指令,可以把一些公共页面元素单独提取出来,然后在需要的地方通过 include 引入,例如: <#-- includes/header.ftl --> <p>我来组成头部</p> <#-- includes/footer.ftl --> <p>我来组成底部</p> <#-- somepage.ftl --> <#include "./includes/header.ftl"> <p>我是页面内容</p> <#include "./includes/footer.ftl"> <script> // 这里是一些 JavaScript 代码 </script> 去除重复:抽象 layout 但是所有类似的页面都要手写这个结构也挺麻烦的,更糟糕的是,一旦这些页面的结构发生变化,得在 N 个页面里反复修改,想想都头大。 很多博客引擎(比如 Jekyll)都支持 layout 功能,允许我们定义统一的页面布局,具体页面只需专注于内容。 FreeMarker 虽然没有内置 layout,但我们可以用 macro 来实现类似的效果。 比如,抽象出一个 layout/page.ftl 文件,作为布局模板: <#-- layout/page.ftl --> <#macro layout body js=""> <#include "../includes/header.ftl" /> ${body} <#include "../includes/footer.ftl" /> ${js} </#macro> 然后在需要的页面这样用: <#import "./layout/page.ftl" as base> <#assign body> <p>我是页面内容</p> <p>当前时间:<span id="current-time">${.now?string("yyyy-MM-dd HH:mm:ss")}</span></p> </#assign> <#assign js> <script> // 每隔一秒刷新当前时间 setInterval(function() { document.getElementById("current-time").innerHTML = new Date().toLocaleString(); }, 1000); </script> </#assign> <@base.layout body=body js=js /> 页面效果如下: 减少手工输入:code snippets 虽然布局复用问题解决了,但每次新建页面还得手写一遍结构,还是不够优雅。程序员的信条是:能自动化的绝不手动! 这时就轮到编辑器/IDE 的 code snippets 功能登场了。把上面的结构定义成代码片段,新建页面时只需输入一个触发词,基本结构就自动生成。 以 VSCode 为例,可以在项目的 .vscode 目录下新建 layout.code-snippets 文件,内容如下: { "page_layout": { "scope": "ftl", "prefix": "layout:page", "body": [ "<#import \"./layout/page.ftl\" as base>", "", "<#assign body>", "", "", "", "</#assign>", "", "<#assign js>", "", "<script>", "", "</script>", "", "</#assign>", "", "<@base.layout body=body js=js />" ], "description": "Page layout template for FTL files" } } 这样新建 .ftl 文件后,输入 layout:page,页面布局结构就自动生成了。 如图所示: IntelliJ IDEA 也可以用 Live Templates 实现同样的效果。 本文相关代码和示例已上传至 GitHub,见 https://github.com/mzlogin/learn-spring 的 freemarker-test 目录。

2025/8/30
articleCard.readMore

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LAUNCHPAD,按照说明在 Mac 上也可以使用。 而我想着后期做一些定制,所以还是需要在 Mac 上搭建 ESP-IDF 开发环境,自己编译和烧录固件。而这个在 小智 AI 聊天机器人百科全书 中没有详细提及,所以我就记录一下搭建过程,供有需要的朋友参考。 先上一个跑起来后的效果: 配置 macOS 平台工具链 这一步参考乐鑫官方的 Linux 和 macOS 平台工具链的标准设置 完成,我这里指定了使用 ESP-IDF v5.4.1 版本,编译目标是 ESP32-S3。 第一步:安装前置依赖 brew install cmake ninja dfu-util ccache python3 第二步:获取 ESP-IDF mkdir ~/github cd ~/github git clone -b v5.4.1 --recursive https://github.com/espressif/esp-idf.git ESP-IDF 将下载至 ~/github/esp-idf 目录。 第三步:设置工具 cd ~/github/esp-idf ./install.sh esp32s3 第四步:设置环境变量 在 ~/.zshrc 中添加以下内容: alias get_idf='. $HOME/github/esp-idf/export.sh' 然后 source ~/.zshrc 使其生效。 这样在需要用到 ESP-IDF 环境的时候,只需要在终端中执行 get_idf 即可。 在执行以上步骤时,如果遇到问题,可以到 乐鑫官方文档 里看看有没有解决方案。 下载和编译小智 AI 固件 cd ~/github git clone -b v1.6.2 git@github.com:78/xiaozhi-esp32.git cd xiaozhi-esp32 然后接入 ESP32-S3 开发板,执行以下命令: get_idf idf.py set-target esp32s3 idf.py build idf.py flash monitor 一切顺利的话,会向 ESP32-S3 开发板烧录小智 AI 固件,并且进入监控模式。 至此,就初步能跑起来了。按照提示进行 WiFi 配置和小智 AI 平台的设备绑定,即可开始使用。 如果后续需要定制固件,可以基于 ~/github/xiaozhi-esp32 目录进行修改和编译。若习惯使用 VSCode 进行开发,可以安装 适用于 VSCode 的 ESP-IDF 扩展,这样可以更方便地进行开发和调试。 参考链接 https://www.espressif.com.cn/zh-hans/support/download/other-tools https://espressif.github.io/esp-launchpad/ https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb https://docs.espressif.com/projects/esp-idf/zh_CN/v5.4.1/esp32s3/get-started/linux-macos-setup.html https://github.com/espressif/vscode-esp-idf-extension

2025/6/5
articleCard.readMore

Mac mini 外接三方键盘如何微调音量

用 Mac mini 外接第三方键盘时,音量调节可能会让人感到痛苦。比如,Fn + F11 和 Fn + F12 这对音量调节快捷键可能没法用,而基于它们的微调组合键(Fn + Option + Shift + F11 和 Fn + Option + Shift + F12)更是想都别想。 我的工作键盘是 IKBC C87,虽然有个 Fn 键,但它和 Mac 键盘的 Fn 完全不是一回事。键盘自带的音量调节组合键只能一格一格地调节,听歌时还好,但在 Coding 时,这“一格”的音量有时就显得过于喧闹,让人无法沉浸式思考。微调音量成了刚需。 最终,Karabiner-Elements 拯救了我。 设置方法 只需将右 Ctrl 键映射为 Fn 键,就能像用苹果妙控键盘一样,正常使用各种基于 Fn 的快捷键,包括音量微调。 关于 Karabiner-Elements Karabiner-Elements 是一款强大的键盘映射工具,功能远不止映射单键这么简单。它还能: 创建自定义快捷键组合; 设计复杂的键盘规则; 根据不同应用程序或环境动态调整键盘映射。 此外,官网还提供了丰富的规则库,按需导入即可:https://ke-complex-modifications.pqrs.org/。 现在,我终于可以在 Coding 时享受“刚刚好”的背景音乐,而不用被“一格音量”的霸道支配。 希望能帮到和我一样有此困扰的你。

2025/5/8
articleCard.readMore

Java|小数据量场景的模糊搜索体验优化

在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。 场景 假设有一张表 t_course,数据量在三到四位数,字段 name 需要支持模糊搜索。用普通的 LIKE 语句,比如: SELECT id, name FROM t_course WHERE name LIKE '%2025数学高一下%'; 结果却查不到 2025年高一数学下学期。这就很尴尬了,用户体验直接拉胯。 方案探索 1. MySQL 全文索引 首先想到 MySQL 的全文索引,但要支持中文分词得改 ngram_token_size 配置,还得重启数据库。为了不动生产环境配置,果断放弃。 2. Elasticsearch 接着想到 Elasticsearch,但对这么简单的场景来说,未免有点“杀鸡用牛刀”。于是继续寻找更轻量的方案。 3. 自定义分词 + MySQL INSTR 最后想到一个“土办法”:先对用户输入进行分词,再用 MySQL 的 INSTR 函数匹配。简单粗暴,但很实用。 实现 分词工具 一开始用了 jcseg 分词库,写了个工具类: public class JcSegUtils { private static final SegmenterConfig CONFIG = new SegmenterConfig(true); private static final ADictionary DIC = DictionaryFactory.createSingletonDictionary(CONFIG); public static List<String> segment(String text) throws IOException { ISegment seg = ISegment.NLP.factory.create(CONFIG, DIC); seg.reset(new StringReader(text)); IWord word; List<String> result = new ArrayList<>(); while ((word = seg.next()) != null) { String wordText = word.getValue(); if (StringUtils.isNotBlank(wordText)) { result.add(wordText); } } return result; } } 本地测试一切正常,但部署到测试环境后,分词结果却变了!比如: 本地:[2025, 数学, 高一, 下] 测试环境:[2025, 数, 学, 高, 1, 下] 原因是 jcseg 在 jar 包中加载默认配置和词库时出问题了。网上的解决方案大多是外置词库,但我懒得折腾,决定自己撸个简易分词工具。 简易分词工具 最终实现如下: public class WordSegmentationUtils { private static final List<String> DICT; private static final String COURSE_SEARCH_KEYWORD_LIST = "数学,物理,化学,生物,地理,历史,政治,英语,语文,高中,高一,高二,高三"; static { DICT = new ArrayList<>(); for (int i = 2018; i <= 2099; i++) { DICT.add(String.valueOf(i)); } DICT.addAll(Arrays.asList(COURSE_SEARCH_KEYWORD_LIST.split(","))); } public static List<String> segment(String text) { if (StringUtils.isBlank(text)) { return new ArrayList<>(); } List<String> segments = new ArrayList<>(); segments.add(text); for (String word : DICT) { segments = segment(segments, word); } return segments; } private static List<String> segment(List<String> segments, String word) { List<String> newSegments = new ArrayList<>(); for (String segment : segments) { if (segment.contains(word)) { newSegments.add(word); String[] split = segment.split(word); for (String s : split) { if (StringUtils.isNotBlank(s)) { newSegments.add(s.trim()); } } } else { newSegments.add(segment); } } return newSegments; } } 这个工具基于一个简单的词典 DICT,按词典中的词对输入文本进行分割。比如: 输入:2025数学高一下 输出:[2025, 数学, 高一, 下] 效果验证 现在,无论用户输入以下哪种形式,都能成功匹配到 2025年高一数学下学期: 2025高一数学下 2025 高一 数学 数学高一2025 小结 这个方案虽然简单,但在小数据量场景下,性能和体验都能满足需求,且实现成本低。如果遇到特殊情况,可以通过动态更新词典来解决。 当然,这种“土办法”并不适合复杂场景。如果需求升级,可以再考虑 MySQL 全文索引或 Elasticsearch。 最后,自己一个人负责开发和运维就是任性!如果有团队一起评审,这方案可能早就被否了吧……额,达摩克利斯之剑高悬。

2025/4/23
articleCard.readMore

家里老人迷信医疗广告?我可能找到办法了

有没有这样一种奇妙体验:家里老人对你的忠告嗤之以鼻,却对网络医疗广告深信不疑?仿佛那些”三天速效”“纯天然”“祖传秘方”字样自带某种魔力,能让他们心甘情愿掏空钱包? 我爸就是这样。医院检查出肠道息肉后,他不愿接受正规治疗,却在网上找了个不知名的乡镇医院,买了一堆贵得离谱的中药。当我劝他去本市三甲医院时,我们吵了一架,不欢而散。 我百思不得其解:为什么长辈会信任网上随机广告,胜过亲生儿女的劝告?是年代差异让他们天然信任媒体?是搜索引擎大品牌的背书效应?还是他们骨子里相信”酒香也怕巷子深,神医总在小诊所”? 曾经,我尝试给他安装丁香医生,希望提供专业的医疗参考。结果他嫌上面评论太少,不够可信。转头却给我展示抖音上的”体外无痛胃肠检查”广告——零评论,明显广告标识。我内心:这双标技术堪称奥运冠军水平啊! 绝望之际,我发现了转机。 一天晚上,我爸向我展示他用”豆包”AI制作的视频。灵光乍现!我让他用豆包查询那个”神奇”的体外检查技术。豆包给出了客观分析,而他竟然接受了这个意见! 第二天他主动用豆包查询其他健康问题,我立刻顺水推舟:”以后不用上百度了,直接问豆包就好。”他居然欣然接受! 所以,我所谓的方法其实很简单:用 AI 助手替代浏览器和搜索引擎。无论是豆包、DeepSeek 还是其他 AI 产品,它们提供简洁明了的单一答案,不会展示五花八门的广告链接,也(暂时)不会被商业利益左右。 感谢技术进步,我终于不用在”爸,这是骗人的”和”儿子,你懂什么”之间无限循环了。 类似的救赎,tk 教主的经历显然更加硬核,但我可能来不及去建立那样的信任了:

2025/3/21
articleCard.readMore

iOS|解决 setBrightness 调节屏幕亮度不生效的问题

在包含视频播放功能的 App 中,一种常见的交互是在播放器界面的左侧上下滑动调节屏幕亮度,右侧上下滑动调节音量。我们的 iOS App 里也是这样设计的,但最近在测试过程中,发现亮度调节不生效了。 摸索之路 代码里面调节亮度的实现是这样的: - (void)setBrightnessUp { if ([UIScreen mainScreen].brightness >=1) { return; } [UIScreen mainScreen].brightness += 0.01; // ... } - (void)setBrightnessDown { if ([UIScreen mainScreen].brightness <=0) { return; } [UIScreen mainScreen].brightness -= 0.01; // ... } 这个实现在较早之前是没有问题的,那我首先想到比较可能是因为系统的更新,对这个 API 做了变更。于是先查阅了 UIKit/UIScreen/brightness 的官方文档,里面只提到了 brightness 属性只在 main screen 上被支持,取值范围是 [0.0, 1.0],以及亮度调节后,直到锁屏后才会失效——即使用户在锁屏之前已经关闭了 App。并没有看到什么值得特别留意的。 然后继续看代码里的 UIScreen.mainScreen,这个属性被标记为: API_DEPRECATED("Use a UIScreen instance found through context instead: i.e, view.window.windowScene.screen", ios(2.0, API_TO_BE_DEPRECATED), visionos(1.0, API_TO_BE_DEPRECATED)) 但当前在我使用的 SDK 18.2 版本中,这个属性应仍可正常使用。 在 Google 和 StackOverflow 找了一圈,大家讨论亮度调节不生效主要集中以下方面: 后台调用不生效; 模拟器上调节不生效; viewDidLoad and viewWillAppear 中调用不生效; 如何优雅地在 App 退出后恢复原有亮度; 也没有找到什么能匹配我的场景的解决方案。 加了一些日志,在调节亮度前后分别打印了 brightness 的值,发现它在调用 setBrightness 方法后并没有发生变化,也没有报错和告警,看起来就像是这个方法根本没有被调用一样。 也做了一些其它尝试,比如把调整亮度的代码显式调度到主线程、使用 view.window.windowScene.screen 替代 UIScreen.mainScreen 等,但都没有效果。 无奈之下,我问了 GitHub Copilot 一嘴,它的回答是这样的: 我按它的建议检查了权限,确认了不存在权限问题。 有点绝望之际,看到它提供的代码里调整亮度的粒度是 0.1,而我的代码里是 0.01,于是我尝试将粒度改为 0.1,然后奇迹发生了,亮度调节生效了。 这就有点匪夷所思了……于是我又尝试了其它的粒度值,结果如下: 0.01,不生效; 0.02,不生效; 0.03 及以上,生效,但是从输出可以看到,实际调整后的亮度值都是 0.05 的倍数,即 0.05、0.1、0.15、0.2……,而不是 0.03、0.06、0.09、0.12…… 我找到安装了以前老版本 App 的一个老平板(iOS 10.3.3),在上面测试了一下,发现在这个版本上,0.01 的调节粒度是可以生效的。 也就是说,在 iOS (10.3.3, 18.2) 之间靠近后者的某个版本上,[UIScreen mainScreen].brightness 的调节粒度发生了变化,由 0.01 变为了 0.05。 至此破案了,顺便吐槽一下,官方文档里对此毫无提及,实在是……略坑。 参考 https://developer.apple.com/documentation/uikit/uiscreen/brightness?language=objc https://stackoverflow.com/questions/54229300/not-able-to-set-brightness-when-app-enter-in-background-can-any-one-have-any-id https://stackoverflow.com/questions/12362885/ios-uiscreen-setbrightness-doesnt-work https://stackoverflow.com/questions/61765014/why-cannot-i-set-uiscreen-main-brightness

2025/2/26
articleCard.readMore