MoreRSS

site iconMaZhuang | 马壮修改

博客名:码志。仰慕「优雅编码的艺术」。坚信熟能生巧,努力改变人生。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

MaZhuang | 马壮的 RSS 预览

Java|FreeMarker 复用 layout

2025-08-30 00:00:00

项目里的页面一多,重复的页面布局就不可避免地冒了出来,作为程序员,消除重复,义不容辞。那么,今天就来聊聊如何在 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 目录。

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

2025-06-05 00:00:00

前一阵子在百度 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 [email protected]: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 扩展,这样可以更方便地进行开发和调试。

参考链接

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

2025-05-08 00:00:00

用 Mac mini 外接第三方键盘时,音量调节可能会让人感到痛苦。比如,Fn + F11Fn + F12 这对音量调节快捷键可能没法用,而基于它们的微调组合键(Fn + Option + Shift + F11Fn + 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 时享受“刚刚好”的背景音乐,而不用被“一格音量”的霸道支配。

希望能帮到和我一样有此困扰的你。

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

2025-04-23 00:00:00

在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。

场景

假设有一张表 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-03-21 00:00:00

有没有这样一种奇妙体验:家里老人对你的忠告嗤之以鼻,却对网络医疗广告深信不疑?仿佛那些”三天速效”“纯天然”“祖传秘方”字样自带某种魔力,能让他们心甘情愿掏空钱包?

我爸就是这样。医院检查出肠道息肉后,他不愿接受正规治疗,却在网上找了个不知名的乡镇医院,买了一堆贵得离谱的中药。当我劝他去本市三甲医院时,我们吵了一架,不欢而散。

我百思不得其解:为什么长辈会信任网上随机广告,胜过亲生儿女的劝告?是年代差异让他们天然信任媒体?是搜索引擎大品牌的背书效应?还是他们骨子里相信”酒香也怕巷子深,神医总在小诊所”?

曾经,我尝试给他安装丁香医生,希望提供专业的医疗参考。结果他嫌上面评论太少,不够可信。转头却给我展示抖音上的”体外无痛胃肠检查”广告——零评论,明显广告标识。我内心:这双标技术堪称奥运冠军水平啊!

绝望之际,我发现了转机。

一天晚上,我爸向我展示他用”豆包”AI制作的视频。灵光乍现!我让他用豆包查询那个”神奇”的体外检查技术。豆包给出了客观分析,而他竟然接受了这个意见!

第二天他主动用豆包查询其他健康问题,我立刻顺水推舟:”以后不用上百度了,直接问豆包就好。”他居然欣然接受!

所以,我所谓的方法其实很简单:用 AI 助手替代浏览器和搜索引擎。无论是豆包、DeepSeek 还是其他 AI 产品,它们提供简洁明了的单一答案,不会展示五花八门的广告链接,也(暂时)不会被商业利益左右。

感谢技术进步,我终于不用在”爸,这是骗人的”和”儿子,你懂什么”之间无限循环了。

类似的救赎,tk 教主的经历显然更加硬核,但我可能来不及去建立那样的信任了:

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

2025-02-26 00:00:00

在包含视频播放功能的 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 找了一圈,大家讨论亮度调节不生效主要集中以下方面:

也没有找到什么能匹配我的场景的解决方案。

加了一些日志,在调节亮度前后分别打印了 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。

至此破案了,顺便吐槽一下,官方文档里对此毫无提及,实在是……略坑。

参考