<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ThxFly (Shooting Fly)</title>
    <link>https://ruby-china.org/ThxFly</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>开源 BI 工具 - Metabase 实战指南</title>
      <description>&lt;h2 id="Metabase 实战指南"&gt;Metabase 实战指南&lt;/h2&gt;
&lt;p&gt;Metabase 是一个开源的 BI 工具，特点是上手简单，界面友好，使用符合直觉。下面讲讲怎么用它搭建数据看板。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/58b42941-53b6-485e-a02a-1445f44fcb6a.png!large" title="" alt="Metabase 界面示例"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="整体流程一览"&gt;整体流程一览&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;连上数据库&lt;/strong&gt; —— 把 Metabase 跟你现有的数据源打通&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;建集合&lt;/strong&gt; —— 相当于文件夹，把东西归类放好&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;建模型&lt;/strong&gt;（推荐）—— 把常用的表封装成模型，方便非技术人员理解和使用&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;做指标&lt;/strong&gt; —— 在集合里创建「问题」，也就是你要查询的数据&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;搭看板&lt;/strong&gt; —— 把指标组装成一个可交互的监控面板&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;置顶发布&lt;/strong&gt; —— 让重要的看板更容易被找到&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="第一步：连接数据库"&gt;第一步：连接数据库&lt;/h2&gt;
&lt;p&gt;首先得让 Metabase 认识你的数据。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;点右上角 &lt;strong&gt;齿轮图标&lt;/strong&gt; → &lt;strong&gt;管理设置&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;左侧选 &lt;strong&gt;数据库&lt;/strong&gt; → &lt;strong&gt;添加数据库&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;填写连接信息：

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;显示名称&lt;/strong&gt;：起个好认的名字，比如「生产数据库」&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;数据库类型&lt;/strong&gt;：MySQL、PostgreSQL、ClickHouse、StarRocks 等&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host、Port、数据库名、用户名、密码&lt;/strong&gt;：按实际情况填写&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;建议打开两个选项：

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Sync schema&lt;/strong&gt;：自动同步表结构&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cache field values&lt;/strong&gt;：缓存字段值，筛选器加载会快很多&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点保存，状态显示绿色即表示连接成功&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="第二步：创建集合"&gt;第二步：创建集合&lt;/h2&gt;
&lt;p&gt;集合相当于文件夹，可以帮你把不同业务的数据分门别类。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;点左上角 &lt;strong&gt;「+ 新建」&lt;/strong&gt; → &lt;strong&gt;「集合」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;命名建议：

&lt;ul&gt;
&lt;li&gt;一级目录按业务域划分，如「交易」「用户」「财务」&lt;/li&gt;
&lt;li&gt;二级目录按主题划分，如「交易/日常监控」「交易/活动复盘」&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;想建子目录？先进去该集合，再按同样方法创建即可&lt;/li&gt;
&lt;li&gt;还可以设置权限——点右上角 &lt;strong&gt;「...」&lt;/strong&gt; → &lt;strong&gt;「编辑集合」&lt;/strong&gt; 即可管理谁能看、谁能改&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/1c90620f-e7de-4d77-8cfe-61ceb07b052f.png!large" height="300px" alt="创建集合界面"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第三步：创建模型（推荐）"&gt;第三步：创建模型（推荐）&lt;/h2&gt;
&lt;p&gt;这是 Metabase 区别于 Superset 的特色概念。&lt;strong&gt;模型 (Model)&lt;/strong&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;li&gt;可以简单理解为 SQL 里视图的概念&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="什么时候用模型？"&gt;什么时候用模型？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;这张表会被频繁复用&lt;/li&gt;
&lt;li&gt;字段名过于技术化，需要给业务人员解释（如 &lt;code&gt;stts_cd&lt;/code&gt; → 「订单状态」）&lt;/li&gt;
&lt;li&gt;想统一管理某类数据的刷新策略&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="怎么创建模型？"&gt;怎么创建模型？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;进入目标集合 → &lt;strong&gt;「+ 新建」&lt;/strong&gt; → &lt;strong&gt;「模型」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;选择数据源（一张表，或写一个 SQL 定义虚拟表）&lt;/li&gt;
&lt;li&gt;添加元数据：

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;显示名称&lt;/strong&gt;：业务友好的名字&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;描述&lt;/strong&gt;：这张表是干什么用的&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;字段标签&lt;/strong&gt;：把技术字段名改成业务名（如 &lt;code&gt;ord_stts&lt;/code&gt; 改成「订单状态」）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;字段类型&lt;/strong&gt;：告诉 Metabase 哪个是日期、哪个是金额&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;过滤/分组&lt;/strong&gt;：标记哪些字段适合筛选、哪些适合分组&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;保存&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;把常用的表（如订单表、用户表）做成模型，后续业务人员用起来会顺手很多。如需修改，点右上角 &lt;strong&gt;「...」&lt;/strong&gt; → &lt;strong&gt;「编辑模型详情」&lt;/strong&gt; 即可。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/4350bcbb-acd3-4c3e-918f-317c863a9f50.png!large" title="" alt="创建模型界面"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第四步：创建指标（问题）"&gt;第四步：创建指标（问题）&lt;/h2&gt;
&lt;p&gt;在 Metabase 里，你创建的查询都叫「问题」(Question)，也就是我们说的「指标」。&lt;/p&gt;

&lt;p&gt;先进到目标集合，点 &lt;strong&gt;「+ 新建」&lt;/strong&gt; → &lt;strong&gt;「问题」&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="方式 A：图形化拖拽"&gt;方式 A：图形化拖拽&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;选一张表，或者直接选一个模型（如果有的话）&lt;/li&gt;
&lt;li&gt;配置数据：点击 &lt;strong&gt;「+ 筛选」&lt;/strong&gt; 添加条件，如「状态 = 已支付」；点击 &lt;strong&gt;「+ 汇总」&lt;/strong&gt; 选择计算方式，如计数、求和、平均值；按需要的维度分组，如「创建时间 - 天」&lt;/li&gt;
&lt;li&gt;配置可视化：右上角切换图表类型（折线图、柱状图、饼图、数字卡等）&lt;/li&gt;
&lt;li&gt;点 &lt;strong&gt;「可视化」&lt;/strong&gt; 预览效果，点 &lt;strong&gt;「保存」&lt;/strong&gt;，记得选对集合，名字起清楚，如「每日支付订单量」&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/a0751204-d5ba-4f6d-a41a-7719d2e02bb0.png!large" width="500px" alt="数据配置示例"&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/3d48cca3-2410-4c70-9ffb-5ea13e5c6059.png!large" width="500px" alt="图表类型示例"&gt;&lt;/p&gt;
&lt;h3 id="方式 B：写 SQL"&gt;方式 B：写 SQL&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;点 &lt;strong&gt;「+ 新建」&lt;/strong&gt; → &lt;strong&gt;「SQL 查询」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;选择数据库（第一步配置的那个）&lt;/li&gt;
&lt;li&gt;写你的 SQL。如果需要动态参数，用 &lt;code&gt;{{变量名}}&lt;/code&gt; 语法：
&lt;code&gt;sql
SELECT COUNT(*) AS total_users
FROM users
WHERE {{created_at_filter}}
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;右侧边栏可以配置变量类型：文本、数值、日期、布尔值……如果是字段筛选器，还需映射到具体字段&lt;/li&gt;
&lt;li&gt;写完点 &lt;strong&gt;「运行」&lt;/strong&gt;（或 &lt;code&gt;Cmd + Enter&lt;/code&gt;），点 &lt;strong&gt;「保存」&lt;/strong&gt;，选好集合和名字&lt;/li&gt;
&lt;li&gt;然后同方式 A 配置图表类型&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/0dc51b99-2113-4786-9be2-cc22ad6a8241.png!large" title="" alt="SQL 查询界面"&gt;&lt;/p&gt;

&lt;p&gt;不管用哪种方式，保存好的「问题」就是可复用的指标了，后面直接拖到看板里就能用。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第五步：搭建仪表板（看板）"&gt;第五步：搭建仪表板（看板）&lt;/h2&gt;
&lt;p&gt;把指标组合在一起，配上过滤条件，就是一个可以看的监控大盘了。&lt;/p&gt;
&lt;h3 id="创建看板"&gt;创建看板&lt;/h3&gt;
&lt;p&gt;进入目标集合 → &lt;strong&gt;「+ 新建」&lt;/strong&gt; → &lt;strong&gt;「仪表板」&lt;/strong&gt;，起个名字，如「交易核心监控大盘」。&lt;/p&gt;
&lt;h3 id="添加指标"&gt;添加指标&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;点看板右上角的 &lt;strong&gt;➕&lt;/strong&gt; 进入编辑模式&lt;/li&gt;
&lt;li&gt;勾选想放的指标&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="调整布局"&gt;调整布局&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;拖拽&lt;/strong&gt;：按住卡片标题拖到想放的位置&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;缩放&lt;/strong&gt;：拖动右下角调整大小，支持跨列&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;排版建议&lt;/strong&gt;：关键数字放顶部，趋势图放中间，明细表放底部&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/7c510647-5c0d-4f02-a40c-cde0bc8f335a.png!large" title="" alt="看板编辑界面"&gt;&lt;/p&gt;
&lt;h3 id="配置过滤条件（重点！）"&gt;配置过滤条件（重点！）&lt;/h3&gt;
&lt;p&gt;这是让看板活起来的关键。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;编辑模式下，点顶部工具栏的 &lt;strong&gt;漏斗图标&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;添加一个全局过滤器：

&lt;ul&gt;
&lt;li&gt;选择类型：如时间范围、下拉列表、搜索框&lt;/li&gt;
&lt;li&gt;命名：如「业务日期」「城市」&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;把过滤器跟卡片关联起来：

&lt;ul&gt;
&lt;li&gt;点过滤器下方的箭头 → &lt;strong&gt;「编辑关联」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;把全局的「时间」映射到卡片 A 的 &lt;code&gt;created_at&lt;/code&gt;、卡片 B 的 &lt;code&gt;paid_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;注意：SQL 创建的卡片需要 SQL 中包含 &lt;code&gt;{{变量名}}&lt;/code&gt; 才能生效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果某张卡只需独立过滤，不跟全局联动，可在卡片的 &lt;strong&gt;「...」&lt;/strong&gt; → &lt;strong&gt;「编辑」&lt;/strong&gt; 中单独设置&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="完成"&gt;完成&lt;/h3&gt;
&lt;p&gt;点右下角 &lt;strong&gt;「完成编辑」&lt;/strong&gt; 退出编辑模式。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/30b065cc-1437-489d-ae5c-64b3b9f94d7c.png!large" title="" alt="过滤器配置示例"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第六步：置顶与分享"&gt;第六步：置顶与分享&lt;/h2&gt;
&lt;p&gt;辛辛苦苦做的看板，可别让它淹没在列表里。&lt;/p&gt;
&lt;h3 id="置顶"&gt;置顶&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在看板页面点右上角 &lt;strong&gt;「...」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;选择 &lt;strong&gt;「置顶」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;选择位置：

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;集合顶部&lt;/strong&gt;：仅在当前集合内置顶&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;全局置顶&lt;/strong&gt;：所有人登录后首页都能看到（需管理员权限）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="验证"&gt;验证&lt;/h3&gt;
&lt;p&gt;回到首页或集合页，确认看板出现在最上方的「已置顶」区域。&lt;/p&gt;
&lt;h3 id="分享（可选）"&gt;分享（可选）&lt;/h3&gt;
&lt;p&gt;点 &lt;strong&gt;「分享按钮」&lt;/strong&gt;，可以生成公开链接、导出 PDF 或设置邮件订阅。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="真实示例"&gt;真实示例&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/8341d41d-5084-44e1-b662-8486f6509a33.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/00827756-a35a-421e-8dcf-b7d038216c3a.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="常用快捷键"&gt;常用快捷键&lt;/h2&gt;&lt;h3 id="全局"&gt;全局&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建问题&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c&lt;/code&gt; &amp;gt; &lt;code&gt;q&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建 SQL 查询&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c&lt;/code&gt; &amp;gt; &lt;code&gt;n&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建仪表板&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c&lt;/code&gt; &amp;gt; &lt;code&gt;d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建集合&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c&lt;/code&gt; &amp;gt; &lt;code&gt;f&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建模型&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c&lt;/code&gt; &amp;gt; &lt;code&gt;m&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建指标 (Metric)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c&lt;/code&gt; &amp;gt; &lt;code&gt;k&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览数据库&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览模型&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;m&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览指标&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;k&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开个人空间&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;p&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开回收站&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;t&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换侧边栏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进入管理后台&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;a&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进入用户设置&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;u&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;返回首页&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g&lt;/code&gt; &amp;gt; &lt;code&gt;h&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="仪表板"&gt;仪表板&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进入编辑模式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加过滤器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加问题侧边栏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加图形化问题&lt;/td&gt;
&lt;td&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加 SQL 问题&lt;/td&gt;
&lt;td&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保存仪表板&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换仪表板信息&lt;/td&gt;
&lt;td&gt;&lt;code&gt;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;收藏/书签看板&lt;/td&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换 Tab&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除仪表板&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl/Cmd&lt;/code&gt; + &lt;code&gt;Backspace&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="问题/查询"&gt;问题/查询&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换到编辑器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开筛选器下拉&lt;/td&gt;
&lt;td&gt;&lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开汇总侧边栏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;收藏/书签问题&lt;/td&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开问题详情&lt;/td&gt;
&lt;td&gt;&lt;code&gt;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;刷新数据&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换可视化类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换可视化设置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换图表类型菜单&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运行查询&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Cmd + Enter&lt;/code&gt;（Mac） / &lt;code&gt;Ctrl + Enter&lt;/code&gt;（Windows）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保存&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Cmd + S&lt;/code&gt;（Mac） / &lt;code&gt;Ctrl + S&lt;/code&gt;（Windows）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除问题&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl/Cmd&lt;/code&gt; + &lt;code&gt;Backspace&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="管理员"&gt;管理员&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换管理后台 Tab&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="开源版 vs 商业版"&gt;开源版 vs 商业版&lt;/h2&gt;
&lt;p&gt;根据 &lt;a href="https://www.metabase.com/pricing/" rel="nofollow" target="_blank" title=""&gt;官方定价页面&lt;/a&gt; 的信息整理：&lt;/p&gt;
&lt;h3 id="开源版（免费）"&gt;开源版（免费）&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;&lt;strong&gt;数据查询&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Query Builder（图形化查询）&lt;/td&gt;
&lt;td&gt;可视化拖拽构建查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅ SQL Editor&lt;/td&gt;
&lt;td&gt;支持原生 SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅ Models（模型）&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;Query and model caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ Metabot AI&lt;/td&gt;
&lt;td&gt;需付费 ($100/月起)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;可视化&lt;/strong&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;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅ X-Ray 自动报告&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;需配置 SMTP，带 Metabase 水印&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Slack 订阅&lt;/td&gt;
&lt;td&gt;带 Metabase 水印&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;⚠️ PDF 导出&lt;/td&gt;
&lt;td&gt;带 Metabase 水印&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;strong&gt;权限管理&lt;/strong&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;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ 沙箱 (Sandbox)&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;认证&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Google Auth&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;⚠️ LDAP&lt;/td&gt;
&lt;td&gt;仅支持基础认证（登录）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ SAML / SSO&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ JWT&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ SCIM 自动账号同步&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;嵌入&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Guest Embeds&lt;/td&gt;
&lt;td&gt;仅基础嵌入，带 "Powered by Metabase" 水印&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ White-label（去水印）&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ 交互式嵌入 (Full-app)&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;组织&lt;/strong&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;❌ 官方集合 (Official Collections)&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ 审核问题 (Moderated questions)&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ 验证模型 (Verified models)&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 20+ 数据库连接器&lt;/td&gt;
&lt;td&gt;MySQL、PostgreSQL、ClickHouse 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✅ CSV 上传&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;部署&lt;/strong&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;需 Starter 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;❌ Serialization（配置导出/导入）&lt;/td&gt;
&lt;td&gt;需 Pro 版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;社区论坛&lt;/td&gt;
&lt;td&gt;&lt;a href="https://discourse.metabase.com" rel="nofollow" target="_blank" title=""&gt;discourse.metabase.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;许可证&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AGPL&lt;/td&gt;
&lt;td&gt;商用需注意开源协议&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="商业版 (Starter / Pro / Enterprise)"&gt;商业版 (Starter / Pro / Enterprise)&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;&lt;strong&gt;Starter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$100/月 + $6/月/人&lt;/td&gt;
&lt;td&gt;全托管云版，含官方支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$575/月 + $12/月/人&lt;/td&gt;
&lt;td&gt;细粒度权限、White-label、嵌入、Usage Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20,000/年 起&lt;/td&gt;
&lt;td&gt;大客户专属成功工程师、合规需求&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;小团队用开源版就够用了；想把看板嵌入到自家产品里，需要买商业版。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="关于汉化的一个问题"&gt;关于汉化的一个问题&lt;/h2&gt;
&lt;p&gt;Metabase 自带中文翻译，但有些翻译比较生硬，甚至让人误解。比如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;某些功能名翻译不够准确&lt;/li&gt;
&lt;li&gt;某些词过于直译，看不懂&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果你们的 Metabase 启用了中文界面，创建用户时，&lt;strong&gt;名字写全名，姓留空&lt;/strong&gt;。这样在仪表板、问题等处显示的就是完整名字，不会出现「小明 李」这类情况。&lt;/p&gt;

&lt;p&gt;热心的小伙伴可以创建 PR 给到官方，帮忙优化翻译。&lt;/p&gt;
&lt;h2 id="其他"&gt;其他&lt;/h2&gt;
&lt;p&gt;我也用过 Superset，对比下来 Metabase 用起来更舒服，很多操作比较符合直觉，看起来是个更不错的 BI 工具。&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Mon, 02 Mar 2026 14:45:19 +0800</pubDate>
      <link>https://ruby-china.org/topics/44500</link>
      <guid>https://ruby-china.org/topics/44500</guid>
    </item>
    <item>
      <title>Rails 技术实战之 AI 找货小助手设计</title>
      <description>&lt;h2 id="AI 找货助手技术实现指南"&gt;AI 找货助手技术实现指南&lt;/h2&gt;
&lt;p&gt;最近阿里云千问的"点奶茶"技能很火，碾压了微信红包的 AI 玩法。这波 AI 热潮下，我们在 APP 里也做了一个 AI 找货功能。&lt;/p&gt;

&lt;p&gt;用户搜"温润的手镯"，传统搜索只认"温润"和"手镯"两个字。但用户真正想要的是和田玉——因为"温润"是和田玉的特征。&lt;/p&gt;

&lt;p&gt;这篇讲讲怎么用向量搜索 + AI 对话，让系统理解用户真正想要什么。淘宝、京东、美团、携程都有类似功能，我们这个还比较粗浅，讲个大概，体验看起来还有待优化，还请包涵。&lt;/p&gt;
&lt;h2 id="需求预览"&gt;需求预览&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/2be068bc-c934-4141-a36b-bc6ff941a9cf.png!large" width="40%"&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/6327870c-2c7f-49b4-b12d-bedad2ed38e6.png!large" width="40%"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="目录"&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5" title=""&gt;核心概念&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%B8%80%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84%E6%80%BB%E8%A7%88" title=""&gt;一、技术架构总览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%BA%8Csse-%E4%BA%8B%E4%BB%B6%E8%AE%BE%E8%AE%A1" title=""&gt;二、SSE 事件设计&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%B8%89dify-%E5%AF%B9%E8%AF%9D%E6%B5%81%E8%AE%BE%E8%AE%A1%E8%AF%A6%E8%A7%A3" title=""&gt;三、Dify 对话流设计详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%9B%9B%E5%90%91%E9%87%8F%E6%90%9C%E7%B4%A2%E6%8A%80%E6%9C%AF%E8%AF%A6%E8%A7%A3" title=""&gt;四、向量搜索技术详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%BA%94%E5%95%86%E5%93%81%E8%AF%AD%E4%B9%89%E6%90%9C%E7%B4%A2%E6%A8%A1%E5%9D%97%E8%AF%A6%E8%A7%A3" title=""&gt;五、商品语义搜索模块详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%85%ADknn-%E6%8C%89%E5%88%86%E7%B1%BB%E9%85%8D%E7%BD%AE" title=""&gt;六、KNN 按分类配置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%B8%83%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5" title=""&gt;七、常见问题与故障排查&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="核心概念"&gt;核心概念&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;开源 LLM 应用开发平台，我们用它做工作流编排。Dify 部署成独立服务，Rails 后端通过 HTTP API 调用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Sent Events，服务器主动推送数据给前端，比 WebSocket 简单，适合这种单向实时推送的场景。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;向量搜索&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;把文本转成 1024 维向量，算向量相似度来找商品。比如"温润的手镯"能匹配到"和田玉手镯"。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KNN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;K 最近邻算法，在向量空间里找最相似的 K 个商品。K 越大结果越多，但也越慢。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embedding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;把文本转成向量的过程，语义相近的文本向量也相近。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HNSW&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一种高效的向量索引算法，ES 8.x 原生支持，用来加速 KNN 搜索。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="一、技术架构总览"&gt;一、技术架构总览&lt;/h2&gt;&lt;h3 id="1.1 整体架构图"&gt;1.1 整体架构图&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/dbcdcb6e-dcd6-464a-b80c-e5fb40bc937e.png!large" width="80%"&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;整体流程&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;用户在前端输入需求 → SSE 建立长连接 → Rails 把请求转给 Dify → Dify 识别意图、提取参数 → Dify 调用 Rails 的搜索接口 → Rails 生成查询向量 → ES 做 KNN 向量搜索 → 结果沿路返回 → 前端展示。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;几个关键点&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前端和 Rails 之间用 SSE，服务器主动推送，不用轮询。&lt;/li&gt;
&lt;li&gt;Dify 是外部服务，Rails 只负责转发和调用搜索接口。&lt;/li&gt;
&lt;li&gt;搜索用的是向量搜索，不是传统的关键词匹配。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;核心配置&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;Dify API 地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DIFY_API_BASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dify 服务的 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dify API Key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DIFY_CHATFLOW_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调用工作流的认证密钥&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;核心接口&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;SSE 对话接口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /api/v1/chat/stream&lt;/code&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;code&gt;POST /api/v1/products/ai_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dify 工作流&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="1.2 商品列表接口"&gt;1.2 商品列表接口&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/cb249fea-8de7-43fe-bbd1-bc4126dc6fef.png!large" width="80%"&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /api/v1/products&lt;/code&gt; 这个接口支持两种搜索方式：普通搜索（传 q、category_id、price_min/max 等参数），和 ai_search_id 搜索（传之前搜索的 ID，复用搜索参数）。&lt;/p&gt;

&lt;p&gt;使用场景是这样的：用户在对话里看到商品列表，想看更多，点"查看更多"按钮，前端把之前的 ai_search_id 传过来，后端直接从数据库捞出之前的搜索参数，做分页查询。&lt;/p&gt;
&lt;h3 id="1.3 数据流转"&gt;1.3 数据流转&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/f05cd6c7-8ab3-475a-a8cf-1729eff4cec0.png!large" width="100%"&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;两个关键点&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;商品向量是上架时生成的，存在 ES 里；查询向量是每次搜索实时生成的。&lt;/li&gt;
&lt;li&gt;向量生成失败了怎么办？降级处理，返回空结果或者转关键词搜索。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="1.4 技术栈"&gt;1.4 技术栈&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;工作流引擎&lt;/td&gt;
&lt;td&gt;Dify&lt;/td&gt;
&lt;td&gt;最新版，做 AI 流程编排&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;通义千问 qwen-plus-latest&lt;/td&gt;
&lt;td&gt;意图理解、参数提取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索引擎&lt;/td&gt;
&lt;td&gt;Elasticsearch 8.x&lt;/td&gt;
&lt;td&gt;商品索引 + KNN 向量搜索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端框架&lt;/td&gt;
&lt;td&gt;Ruby on Rails 8.x&lt;/td&gt;
&lt;td&gt;业务逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="二、SSE 事件设计"&gt;二、SSE 事件设计&lt;/h2&gt;&lt;h3 id="2.1 事件流程图"&gt;2.1 事件流程图&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/b7b1e0cf-fde5-48f5-b97e-163c07e9653d.png!large" width="100%"&gt;&lt;/p&gt;
&lt;h3 id="2.2 事件类型"&gt;2.2 事件类型&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;&lt;code&gt;system_message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接建立后、stream_end 前&lt;/td&gt;
&lt;td&gt;系统提示，比如"正在匹配货品中"，或者错误提示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message_start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第一次收到 Dify 的 message&lt;/td&gt;
&lt;td&gt;告诉前端准备接收消息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;收到 Dify 的 message&lt;/td&gt;
&lt;td&gt;真正推送内容，可能拆成多个片段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thought&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dify 节点开始处理&lt;/td&gt;
&lt;td&gt;显示"正在处理：xxx"，让用户知道进度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message_end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;收到 Dify 的 message_end&lt;/td&gt;
&lt;td&gt;一条消息发送完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stream_end&lt;/code&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;strong&gt;msg_type 的取值&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;message_start 和 message 的 msg_type：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dify 返回 &lt;code&gt;PRODUCT_CARD&lt;/code&gt; → &lt;code&gt;product_card&lt;/code&gt;（商品卡片）&lt;/li&gt;
&lt;li&gt;Dify 返回 &lt;code&gt;CLARIFY_CARD&lt;/code&gt; → &lt;code&gt;clarify_card&lt;/code&gt;（追问卡片）&lt;/li&gt;
&lt;li&gt;Dify 返回 &lt;code&gt;TEXT&lt;/code&gt; → &lt;code&gt;text&lt;/code&gt;（普通文本）&lt;/li&gt;
&lt;li&gt;其他情况默认 &lt;code&gt;text&lt;/code&gt;，结构化内容可能是 &lt;code&gt;blocks&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;system_message 的 msg_type：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;show_thinking&lt;/code&gt;：显示思考过程&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hide_thinking&lt;/code&gt;：隐藏思考过程&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text&lt;/code&gt;：普通系统文本&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retry&lt;/code&gt;：可重试的错误&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2.3 事件数据格式"&gt;2.3 事件数据格式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;message_start&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message_start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msg_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msg_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"task_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;message&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msg_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"这是消息内容"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"task_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;message_end&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message_end"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msg_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;stream_end&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stream_end"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"conversation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;system_message&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system_message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msg_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"正在匹配货品中"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"user_message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;thought&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thought"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msg_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"正在处理：属性提取"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"task_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.4 事件顺序和处理规范"&gt;2.4 事件顺序和处理规范&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;标准顺序&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;连接建立后，先发 &lt;code&gt;system_message&lt;/code&gt;（show_thinking）告诉用户"正在匹配"&lt;/li&gt;
&lt;li&gt;收到 Dify 的 message，第一次发 &lt;code&gt;message_start&lt;/code&gt;，然后发 &lt;code&gt;message&lt;/code&gt;（可能多次）&lt;/li&gt;
&lt;li&gt;收到 &lt;code&gt;node_started&lt;/code&gt; 时可以发 &lt;code&gt;thought&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;收到 &lt;code&gt;message_end&lt;/code&gt; 时发 &lt;code&gt;message_end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;有系统消息（错误或提示）发 &lt;code&gt;system_message&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;最后发 &lt;code&gt;stream_end&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;错误处理&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;错误不走单独的 error 事件，统一用 &lt;code&gt;system_message&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;msg_type&lt;/code&gt; 是 &lt;code&gt;retry&lt;/code&gt; 表示可重试的错误&lt;/li&gt;
&lt;li&gt;不管成功失败，最后都会发 &lt;code&gt;stream_end&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;前端注意事项&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;message&lt;/code&gt; 是流式的，前端要累积内容直到收到 &lt;code&gt;message_end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;根据 &lt;code&gt;msg_type&lt;/code&gt; 决定怎么渲染&lt;/li&gt;
&lt;li&gt;收到 &lt;code&gt;stream_end&lt;/code&gt; 就关闭连接&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="三、Dify 对话流设计详解"&gt;三、Dify 对话流设计详解&lt;/h2&gt;&lt;h3 id="3.1 工作流核心结构"&gt;3.1 工作流核心结构&lt;/h3&gt;
&lt;p&gt;我们用 Dify 的节点式编排来做对话流，核心节点如下：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;节点类型&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;接收用户输入和认证信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Question Classifier&lt;/td&gt;
&lt;td&gt;意图识别，分成 4 种意图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;属性提取，把用户输入转成结构化参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Else&lt;/td&gt;
&lt;td&gt;判断要不要追问用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;生成追问话术和选项按钮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP Request&lt;/td&gt;
&lt;td&gt;调用后端搜索接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;结果合并，各分支互斥，只返回其中一个结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;工作流里的变量：环境变量 &lt;code&gt;HTTP_DOMAIN&lt;/code&gt; 存 API 域名。&lt;/p&gt;
&lt;h3 id="3.2 Start 节点"&gt;3.2 Start 节点&lt;/h3&gt;
&lt;p&gt;起点，接收前端传来的变量：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;intent&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;用户意图标识&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;authorization&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Bearer Token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;version_date&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;版本日期（可选）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="3.3 意图识别"&gt;3.3 意图识别&lt;/h3&gt;
&lt;p&gt;用 Dify 的 Question Classifier 节点，把用户请求分成 4 类：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;分类 ID&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;例子&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;找货意图&lt;/td&gt;
&lt;td&gt;"找翡翠手镯"、"想买和田玉"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;知识问答&lt;/td&gt;
&lt;td&gt;"怎么鉴别翡翠 A 货"、"如何保养和田玉"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;App 问答&lt;/td&gt;
&lt;td&gt;"怎么注册账号"、"如何发布商品"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&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;模型配置：通义千问 qwen-plus-latest，Temperature 设 0.3 保证分类稳定，对话记忆保留最近 10 轮。&lt;/p&gt;
&lt;h3 id="3.4 属性提取"&gt;3.4 属性提取&lt;/h3&gt;
&lt;p&gt;从用户输入里提取搜索参数，用 LLM 节点，模型也是通义千问，Temperature 0.3。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;q 字段（核心搜索描述）&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;要包含明确的商品品类词&lt;/li&gt;
&lt;li&gt;50-100 字的自然语言描述&lt;/li&gt;
&lt;li&gt;有些默认规则：用户说"玉石"没具体说是和田玉还是翡翠，默认当和田玉处理；说"高货"默认当翡翠处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;价格范围&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;price_min&lt;/code&gt; 只有满足"品类 + 高价值信号 + 高价值形态 + 无瑕疵"时才推断。比如用户说"高品质玻璃种翡翠手镯，无纹无裂"，才可能推断最低价。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;price_max&lt;/code&gt; 按用户字面描述来，用户说"5000 元左右"就设 5000。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;分类映射&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1=&amp;gt;翡翠，2=&amp;gt;玉石，3=&amp;gt;钻石，4=&amp;gt;彩宝，39=&amp;gt;书画，40=&amp;gt;黄金，107=&amp;gt;黄金饰品，109=&amp;gt;文玩古玩，110=&amp;gt;钱币邮票&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;输出字段&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;必填&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;q&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;核心搜索描述&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;price_min/max&lt;/td&gt;
&lt;td&gt;number/null&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;价格范围（元）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;category_id&lt;/td&gt;
&lt;td&gt;number/null&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;商品分类 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inner_circle_size_min/max&lt;/td&gt;
&lt;td&gt;number/null&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;圈口尺寸（mm）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;heat_min/max&lt;/td&gt;
&lt;td&gt;number/null&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;参与热度范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is_uncertain&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是否模糊需追问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;has_discount&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是否要优惠商品&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;negative_filters&lt;/td&gt;
&lt;td&gt;array[string]&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;h3 id="3.5 条件判断"&gt;3.5 条件判断&lt;/h3&gt;
&lt;p&gt;检查 &lt;code&gt;is_uncertain&lt;/code&gt; 字段，&lt;code&gt;true&lt;/code&gt; 就进入追问分支，&lt;code&gt;false&lt;/code&gt; 直接搜索。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;触发追问的例子&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"想买个手镯" → 缺少价格、材质，追问&lt;/li&gt;
&lt;li&gt;"5000 元的" → 缺少商品类型，追问&lt;/li&gt;
&lt;li&gt;"翡翠手镯，5000-8000 元" → 信息完整，直接搜&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3.6 生成追问"&gt;3.6 生成追问&lt;/h3&gt;
&lt;p&gt;当需要追问时，用 LLM 生成话术和选项。Temperature 设 0.7 高一点，让回答更有创造性。&lt;/p&gt;

&lt;p&gt;生成规则：&lt;code&gt;message&lt;/code&gt; 字段要共情 + 归因 + 引导，&lt;code&gt;suggested_questions&lt;/code&gt; 至少 3 个肯定式选项。&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"追问话术"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suggested_questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"问题标题"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"find_item"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3.7 HTTP 请求节点"&gt;3.7 HTTP 请求节点&lt;/h3&gt;
&lt;p&gt;真正调用 Rails 搜索接口的地方：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;POST 请求&lt;/li&gt;
&lt;li&gt;URL：&lt;code&gt;{{#env.HTTP_DOMAIN#}}/api/v1/products/ai_search&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;请求头带 &lt;code&gt;X-Authorization&lt;/code&gt;（从 Start 节点取）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;超时和重试：连接/读取/写入都设 10 秒，失败自动重试 1 次。&lt;/p&gt;
&lt;h3 id="3.8 结果合并"&gt;3.8 结果合并&lt;/h3&gt;
&lt;p&gt;根据条件判断走不同的分支，各分支互斥，只返回其中一个：追问分支返回追问话术，搜索分支返回商品列表结果，无结果时返回默认话术。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="四、向量搜索技术详解"&gt;四、向量搜索技术详解&lt;/h2&gt;&lt;h3 id="4.1 为什么用向量搜索"&gt;4.1 为什么用向量搜索&lt;/h3&gt;
&lt;p&gt;传统搜索的局限：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;搜索方式&lt;/th&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;布尔搜索&lt;/td&gt;
&lt;td&gt;AND/OR/NOT 组合太复杂，用户不会用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;向量搜索能理解语义：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;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;h3 id="4.2 向量搜索原理"&gt;4.2 向量搜索原理&lt;/h3&gt;
&lt;p&gt;流程：文本 → Embedding 模型 → 1024 维向量 → KNN 搜索 → 返回结果。&lt;/p&gt;

&lt;p&gt;步骤：1) 把用户输入转成向量；2) 在 ES 里用余弦相似度找最相似的 20 个商品；3) 用 &lt;code&gt;min_score&lt;/code&gt; 阈值过滤。&lt;/p&gt;
&lt;h3 id="4.3 EmbeddingService 实现"&gt;4.3 EmbeddingService 实现&lt;/h3&gt;
&lt;p&gt;我们用阿里云百炼的 Embedding 接口。一次请求把文本转成 1024 维向量，耗时大概 100-200ms。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;缓存策略&lt;/strong&gt;：相同文本 10 分钟内重复查询直接返回缓存。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;错误处理&lt;/strong&gt;：向量生成失败了返回空数组，后面的 ES 查询就不做了，避免浪费资源。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;EmbeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"翡翠手镯，种水细腻，色泽温润"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; [0.023, -0.156, 0.089, ..., 0.012] (1024个浮点数)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="4.4 商品向量同步 Job"&gt;4.4 商品向量同步 Job&lt;/h3&gt;
&lt;p&gt;商品上架后，AI 总结生成完成时自动触发向量生成。流程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;取商品的 AI 总结（必须是已生成的）&lt;/li&gt;
&lt;li&gt;组合文本：商品标题 + AI 总结内容&lt;/li&gt;
&lt;li&gt;调用 EmbeddingService 生成 1024 维向量&lt;/li&gt;
&lt;li&gt;验证向量维度&lt;/li&gt;
&lt;li&gt;同步到 ES 的 &lt;code&gt;ai_summary_vector&lt;/code&gt; 字段&lt;/li&gt;
&lt;li&gt;标记生成时间 &lt;code&gt;vector_generated_at&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;：版本冲突自动重试 3 次，失败了要报警。向量是预先生成的，搜索时直接用，不用实时生成。&lt;/p&gt;
&lt;h3 id="4.5 ES KNN 查询构建"&gt;4.5 ES KNN 查询构建&lt;/h3&gt;
&lt;p&gt;核心方法：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;build_ai_knn_search_body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;构建 KNN 查询 Body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_knn_filters&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取过滤条件（排除关键词、圈口尺寸等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apply_filters_to_knn_query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;应用过滤条件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键参数&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;k&lt;/code&gt;：返回多少条结果。翡翠设 30，玉石 25，钻石和彩宝设 20。翡翠商品多，设大一点有足够候选。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;min_score&lt;/code&gt;：相似度阈值。翡翠设 0.85，玉石 0.8。翡翠商品描述比较标准化，设高一点不容易跑偏。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;核心词加权&lt;/strong&gt;：如果提取到核心词（如"翡翠"），生成向量时重复 3 次，让这个词权重更高。"翡翠手镯" → "翡翠 翡翠 翡翠 手镯" → 生成向量。&lt;/p&gt;
&lt;h3 id="4.6 召回、排序、重排"&gt;4.6 召回、排序、重排&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;召回流程&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;生成查询向量&lt;/li&gt;
&lt;li&gt;用 HNSW 在向量空间里找最近的 k 个&lt;/li&gt;
&lt;li&gt;应用 &lt;code&gt;knn.filter&lt;/code&gt; 过滤条件&lt;/li&gt;
&lt;li&gt;计算余弦相似度，过滤低于 &lt;code&gt;min_score&lt;/code&gt; 的&lt;/li&gt;
&lt;li&gt;返回商品 ID 列表&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;排序&lt;/strong&gt;：目前只用 &lt;code&gt;_score&lt;/code&gt;（相似度分数）降序，简单的做法，后续可以加其他排序维度。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;重排&lt;/strong&gt;：没有额外的重排逻辑，ES 返回什么顺序就是什么顺序。&lt;/p&gt;
&lt;h3 id="4.7 完整 ES KNN 查询示例"&gt;4.7 完整 ES KNN 查询示例&lt;/h3&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"knn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai_summary_vector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"query_vector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.156&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.089&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.067&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.178&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.012&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"k"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"onsale"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hide_in_miniprogram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmed_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inner_circle_size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"exists"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai_summary_vector"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"must_not"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match_phrase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"goods_description_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"镶嵌"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"analyzer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ik_max_word"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"min_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"desc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"desc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：过滤条件都在 &lt;code&gt;knn.filter&lt;/code&gt; 里处理，在向量搜索阶段就过滤，减少计算量。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="五、商品语义搜索模块详解"&gt;五、商品语义搜索模块详解&lt;/h2&gt;&lt;h3 id="5.1 AiSearch 数据模型"&gt;5.1 AiSearch 数据模型&lt;/h3&gt;
&lt;p&gt;AiSearch 表记录每次搜索的完整参数，搜索条件用 JSONB 存。为什么要冗余存一份 keywords_text？因为运营同学要统计数据，JSONB 查起来麻烦。每条记录关联 ai_chat_id 和 ai_chat_message_id，方便回溯"这条搜索结果是谁发的"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;主要字段&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;msg_id&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;消息 ID（唯一索引）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;search_params&lt;/td&gt;
&lt;td&gt;jsonb&lt;/td&gt;
&lt;td&gt;完整搜索参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keywords_text&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;搜索关键词文本（冗余字段）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;category&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;商品分类（冗余字段）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;price_min/max&lt;/td&gt;
&lt;td&gt;decimal&lt;/td&gt;
&lt;td&gt;价格范围（冗余字段）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ai_chat_id&lt;/td&gt;
&lt;td&gt;bigint&lt;/td&gt;
&lt;td&gt;关联的对话 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ai_chat_message_id&lt;/td&gt;
&lt;td&gt;bigint&lt;/td&gt;
&lt;td&gt;关联的消息 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：记录搜索参数支持"再次搜索"，通过 ai_search_id 复用参数实现"查看更多"，也支持搜索行为分析和统计。&lt;/p&gt;
&lt;h3 id="5.2 API Controller 实现"&gt;5.2 API Controller 实现&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;POST /api/v1/products/ai_search&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;处理流程&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;参数校验：q 或 category_id 至少一个不为空&lt;/li&gt;
&lt;li&gt;创建 AiSearch 记录，生成 ai_search_id&lt;/li&gt;
&lt;li&gt;执行搜索：生成查询向量 → 构建 KNN 查询 → ES 查询&lt;/li&gt;
&lt;li&gt;数据组装：从数据库捞商品详情，用 Presenter 格式化&lt;/li&gt;
&lt;li&gt;返回结果：带 ai_search_id、total、products&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;成功返回&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ai_search_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"翡翠手镯"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;错误返回&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"参数错误：q 或 category_id 不能为空"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;curl 调用示例&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.example.com/api/v1/products/ai_search"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Authorization: Bearer token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"is_ai_search": true, "q": "翡翠手镯，5000-8000元", "category_id": 1, "price_min": 5000, "price_max": 8000}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;错误处理&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;错误类型&lt;/th&gt;
&lt;th&gt;处理方式&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数校验失败&lt;/td&gt;
&lt;td&gt;返回 400，提示具体错误信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向量生成失败&lt;/td&gt;
&lt;td&gt;记录日志，返回空结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ES 查询失败&lt;/td&gt;
&lt;td&gt;返回 500，提示"搜索失败，请稍后重试"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="六、KNN 按分类配置"&gt;六、KNN 按分类配置&lt;/h2&gt;
&lt;p&gt;配置表：ai_search_knn_category_config&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;分类 ID&lt;/th&gt;
&lt;th&gt;分类名称&lt;/th&gt;
&lt;th&gt;k&lt;/th&gt;
&lt;th&gt;min_score&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;翡翠&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;玉石（和田玉等）&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;钻石&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;彩宝&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;k&lt;/strong&gt;：返回多少条结果。翡翠商品多，设 30；其他品类设 20-25。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;min_score&lt;/strong&gt;：相似度阈值。翡翠设 0.85 因为商品描述标准化，不容易跑偏；其他品类设 0.8。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;调参建议&lt;/strong&gt;：搜索结果太少就降 min_score 或增 k；结果不相关就提高 min_score。如果某个分类经常没结果，可以把 min_score 降 0.05-0.1。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="七、常见问题与故障排查"&gt;七、常见问题与故障排查&lt;/h2&gt;&lt;h3 id="7.1 SSE 连接问题"&gt;7.1 SSE 连接问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：SSE 连接建立失败或频繁断开&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;检查请求头：&lt;code&gt;Accept: text/event-stream&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;检查 token：&lt;code&gt;X-Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;检查网络和代理设置&lt;/li&gt;
&lt;li&gt;看 Rails 日志里的错误信息&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;常见原因&lt;/strong&gt;：token 过期或无效、网络超时（默认 60 秒）、服务器主动关闭连接。&lt;/p&gt;
&lt;h3 id="7.2 向量生成失败"&gt;7.2 向量生成失败&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：搜索返回空结果&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;检查 Embedding 服务 API 是否可访问&lt;/li&gt;
&lt;li&gt;检查 API 密钥是否有效&lt;/li&gt;
&lt;li&gt;看 Rails 日志的错误信息&lt;/li&gt;
&lt;li&gt;检查缓存是否正常（失败的请求也可能被缓存）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：向量失败会降级为返回空结果，检查 Embedding 服务配置和网络，清理缓存后重试。&lt;/p&gt;
&lt;h3 id="7.3 Dify 工作流调用失败"&gt;7.3 Dify 工作流调用失败&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：调用超时或返回错误&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;检查 &lt;code&gt;DIFY_API_BASE_URL&lt;/code&gt; 环境变量&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;DIFY_CHATFLOW_API_KEY&lt;/code&gt; 是否有效&lt;/li&gt;
&lt;li&gt;检查 Dify 服务是否正常&lt;/li&gt;
&lt;li&gt;看 Rails 日志的详细错误&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;常见错误&lt;/strong&gt;：&lt;code&gt;ConnectionError&lt;/code&gt;（连不上 Dify）、&lt;code&gt;ResponseError&lt;/code&gt;（Dify 返回错误）、&lt;code&gt;ParseError&lt;/code&gt;（响应解析失败）。&lt;/p&gt;
&lt;h3 id="7.4 ES 查询慢"&gt;7.4 ES 查询慢&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：查询超时或延迟高&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;检查 ES 集群状态和负载&lt;/li&gt;
&lt;li&gt;检查索引分片和副本配置&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;k&lt;/code&gt; 值是否过大（建议不超过 50）&lt;/li&gt;
&lt;li&gt;检查 HNSW 参数&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;优化&lt;/strong&gt;：适当降低 &lt;code&gt;k&lt;/code&gt;、缩小 &lt;code&gt;knn.filter&lt;/code&gt; 范围、检查 ES 集群资源。&lt;/p&gt;
&lt;h3 id="7.5 搜索结果不相关"&gt;7.5 搜索结果不相关&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：返回的商品和用户需求不匹配&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;检查 Dify 提取的 &lt;code&gt;q&lt;/code&gt; 字段是否正确&lt;/li&gt;
&lt;li&gt;检查 ES 里的 &lt;code&gt;ai_summary_vector&lt;/code&gt; 是否正常&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;min_score&lt;/code&gt; 阈值是否合适&lt;/li&gt;
&lt;li&gt;检查核心词加权是否生效&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：提高 &lt;code&gt;min_score&lt;/code&gt;、检查商品 AI 总结质量、优化属性提取规则。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="技术架构总结"&gt;技术架构总结&lt;/h2&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────
│                    AI 找货助手技术架构                         
├─────────────────────────────────────────────────────────────
│  前端层 → SSE 对话接口 → Dify 工作流 → 商品语义搜索接口       
│                              ↓                               
│                      Elasticsearch KNN 搜索                   
│                              ↓                               
│                      商品向量同步 + 查询向量生成               
├─────────────────────────────────────────────────────────────
│  核心流程：Dify 对话流 → 意图识别 → 属性提取 → 追问 → 搜索     
│  核心技术：SSE 实时推送 + 向量语义搜索 + HNSW 高性能索引       
└─────────────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>ThxFly</author>
      <pubDate>Tue, 10 Feb 2026 19:16:13 +0800</pubDate>
      <link>https://ruby-china.org/topics/44477</link>
      <guid>https://ruby-china.org/topics/44477</guid>
    </item>
    <item>
      <title>5 分钟快速入门 AI 辅助编程：基于 OpenCode 与免费版 MiniMax M2.1</title>
      <description>&lt;p&gt;基于 OpenCode + 免费版 MiniMax M2.1，手把手带你 5 分钟快速上手 AI 辅助编程。&lt;/p&gt;
&lt;h2 id="操作指南"&gt;操作指南&lt;/h2&gt;&lt;h3 id="步骤一: 安装opencode最新版"&gt;步骤一：安装 opencode 最新版&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;anomalyco/tap/opencode
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="步骤二: 终端运行opencode"&gt;步骤二：终端运行 opencode&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;项目根目录
opencode 
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="步骤三: 切换模型"&gt;步骤三：切换模型&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/50608e59-c1e7-4c64-9070-481c30e2e149.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/cae32701-58f5-4c4c-b451-2947c928f953.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="步骤四: 终端授权"&gt;步骤四：终端授权&lt;/h3&gt;
&lt;p&gt;iterm2 会弹出一些授权警告，正常授权即可。&lt;/p&gt;
&lt;h3 id="步骤五: 开始AI编程"&gt;步骤五：开始 AI 编程&lt;/h3&gt;
&lt;p&gt;示例：帮我新增一个功能：在列表页右上角支持表结构。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/07fadad4-2bcc-4880-8cd4-e6f7ab96e9da.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/99883029-e699-413b-bcb1-4def594da37f.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/ea0cf331-13dd-4912-b93d-d06ec362b049.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/791a8eed-c9a7-4743-a217-f37edce97843.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/c538229d-2f62-4162-8193-50f19a4c7ebb.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/2f7f22c0-d905-4616-9053-7e881cef5022.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/b26bf383-f5fb-4ccd-afa3-1d9ef8a9d7d4.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/d02bd1fc-7c40-46cd-94c2-bde6ee80bee7.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/ed9d8fe6-8c39-49f5-a51d-dd23cb7dcdc7.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/16154a6b-7ad3-48ab-8f07-1d92bf59cb8b.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/2a5b6aae-04f3-46de-8b41-91d52cd5ae54.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/c7b639f1-f0e6-4167-a74f-c67bad6a91ab.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="最终预览效果"&gt;最终预览效果&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/38817420-5444-483c-a9bd-f9bc52dbd54e.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/0ad9731a-abb8-4643-acc2-dafcc9f19b84.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;ps: 花了约 1 个多小时，通过与 OpenCode 交互 10 来轮，才最终实现当前效果。过程中包括：导入数据库、插入管理员账号、修改管理员密码、修复 Bug，以及新增“显示 CREATE SQL”等功能。执行一次较复杂的完整流程，大约需要 4 分钟。正常自然语言询问即可。毕竟是白嫖的模型，偶尔也会遇到需要等待的情况。&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Tue, 10 Feb 2026 12:29:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/44475</link>
      <guid>https://ruby-china.org/topics/44475</guid>
    </item>
    <item>
      <title>把 Mac 变成键盘流神器：Hammerspoon 配置指南（窗口管理 + 应用秒开 + 快捷键自定义）</title>
      <description>&lt;p&gt;Hammerspoon 是 macOS 上强大的开源自动化工具，通过 Lua 脚本可以控制窗口、绑定快捷键、切换应用等，大幅提升工作效率。&lt;/p&gt;
&lt;h2 id="🔧 安装"&gt;🔧 安装&lt;/h2&gt;&lt;h3 id="方式一：使用 Homebrew（推荐）"&gt;方式一：使用 Homebrew（推荐）&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; hammerspoon
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="方式二：手动安装"&gt;方式二：手动安装&lt;/h3&gt;
&lt;p&gt;从 &lt;a href="https://github.com/Hammerspoon/hammerspoon/releases" rel="nofollow" target="_blank" title=""&gt;GitHub Releases&lt;/a&gt; 下载 &lt;code&gt;.zip&lt;/code&gt; 文件并解压。解压后移到&lt;code&gt;/Applications&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="首次配置"&gt;首次配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;授权辅助功能&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;打开 &lt;code&gt;系统设置&lt;/code&gt; &amp;gt; &lt;code&gt;隐私与安全性&lt;/code&gt; &amp;gt; &lt;code&gt;辅助功能&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;勾选 &lt;code&gt;Hammerspoon&lt;/code&gt; 以允许控制电脑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;配置文件位置&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;配置文件位于：&lt;code&gt;~/.hammerspoon/init.lua&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;首次启动会自动生成示例文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="📝 配置示例"&gt;📝 配置示例&lt;/h2&gt;
&lt;p&gt;编辑配置文件：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vim ~/.hammerspoon/init.lua
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="1. 安全重载配置"&gt;1. 安全重载配置&lt;/h3&gt;
&lt;p&gt;配置重载快捷键，出错时会弹窗提示：&lt;/p&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 安全重载：出错时弹窗提示&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"R"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;pcall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"❌ 配置错误:\n"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="nb"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"✅ 配置已重载"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2. 窗口管理"&gt;2. 窗口管理&lt;/h3&gt;&lt;h4 id="窗口分屏（四象限布局）"&gt;窗口分屏（四象限布局）&lt;/h4&gt;&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 窗口分屏函数&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;focusedWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键绑定&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;      &lt;span class="c1"&gt;-- 上半屏&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Down"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;   &lt;span class="c1"&gt;-- 下半屏&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Left"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;     &lt;span class="c1"&gt;-- 左半屏&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Right"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;  &lt;span class="c1"&gt;-- 右半屏&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="多显示器切换"&gt;多显示器切换&lt;/h4&gt;&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 将窗口移到下一显示器，并移动鼠标到窗口中心&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;focusedWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;moveToScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"窗口已移至下一屏幕"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setAbsolutePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3. 应用快速切换"&gt;3. 应用快速切换&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;Option + 字母&lt;/code&gt; 快速切换到常用应用：&lt;/p&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 浏览器&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Safari"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Google Chrome"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 开发工具&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Apifox"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"iTerm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cursor"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DBeaver"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- AI 工具&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Qianwen"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 社交应用&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"WeChat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"企业微信"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 系统工具&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"System Settings"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 娱乐&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"NeteaseMusic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="4. 快速打开网页"&gt;4. 快速打开网页&lt;/h3&gt;&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 快速打开 GitHub&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"G"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlevent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://github.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="5. 启动提示"&gt;5. 启动提示&lt;/h3&gt;&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 菜单栏显示通知&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Hammerspoon 已加载！"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="🔄 重新加载配置"&gt;🔄 重新加载配置&lt;/h2&gt;
&lt;p&gt;修改 &lt;code&gt;init.lua&lt;/code&gt; 后，有两种方式重新加载：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;快捷键&lt;/strong&gt;：按 &lt;code&gt;Cmd+Ctrl+R&lt;/code&gt;（默认快捷键）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;菜单栏&lt;/strong&gt;：点击 Hammerspoon 菜单栏图标 → &lt;code&gt;Reload Config&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="💡 实用技巧"&gt;💡 实用技巧&lt;/h2&gt;&lt;h3 id="查找应用名称"&gt;查找应用名称&lt;/h3&gt;
&lt;p&gt;如果应用切换不生效，可能是应用名称不正确。在 Hammerspoon 控制台中运行：&lt;/p&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runningApplications&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者在终端中运行：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'tell application "System Events" to get name of every process'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="常用 API 参考"&gt;常用 API 参考&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hs.window.focusedWindow()&lt;/code&gt; - 获取当前焦点窗口&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hs.application.launchOrFocus(name)&lt;/code&gt; - 启动或聚焦应用&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hs.alert.show(message, seconds)&lt;/code&gt; - 显示提示&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hs.hotkey.bind(modifiers, key, fn)&lt;/code&gt; - 绑定快捷键&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="📚 更多资源"&gt;📚 更多资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;官方文档&lt;/strong&gt;：&lt;a href="https://www.hammerspoon.org/docs/index.html" rel="nofollow" target="_blank" title=""&gt;https://www.hammerspoon.org/docs/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub 仓库&lt;/strong&gt;：&lt;a href="https://github.com/Hammerspoon/hammerspoon" rel="nofollow" target="_blank" title=""&gt;https://github.com/Hammerspoon/hammerspoon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;示例配置&lt;/strong&gt;：&lt;a href="https://github.com/Hammerspoon/hammerspoon/wiki/Sample-Configurations" rel="nofollow" target="_blank" title=""&gt;https://github.com/Hammerspoon/hammerspoon/wiki/Sample-Configurations&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="🎯 快速上手"&gt;🎯 快速上手&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一句话总结&lt;/strong&gt;：写 Lua 脚本 → 绑定快捷键 → 自动化你的 Mac！&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：根据个人需求调整快捷键和应用名称，打造专属的自动化工作流。&lt;/p&gt;
&lt;h2 id="完整代码示例"&gt;完整代码示例&lt;/h2&gt;&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 安全重载：出错时弹窗提示&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"R"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;pcall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"❌ 配置错误:\n"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="nb"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"✅ 配置已重载"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 切换窗口&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;focusedWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Down"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Left"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"Right"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveWindowTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="c1"&gt;-- 将窗口移到下一显示器&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;focusedWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;moveToScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"窗口已移至下一屏幕"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;focusedWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setAbsolutePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快速打开 GitHub&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ctrl"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"G"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlevent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://github.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+s 切换到 Safari&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Safari"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+g 切换到 Google Chrome&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Google Chrome"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+a 切换到 Apifox&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Apifox"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+t 切换到 iTerm&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"iTerm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+c 切换到 Cursor&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cursor"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+e 切换到 千问&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Qianwen"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+w 切换到 微信&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"WeChat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+q 切换到 企业微信&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"企业微信"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+p 切换到 系统设置&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"System Settings"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+f 切换到 Finder&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Finder"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+y 切换到 网易云音乐&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"NeteaseMusic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 快捷键: Option+d 切换到 DBeaver&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchOrFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DBeaver"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 菜单栏显示通知&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Hammerspoon 已加载！"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>ThxFly</author>
      <pubDate>Fri, 28 Nov 2025 11:20:30 +0800</pubDate>
      <link>https://ruby-china.org/topics/44401</link>
      <guid>https://ruby-china.org/topics/44401</guid>
    </item>
    <item>
      <title>过时的 Wiki 可以下掉吧？</title>
      <description>&lt;p&gt;Ruby China 的 Wiki 好多都已经过时了，没有与时俱进，管理员可以将他们都删了不，从某种程度，不利于新手使用 Ruby 了.
&lt;img src="https://l.ruby-china.com/photo/ThxFly/bd9ba58a-27da-42ef-b263-421702bd5c85.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/ThxFly/e4e229e9-b857-42c3-ae5f-5604f7b6d5d5.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Sat, 01 Nov 2025 10:36:37 +0800</pubDate>
      <link>https://ruby-china.org/topics/44373</link>
      <guid>https://ruby-china.org/topics/44373</guid>
    </item>
    <item>
      <title>零成本搭建产品知识库：让产研同事能“问”到原型</title>
      <description>&lt;h2 id="🚀 零成本搭个产品知识库：让同事直接“问”出原型"&gt;🚀 零成本搭个产品知识库：让同事直接“问”出原型&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一个好的知识库，不是存了多少文档，而是大家愿不愿意用、能不能快速找到答案。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;作者&lt;/strong&gt;：Shootingfly&lt;br&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;适用场景&lt;/strong&gt;：中小团队、用 Axure 做原型、企业微信办公&lt;br&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;关键词&lt;/strong&gt;：知识库、Axure、PDF、企业微信、智能机器人&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="你有没有遇到过这些情况？"&gt;你有没有遇到过这些情况？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;❓ “这个按钮点完跳哪去了？” —— 得一个一个打开 Axure 文件翻&lt;/li&gt;
&lt;li&gt;❓ “上个版本的下单流程是啥样？” —— 找了一圈，发现本地没存&lt;/li&gt;
&lt;li&gt;❓ “产品又改了？没人通知啊！” —— 只能私聊产品经理问&lt;/li&gt;
&lt;li&gt;❓ “客服想看看页面逻辑” —— 可人家打不开 &lt;code&gt;.rp&lt;/code&gt; 文件啊&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些问题说白了就一点：&lt;strong&gt;Axure 原型“藏得太深”，想查个东西太费劲。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我们试过各种办法，最后发现：&lt;strong&gt;用企业微信自带的“智能机器人”，配合 PDF 文档，就能让同事在聊天里直接问出原型内容。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;整个过程&lt;strong&gt;不用写代码、不花钱、不运维&lt;/strong&gt;，今天分享下我们是怎么做的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第一步：把 Axure 原型变成“能被搜”的文档"&gt;第一步：把 Axure 原型变成“能被搜”的文档&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.rp&lt;/code&gt; 是 Axure 自己的格式，电脑没装 Axure 就打不开，更别说让机器人“读懂”了。&lt;/p&gt;

&lt;p&gt;我们试了三种办法：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&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;tr&gt;
&lt;td&gt;&lt;strong&gt;转成 PDF&lt;/strong&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;strong&gt;转成图片&lt;/strong&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;strong&gt;转成 Markdown + 图&lt;/strong&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;h3 id="✅ 最后我们选了：Axure 导出 PDF"&gt;✅ 最后我们选了：&lt;strong&gt;Axure 导出 PDF&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;操作特别简单：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;打开 Axure&lt;/li&gt;
&lt;li&gt;点菜单栏 &lt;strong&gt;File → Print&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;选择 &lt;strong&gt;“Save as PDF”&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;保存就行&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="第二步：让机器人“学会”这些文档"&gt;第二步：让机器人“学会”这些文档&lt;/h2&gt;
&lt;p&gt;目标是：谁在某处问“下单流程是啥？”，机器人能直接回。&lt;/p&gt;

&lt;p&gt;我们看了两种方案：&lt;/p&gt;
&lt;h3 id="方案一：自己搭 RAG（比如 Dify、RAGFlow）"&gt;方案一：自己搭 RAG（比如 Dify、RAGFlow）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ 能自建，数据自己管&lt;/li&gt;
&lt;li&gt;✅ 支持上传 PDF、Word、Excel&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;h3 id="方案二：企业微信自带的“智能机器人”"&gt;方案二：企业微信自带的“智能机器人”&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ 直接在企业微信里用，谁都会&lt;/li&gt;
&lt;li&gt;✅ 支持上传 PDF、Word、Excel&lt;/li&gt;
&lt;li&gt;✅ 上传后自动“读”内容，能回答问题&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;blockquote&gt;
&lt;p&gt;✅ 最后我们选了企业微信机器人，与其自己辛苦去调参调优，不如让腾讯的开发人员帮你完成这一步。&lt;strong&gt;落地快、用着顺、不用费心思调参数和部署。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="✅ 怎么搭？手把手教你（手机端操作）"&gt;✅ 怎么搭？手把手教你（手机端操作）&lt;/h2&gt;
&lt;p&gt;我们主要用手机操作，步骤很简单：&lt;/p&gt;
&lt;h3 id="1. 创建机器人"&gt;1. 创建机器人&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;更新企业微信到最新版（V5.0+）&lt;/li&gt;
&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;/li&gt;
&lt;li&gt;名字：产品助手&lt;/li&gt;
&lt;li&gt;简介：APP 的设计问题都可以问我！&lt;/li&gt;
&lt;li&gt;模型：选 &lt;strong&gt;DeepSeek-V3 快速问答&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;知识：先上传几份产品原型 PDF&lt;/li&gt;
&lt;li&gt;角色设定：留空（不需要）&lt;/li&gt;
&lt;li&gt;权限：

&lt;ul&gt;
&lt;li&gt;谁能问？→ 产研团队&lt;/li&gt;
&lt;li&gt;谁能改文档？→ 产品经理、研发负责人&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/784bcdc7-b9af-490e-a7e6-f31e87883614.png" width="30%"&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ 创建完就能用，不需要写一行代码。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="🖼️ 实际效果（附截图）"&gt;🖼️ 实际效果（附截图）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;同事直接 @机器人 问问题，秒回答案&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/b2bfc0ab-89b5-43a1-816a-ca7c46fdd187.png" width="100%"&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;更多时候是私聊机器人得到答案&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/78c2c5a6-203e-47f2-86df-5ce4833514e4.png!large" width="100%"&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;支持多种文档来源，不仅能传本地文件，还能直接引用共享的 Doc 文档和 Excel 文档&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/5ff15166-10a6-48cc-aa27-fe010aa26b12.png!large" width="30%"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="💡 我们踩过的坑和经验"&gt;💡 我们踩过的坑和经验&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PDF 是最省事的格式&lt;/strong&gt;&lt;br&gt;
不用装 Axure，也不用学新工具，谁都能打开，机器人也能“读懂”。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;文件名要规范&lt;/strong&gt;&lt;br&gt;
别叫“最新版.pdf”、“改好了.pdf”，我们统一用：&lt;code&gt;版本_功能_日期_作者.pdf&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;记得及时更新&lt;/strong&gt;&lt;br&gt;
每次原型改了，必须重新导出 PDF 并上传，不然机器人“学”的是旧的。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;和上线流程绑在一起&lt;/strong&gt;&lt;br&gt;
我们把“更新知识库”加进了上线 Checklist，确保文档不掉队。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="🌟 最后总结"&gt;🌟 最后总结&lt;/h2&gt;
&lt;p&gt;我们没花一分钱，也没搭服务器，就靠企业微信自带的功能，搞定了一个&lt;strong&gt;真正能用的知识库&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;核心就两点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Axure 原型导出为 PDF&lt;/strong&gt; → 让文档“可读、可搜、可分享”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;用企业微信智能机器人&lt;/strong&gt; → 让同事“在聊天里直接问”&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h3 id="🔗 小提示"&gt;🔗 小提示&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;企业微信这个功能 9 月份刚推不久，很多人还不知道&lt;/li&gt;
&lt;li&gt;相信钉钉、飞书很快也会有类似功能，或已有类似功能&lt;/li&gt;
&lt;li&gt;功能完全免费，就算后续收费咱小公司也是用得起的&lt;/li&gt;
&lt;li&gt;单个 PDF 最大支持 100M，文档上传后需要几分钟的解析才能使用&lt;/li&gt;
&lt;li&gt;从截图可以看出，回答效果是非常好，还能带图！&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>ThxFly</author>
      <pubDate>Sat, 18 Oct 2025 23:47:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/44345</link>
      <guid>https://ruby-china.org/topics/44345</guid>
    </item>
    <item>
      <title>【新工具上线】yapi_check——让 API 验证不再头疼！</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;YAPI 是一款比较好用的接口文档管理工具。可见 V 站讨论 &lt;a href="https://www.v2ex.com/t/697920" rel="nofollow" target="_blank"&gt;https://www.v2ex.com/t/697920&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gem: &lt;a href="https://github.com/shootingfly/yapi_check" rel="nofollow" target="_blank"&gt;https://github.com/shootingfly/yapi_check&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;大家好！&lt;/p&gt;

&lt;p&gt;在日复一日的开发工作中，我们常常面临着 API 文档与代码不一致的挑战。无论是确保 API 请求的格式正确，还是验证返回数据的准确性，这一切的工作都显得繁琐且耗时。但现在，有了新工具 yapi_check，这一切都将变得简单和高效！&lt;/p&gt;
&lt;h3 id="YapiCheck说明"&gt;YapiCheck 说明&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;yapi_check 是什么？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;yapi_check 是一款专为 Rails 开发者设计的 Gem 包，旨在提供一个简洁、易用的 YAPI 验证解决方案。它能帮助你轻松校验 YAPI 请求与响应，确保数据的准确性，从而大幅提高开发效率。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;yapi_check 能做什么？&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;自动验证 YAPI 请求和响应&lt;/strong&gt;：通过简单的配置，yapi_check 能够自动验证 YAPI 的请求和响应格式，确保它们符合预期。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;详细的错误报告&lt;/strong&gt;：当 API 验证失败时，yapi_check 能够提供详细的错误报告，帮助你快速定位问题所在。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;简化测试流程&lt;/strong&gt;：通过自动化的验证流程，yapi_check 可以帮助你减少手动测试的工作量，让测试更加高效。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;如何开始使用？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;开始使用 yapi_check 非常简单，只需要几个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在你的 Gemfile 中添加&lt;code&gt;gem 'yapi_check'&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;运行&lt;code&gt;bundle install&lt;/code&gt;安装。&lt;/li&gt;
&lt;li&gt;根据 yapi_check 的文档设置你的 API 验证规则。&lt;/li&gt;
&lt;li&gt;开始享受轻松、高效的 API 验证体验！&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="技术原理概述"&gt;技术原理概述&lt;/h3&gt;
&lt;p&gt;在现代 Web 开发中，API 的准确性和一致性对于应用的稳定性和用户体验至关重要。我们的 Gem，yapi_check，利用了 YAPI 项目开放的 OpenAPI，这使得获取接口文档的元数据（如参数类型等）成为可能。得益于 Ruby 的声明式语法特性、Rails 的惯例式编程，以及 method_source 的支持，我们可以轻松读取和规范化 Rails 项目的源码。通过设定精准的正则匹配规则，yapi_check 能够有效地比对和确保项目的 API 接口与其文档的一致性。&lt;/p&gt;
&lt;h3 id="工作流程"&gt;工作流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;配置获取：&lt;/strong&gt; 首先，从 YAPI 项目配置中获取项目的 token。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;接口列表调用：&lt;/strong&gt; 使用 token 调用接口列表 API，(可选) 筛选指定迭代版本下的接口，并通过路径获取接口 ID。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;获取接口详情：&lt;/strong&gt; 通过接口 ID，获取并提取接口的详细信息，包括路径、请求参数类型和响应参数类型。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;请求参数校验：&lt;/strong&gt; 根据接口路径，提取 controller 对应 action 中的请求参数字段、参数类型、是否必填，并进行参数正确性比对。（注：非 JSON 参数的类型比对不在此范围内）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;响应参数校验：&lt;/strong&gt; 提取 action 对应 jbuilder 的响应参数和类型，进行响应参数的正确性比对。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;报告生成：&lt;/strong&gt; 生成并输出详细的排查报告，指出可能的不一致之处。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;自动化执行：&lt;/strong&gt; 全过程可通过 rake 任务自动化执行，提高效率和准确性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="使用注意"&gt;使用注意&lt;/h3&gt;
&lt;p&gt;为了确保 yapi_check 的兼容性，请注意以下最佳实践原则：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;参数校验：&lt;/strong&gt; 我们推荐使用 lucky_param 进行参数校验，以确保参数的准确性和安全性。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;响应格式：&lt;/strong&gt; 应通过 jbuilder 来管理响应输出，确保响应的 JSON 数据被正确地封装在"data"对象中，例如&lt;code&gt;{"data": {}}&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;文档管理：&lt;/strong&gt; 我们采用 YAPI 进行接口文档管理。对于需要非接口类文档，请在接口名称的开头明确标注"[接口说明]"。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;自定义修改：&lt;/strong&gt; 如果您的项目实践与上述不完全一致，您可以 fork 此项目并进行必要的修改，之后欢迎提交 MR 以共享您的改进。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;请注意，目前我们不支持布尔型参数的校验，布尔型应都用整型参数替代，以支持可能的扩展。&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Wed, 13 Mar 2024 15:49:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/43625</link>
      <guid>https://ruby-china.org/topics/43625</guid>
    </item>
    <item>
      <title>YAPI 接口文档编写规范</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;我有一个朋友，这是他公司实施的 YAPI 接口文档编写规范，屏蔽了一些涉及技术隐私的规范&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="接口文档编写规范"&gt;接口文档编写规范&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;枚举值

&lt;ol&gt;
&lt;li&gt;枚举值参数统一在备注中使用&lt;strong&gt;$$&lt;/strong&gt;后缀注明&lt;/li&gt;
&lt;li&gt;于项目中的公共分类中创建/enum 目录，录入枚举值&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;参数

&lt;ol&gt;
&lt;li&gt;参数英文名称需与中文注释保持一致&lt;/li&gt;
&lt;li&gt;参数需注明参数类型&lt;/li&gt;
&lt;li&gt;参数需注明是否必填&lt;/li&gt;
&lt;li&gt;参数需提供 mock 值&lt;/li&gt;
&lt;li&gt;时间型参数用秒级时间戳形式的整型表示&lt;/li&gt;
&lt;li&gt;ID 型参数用字符串表示&lt;/li&gt;
&lt;li&gt;日期型参数用"YYYY-mm-dd"形式的字符串表示&lt;/li&gt;
&lt;li&gt;不下发 null, 用对应类型的空值替代&lt;/li&gt;
&lt;li&gt;对象的空值是空对象&lt;/li&gt;
&lt;li&gt;数组的空值是空数组&lt;/li&gt;
&lt;li&gt;数值型的空值是 0&lt;/li&gt;
&lt;li&gt;字符串型的空值是空串&lt;/li&gt;
&lt;li&gt;公共参数不应在接口中出现&lt;/li&gt;
&lt;li&gt;新项目中，GET 请求使用查询参数，POST 请求使用 JSON 参数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;布尔值

&lt;ol&gt;
&lt;li&gt;布尔类型统一以整型进行上传和下发&lt;/li&gt;
&lt;li&gt;0 表示假，1 表示真&lt;/li&gt;
&lt;li&gt;布尔型参数统一使用_flag 后缀进行命名，如 valid_flag&lt;/li&gt;
&lt;li&gt;命名方式需采用肯定形式，禁止采用双重否定&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;备注

&lt;ol&gt;
&lt;li&gt;对接口的每次更改，需于备注中进行注明新增/修改的参数&lt;/li&gt;
&lt;li&gt;接口的使用若复杂难懂，需于备注中进行注明&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;接口名称

&lt;ol&gt;
&lt;li&gt;采用动宾结构&lt;/li&gt;
&lt;li&gt;例外情况：登录、注册等固定搭配&lt;/li&gt;
&lt;li&gt;已废弃的接口于名称后尾标注“(已废弃)”,并于接口备注说明废弃版本号&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;接口请求方式

&lt;ol&gt;
&lt;li&gt;只存在 GET 和 POST&lt;/li&gt;
&lt;li&gt;语义上更偏向获取信息的用 GET&lt;/li&gt;
&lt;li&gt;对安全性要求高，涉及到数据变更，需使用请求体传输数据的用 POST&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;接口路径

&lt;ol&gt;
&lt;li&gt;函数式命名风格，动宾搭配&lt;/li&gt;
&lt;li&gt;获取列表以 list_开头，并采用单数，如 list_user&lt;/li&gt;
&lt;li&gt;获取单个数据以 show_开头&lt;/li&gt;
&lt;li&gt;创建数据以 create_开头&lt;/li&gt;
&lt;li&gt;更新数据以 update_开头&lt;/li&gt;
&lt;li&gt;删除数据以 delete_开头&lt;/li&gt;
&lt;li&gt;资源型命名空间采用复数命名，整体路径如"/api/v1/users/list_user"&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;接口状态

&lt;ol&gt;
&lt;li&gt;当接口与测试服一致时，为已完成&lt;/li&gt;
&lt;li&gt;当处于实现中或代码修改中，为未完成&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;标签

&lt;ol&gt;
&lt;li&gt;每次新增或修改接口，需打上版本号标签，如"2.0.3", 不带 V&lt;/li&gt;
&lt;li&gt;对于以 H5 为主的项目，需打上"原生"标签，注明为原生调用的接口&lt;/li&gt;
&lt;li&gt;对于以原生为主的项目，需打上"H5"标签，注明为 H5 调用的接口&lt;/li&gt;
&lt;li&gt;对于免登录的接口，需打上"免登录"标签&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;分类名

&lt;ol&gt;
&lt;li&gt;分类名为中文单词 + (英文复数), 如"评论 (comments)"&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;环境配置

&lt;ol&gt;
&lt;li&gt;环境配置需配置开发、测试、正式三个环境&lt;/li&gt;
&lt;li&gt;请求配置需配置预处理代码，便于后台人员在线运行接口&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;测试集合

&lt;ol&gt;
&lt;li&gt;每次迭代，涉及的接口需创建测试集合放置&lt;/li&gt;
&lt;li&gt;上线测试服，测试集合应能看到对应的调用结果&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;公共分类

&lt;ol&gt;
&lt;li&gt;需统一公共参数 /common_params&lt;/li&gt;
&lt;li&gt;需统一说明枚举值 /enum&lt;/li&gt;
&lt;li&gt;需统一说明接口响应码 /code&lt;/li&gt;
&lt;li&gt;其他说明文档也放置于公共分类，如云存储空间说明等&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;请求头

&lt;ol&gt;
&lt;li&gt;按需设置请求头&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;接口设计

&lt;ol&gt;
&lt;li&gt;设计时需思考调用是否符合人类直觉&lt;/li&gt;
&lt;li&gt;设计时需思考参数设计是否能应对需求变更&lt;/li&gt;
&lt;li&gt;设计时需思考前端调用接口是否方便&lt;/li&gt;
&lt;li&gt;设计时需思考对于扩展是否方便&lt;/li&gt;
&lt;li&gt;设计时对接口涉及的改动信息需及时在讨论群组中说明并记录在文档中&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>ThxFly</author>
      <pubDate>Fri, 20 Oct 2023 11:56:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/43408</link>
      <guid>https://ruby-china.org/topics/43408</guid>
    </item>
    <item>
      <title>Crystal 1.0.0 Released!</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/ThxFly/36c78bf0-24c9-4b71-96c7-9025260bd421.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crystal-lang.org/2021/03/22/crystal-1.0-what-to-expect.html" rel="nofollow" target="_blank"&gt;https://crystal-lang.org/2021/03/22/crystal-1.0-what-to-expect.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img title=":clap:" alt="👏" src="https://twemoji.ruby-china.com/2/svg/1f44f.svg" class="twemoji"&gt; &lt;img title=":clap:" alt="👏" src="https://twemoji.ruby-china.com/2/svg/1f44f.svg" class="twemoji"&gt; &lt;img title=":clap:" alt="👏" src="https://twemoji.ruby-china.com/2/svg/1f44f.svg" class="twemoji"&gt; &lt;img title=":clap:" alt="👏" src="https://twemoji.ruby-china.com/2/svg/1f44f.svg" class="twemoji"&gt; &lt;img title=":clap:" alt="👏" src="https://twemoji.ruby-china.com/2/svg/1f44f.svg" class="twemoji"&gt; &lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Tue, 23 Mar 2021 20:02:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/41059</link>
      <guid>https://ruby-china.org/topics/41059</guid>
    </item>
    <item>
      <title> [预热] Crystal 语言将于 2021 年 3 月发布 1.0.0 版本</title>
      <description>&lt;p&gt;期待 &lt;img title=":grinning:" alt="😀" src="https://twemoji.ruby-china.com/2/svg/1f600.svg" class="twemoji"&gt; &lt;/p&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/crystal-lang/crystal/pull/10500" rel="nofollow" target="_blank"&gt;https://github.com/crystal-lang/crystal/pull/10500&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;变更日志：&lt;a href="https://github.com/bcardiff/crystal/blob/changelog/1.0.0/CHANGELOG.md" rel="nofollow" target="_blank"&gt;https://github.com/bcardiff/crystal/blob/changelog/1.0.0/CHANGELOG.md&lt;/a&gt;&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Fri, 19 Mar 2021 11:11:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/41045</link>
      <guid>https://ruby-china.org/topics/41045</guid>
    </item>
    <item>
      <title>Rails 5.2 不支持 Ruby 3.0</title>
      <description>&lt;p&gt;&lt;a href="https://github.com/rails/rails/issues/40938" rel="nofollow" target="_blank"&gt;https://github.com/rails/rails/issues/40938&lt;/a&gt;&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Sun, 27 Dec 2020 22:15:20 +0800</pubDate>
      <link>https://ruby-china.org/topics/40753</link>
      <guid>https://ruby-china.org/topics/40753</guid>
    </item>
    <item>
      <title>分享一个极简 API 框架——Runcobo</title>
      <description>&lt;h3 id="开发背景"&gt;开发背景&lt;/h3&gt;
&lt;p&gt;Crystal 目前最火的有三个框架&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kemal: Sinatra on Crystal&lt;/li&gt;
&lt;li&gt;Amber: Rails on Crystal&lt;/li&gt;
&lt;li&gt;Lucky：Crystal 活跃程度最高的一门框架，开发思想为“更少的 bug”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我试用了以上三种，分享些个人的感受：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kemal 用来做些小项目可以，但商用的话太难，毕竟 Sinatra 的底子给了它一个黑暗的未来；&lt;/li&gt;
&lt;li&gt;Amber 用起来行云流水，模仿 Rails 的确很像，应该能吸引一票的 Rails 程序员。但除了 Laravel，其他语言模仿 Rails 的都哑火了，也许是因为 Rails 太过优秀，导致模仿者难以超越前作；&lt;/li&gt;
&lt;li&gt;Lucky 更多是面向 Web 开发，这个框架提出的概念有些多。作为前后端分离的践行者，个人更想要一款面向 API 开发的微框架，去掉那些涉及网页开发的概念。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对以上框架不太满意，而这时公司又有 KPI 指标，于是乎我散失了几个周末不打机的美好时光，开发了以公司名字"云康宝"命名的&lt;strong&gt;Runcobo 框架&lt;/strong&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果你听过买过或用过云康宝体脂秤与共享秤，那你已自动成为本框架的终身 VIP 用户&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="第一段代码"&gt;第一段代码&lt;/h3&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"runcobo"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloWorld&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseAction&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/hello_world"&lt;/span&gt;

  &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;render_plain&lt;/span&gt; &lt;span class="s2"&gt;"Hello World"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Runcobo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先引入库，然后定义一个 Action，在 Action 中声明接口路径，声明需要渲染”Hello World“，最后启动服务器。&lt;/p&gt;

&lt;p&gt;乍一看没啥亮眼的，比不上 Sinatra 的三行情书短小精悍。&lt;/p&gt;

&lt;p&gt;但故事才刚刚开始......&lt;/p&gt;
&lt;h3 id="控制器"&gt;控制器&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;为了避免单个控制器代码过多，采用一个动作一个类的方式（赤裸裸抄袭前文提到的 Lucky 框架）&lt;/li&gt;
&lt;li&gt;本着微框架的初心，路由不应该单独定义在一个文件里，故路由作为声明语句，写在对应的动作中&lt;/li&gt;
&lt;li&gt;本着约定大于配置的准则，与最少惊讶原则，这里只约定了视图所在的目录"src/views/", 布局所在的目录"src/views/layouts/"&lt;/li&gt;
&lt;li&gt;参数先声明，后使用，使用简单且类型安全&lt;/li&gt;
&lt;li&gt;支持过滤器&lt;/li&gt;
&lt;li&gt;支持多种响应格式，纯文本、HTML 与 JSON&lt;/li&gt;
&lt;li&gt;渲染 json 视图采用 jbuilder，Rails 自带的 JSON 引擎，Runcobo 也将它带来了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="稍复杂的代码"&gt;稍复杂的代码&lt;/h3&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Books&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseAction&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/books"&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;author: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;author: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;render_jbuilder&lt;/span&gt; &lt;span class="s2"&gt;"books/index"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array!&lt;/span&gt; &lt;span class="s2"&gt;"books"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;book_id&lt;/span&gt;      &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;       &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;         &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="参数设计"&gt;参数设计&lt;/h3&gt;
&lt;p&gt;作为一门编译语言搭建的 API 框架，理应参数类型安全。Runcobo 采用参数声明的方式来进行参数解析，从而保证类型安全。与 Kemal、Amber、Lucky 相比，自认为 Runcobo 的参数设计还是比较漂亮的。&lt;/p&gt;

&lt;p&gt;URL 类型安全&lt;/p&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="no"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="ss"&gt;c: &lt;/span&gt;&lt;span class="no"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询参数类型安全&lt;/p&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="no"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="ss"&gt;c: &lt;/span&gt;&lt;span class="no"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表单参数类型安全&lt;/p&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="no"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="ss"&gt;c: &lt;/span&gt;&lt;span class="no"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON 参数类型安全&lt;/p&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="no"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="ss"&gt;c: &lt;/span&gt;&lt;span class="no"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;d: &lt;/span&gt;&lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="no"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;e: &lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用方式&lt;/p&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:b&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:d&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如你所见，参数支持嵌套对象、嵌套数组、支持可空，使用上也与 Rails 的参数大致相同，最大的不同便是编译期便确定了参数的类型。Runcobo 采用了宏魔法，使得参数在控制器的展现形式为命名元祖的形式，声明采用命名元组，使用的也是命名元组。（注：命名元组是一种固定大小，不可变的，已知键名及其类型、已知值类型的键值对）&lt;/p&gt;
&lt;h3 id="我们公司的使用情况"&gt;我们公司的使用情况&lt;/h3&gt;
&lt;p&gt;因为需要保证源码安全，需静态编译，目前我司已在分发给第三方定制客户的服务器 SDK（镜像包）使用了该框架。&lt;/p&gt;
&lt;h3 id="总结"&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果你喜欢 Crystal 的岁月静好，编译执行与类型安全，那你一定会喜欢 Runcobo；&lt;/li&gt;
&lt;li&gt;如果你想像书写接口文档那样书写 API 代码，那你一定会喜欢 Runcobo；&lt;/li&gt;
&lt;li&gt;如果你是处女座，享受设计的一致性，那你一定会喜欢 Runcobo；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;项目已经发布 1.0.0 版本，如果有想要优化的地方，等你成为贡献者与参与者！
项目地址 &lt;a href="https://github.com/runcobo/runcobo/" rel="nofollow" target="_blank"&gt;https://github.com/runcobo/runcobo/&lt;/a&gt; 欢迎 star&lt;/p&gt;

&lt;p&gt;更多文档请查看 &lt;a href="https://runcobo.github.io/docs/" rel="nofollow" target="_blank"&gt;https://runcobo.github.io/docs/&lt;/a&gt; ，需科学上网&lt;/p&gt;

&lt;p&gt;夜已深，未完待续........&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Thu, 24 Sep 2020 00:10:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/40433</link>
      <guid>https://ruby-china.org/topics/40433</guid>
    </item>
    <item>
      <title>ActiveAdmin 定制化实战</title>
      <description>&lt;p&gt;ActiveAdmin 是一款基于 Rails 的声明式后台管理框架，能够让大家写后台管理系统时会心一笑，下面是我司在生产中一些定制。&lt;/p&gt;
&lt;h3 id="一、换用多个主题"&gt;一、换用多个主题&lt;/h3&gt;
&lt;p&gt;ActiveAdmin 默认主题是灰色的，巨丑。为此，使用他人开源的三个主题包，让客服姐姐和程序员哥哥按照个人喜好选用不同主题。管理员表需加个主题字段&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'active_admin_theme'&lt;/span&gt;  &lt;span class="c1"&gt;# 上下布局主题，比较实用，根据README说明安装，下同&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'active_material'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1.4.2'&lt;/span&gt; &lt;span class="c1"&gt;# 上下布局主题，支持移动端，Material Design风格；由于列表页的动作会隐藏，使得这个主题在我司用的人最少&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'arctic_admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'3.0.0'&lt;/span&gt;  &lt;span class="c1"&gt;# 左右布局主题，简约风，支持移动端&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/assets/stylesheets/active_admin_theme.scss    active_admin_theme这个主题不定制还是会有点丑&lt;/span&gt;
&lt;span class="nv"&gt;$skinMainSecondColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#606ef0&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"active_admin/mixins"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"active_admin/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;.site_title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;29px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="mh"&gt;#e4eaec&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;a&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="nc"&gt;.index_table&lt;/span&gt; &lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="nc"&gt;.even&lt;/span&gt; &lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_stylesheet&lt;/span&gt; &lt;span class="s1"&gt;'active_admin_theme.css'&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_stylesheet&lt;/span&gt; &lt;span class="s1"&gt;'active_material_theme.css'&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_stylesheet&lt;/span&gt; &lt;span class="s1"&gt;'arctic_admin_theme.css'&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_javascript&lt;/span&gt; &lt;span class="s1"&gt;'active_admin_theme.js'&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_javascript&lt;/span&gt; &lt;span class="s1"&gt;'active_material_theme.js'&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_javascript&lt;/span&gt; &lt;span class="s1"&gt;'arctic_admin_theme.js'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveAdmin::Views::Pages::Base&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Arbre&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Document&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_active_admin_head&lt;/span&gt;
    &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;html_title&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_admin_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;site_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' | '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;active_admin_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meta_tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;current_admin_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;theme&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'active_material'&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active_material_theme.css'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;javascript_include_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active_material_theme.js'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'arctic_admin'&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'arctic_admin_theme.css'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;javascript_include_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'arctic_admin_theme.js'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active_admin_theme.css'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;javascript_include_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active_admin_theme.js'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active_admin_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;favicon&lt;/span&gt;
        &lt;span class="n"&gt;text_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;favicon_link_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active_admin_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;favicon&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;text_node&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tag&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/admin/home_controller.rb, config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/admin/check_theme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'admin/home#check_theme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :check_theme&lt;/span&gt;

&lt;span class="c1"&gt;# 切换主题&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_theme&lt;/span&gt;
  &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;current_admin_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;theme&lt;/span&gt;
          &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'active_admin_theme'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'arctic_admin'&lt;/span&gt;
          &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'arctic_admin'&lt;/span&gt;       &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'active_material'&lt;/span&gt;
          &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'active_material'&lt;/span&gt;    &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'active_admin_theme'&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="s1"&gt;'active_admin_theme'&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;current_admin_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;theme: &lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;redirect_back&lt;/span&gt; &lt;span class="ss"&gt;fallback_location: &lt;/span&gt;&lt;span class="n"&gt;root_path&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="二、更改默认匹配顺序"&gt;二、更改默认匹配顺序&lt;/h3&gt;
&lt;p&gt;ActiveAdmin 字符串类型的筛选框默认是模糊匹配，即用 like‘%?%’，这不符合索引优化原则，故改为完全匹配&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb 末尾添加&lt;/span&gt;
&lt;span class="no"&gt;ActiveAdmin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Inputs&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StringInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;
&lt;span class="no"&gt;ActiveAdmin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Inputs&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StringInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:equals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:contains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:starts_with&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:ends_with&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="三、关闭自动加载关联关系"&gt;三、关闭自动加载关联关系&lt;/h3&gt;
&lt;p&gt;ActiveAdmin 进入列表页时外键对应的 select 条件框会加载所有关联的 ID，数据表如果有上百万行就会卡死，改为默认关闭&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_default_association_filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="四、关闭默认列排序"&gt;四、关闭默认列排序&lt;/h3&gt;
&lt;p&gt;默认情况下，每一列都会生成排序按钮，如果某列没有索引，排序会很慢，改为默认关闭，要用时手动开启&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveAdmin::Views::Pages::TableFor&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sortable?&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="五、过滤搜索条件的空格"&gt;五、过滤搜索条件的空格&lt;/h3&gt;
&lt;p&gt;搜索条件如果输多了空格，客服姐姐就会来找你麻烦，默认帮她们去掉&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:strip_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: :index&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_params&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:q&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:q&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:q&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="六、关闭强参数验证"&gt;六、关闭强参数验证&lt;/h3&gt;
&lt;p&gt;内部系统，别人要有管理员账号和密码才能黑进来，做不做强参数无所谓，关闭默认的强参数机制，省一些容易出错代码&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit!&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="七、新增额外列方法"&gt;七、新增额外列方法&lt;/h3&gt;
&lt;p&gt;提取对于通用的 column 操作，避免代码重复（具体定义根据自己需求和代码来，以下是示例）&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveAdmin::Views::Pages::TableFor&lt;/span&gt;
  &lt;span class="c1"&gt;# 昵称列，通过user_id在缓存中搜索昵称, 避免关联用户表&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'用户'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nick_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;admin_user_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 手机列，通过user_id在缓存中搜索手机, 避免关联用户表&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;phone_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'手机号'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;admin_user_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 数据库存储的数字，布尔列&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bool_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;status_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'no'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 图片列&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;image_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 枚举值&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enum_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias_attribute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;enums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BaseValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias_attribute&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;enums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 时间戳转时间显示&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;time_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="八、新增额外行方法"&gt;八、新增额外行方法&lt;/h3&gt;
&lt;p&gt;提取通用的 row 操作，避免代码重复&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveAdmin::Views::Pages::AttributesTable&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'用户'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nick_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;admin_user_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;phone_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'手机号'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;admin_user_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bool_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;status_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'no'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 调用示例       image_row :avatar_qnniu, size: '50', style: 'border-radius: 50%'&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;image_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 枚举值&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enum_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias_attribute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;enums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BaseValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias_attribute&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;enums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;time_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="九、新增通用视图帮助类"&gt;九、新增通用视图帮助类&lt;/h3&gt;
&lt;p&gt;不同资源的列表页，当用 user_id 搜索时，都需要显示用户相关的一些按钮，可以提取成一个 helper 类，避免重复定义&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/concerns/show_user_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ShowUserHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;included&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:show_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:user_id_equals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
       &lt;span class="c1"&gt;# ......省略好些个按钮&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;authorized?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:view_operates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="s1"&gt;'日志'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="n"&gt;admin_operates_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;user_id_equals: &lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;authorized?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:generate_temp_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="s1"&gt;'生成临时密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="n"&gt;generate_temp_password_admin_user_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'data-method'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;ActiveAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_page&lt;/span&gt; &lt;span class="s1"&gt;'Operates'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;menu&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s1"&gt;'日志管理'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;parent: &lt;/span&gt;&lt;span class="s1"&gt;'系统管理'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;priority: &lt;/span&gt;&lt;span class="mi"&gt;54&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ShowUserHelper&lt;/span&gt;   &lt;span class="c1"&gt;# 包含helper， 这样上面那些按钮都有了&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="十、自定义页脚"&gt;十、自定义页脚&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/active_admin.rb&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;footer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_footer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;MyFooter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/my_footer.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyFooter&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveAdmin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Component&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Change Language'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="s1"&gt;'/admin/check_locale'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;
      &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Change Theme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="s1"&gt;'/admin/check_theme'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="十一、定制详情页的标题"&gt;十一、定制详情页的标题&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 对应model代码里定义实例方法&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;display_name&lt;/span&gt;
  &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这些实战，主要是通过打开类，修改 ActiveAdmin 源代码的方式操作的。&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Sun, 12 Jul 2020 16:58:22 +0800</pubDate>
      <link>https://ruby-china.org/topics/40138</link>
      <guid>https://ruby-china.org/topics/40138</guid>
    </item>
    <item>
      <title>100 行 Crystal 代码实现 Jbuilder</title>
      <description>&lt;h3 id="一、原理"&gt;一、原理&lt;/h3&gt;
&lt;p&gt;一个 jbuilder 文件，实际对应于一个特殊的哈希对象——Jbuilder 对象，在哈希对象上调用 to_json，便会生成 json 字符串。&lt;br&gt;
哈希生成原理：利用 method_missing, 转发不认识的实例方法，方法名作为键，第一参数作为值，支持块处理。&lt;br&gt;
布局原理：利用源码替换技术替换掉布局文件的&lt;strong&gt;yield_content&lt;/strong&gt; &lt;br&gt;
子视图原理：利用语言自带的 read_file 函数，编译期读取文件 &lt;br&gt;&lt;/p&gt;

&lt;p&gt;简单介绍下 Crystal 的编译期元编程， {{ }} 和 {%  %}是编译期宏的语法格式，长得很像 erb 模板。{{ }}里的东西在编译期会被输出，{% %}里的是编译期控制流，用来控制代码编译的。read_file、run、id 是编译期特有的方法，用于代码生成的。&lt;/p&gt;
&lt;h3 id="二、示例代码"&gt;二、示例代码&lt;/h3&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"jbuilder"&lt;/span&gt;

&lt;span class="n"&gt;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Jbuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;null&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;msg&lt;/span&gt; &lt;span class="s2"&gt;"ok"&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array!&lt;/span&gt; &lt;span class="s2"&gt;"array1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"array2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array!&lt;/span&gt; &lt;span class="s2"&gt;"array3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"custom_field"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;%w[1 2]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;plain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="三、样例输出"&gt;三、样例输出&lt;/h3&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"null"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"array1"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"array2"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"array3"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"custom_field"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="四、源代码"&gt;四、源代码&lt;/h3&gt;&lt;h4 id="src/jbuilder.cr"&gt;src/jbuilder.cr&lt;/h4&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"json"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./jbuilder/version"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./jbuilder/embed"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Jbuilder&lt;/span&gt;
  &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Integer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Int8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Int16&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Int32&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Int64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Int128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;UInt8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;UInt16&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;UInt32&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;UInt64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;UInt128&lt;/span&gt;
  &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;BasicType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Bool&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Float32&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Float64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Integer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Nil&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;
  &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BasicObject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;BasicType&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 定义类型&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 实际初始化方法， 返回一个哈希&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;
    &lt;span class="kp"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

 &lt;span class="c1"&gt;# 转发未定义的实例方法&lt;/span&gt;
  &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nf"&gt;method_missing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 如果调用非自带的感叹号方法，如merge!,  set!， 则抛出编译期异常&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ends_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
        &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"Values only allowed Array | Tuple | Hash | NamedTuple | &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;BasicType&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="p"&gt;%}&lt;/span&gt; 
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;

    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;block&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt; &lt;span class="c1"&gt;# 如果块存在，则新建一个jbuilder实例赋值给哈希键&lt;/span&gt;
      &lt;span class="n"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="no"&gt;Jbuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt; &lt;span class="c1"&gt;# 如果块不存在，直接赋值给哈希键&lt;/span&gt;
      &lt;span class="n"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;array!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 简单处理数组，第一参数作为键，第二参数才是值&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt;
    &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;array!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 使用块处理对象数组，第一参数作为键，第二参数才是值&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt;
    &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Jbuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@result.merge&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;array!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;array!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;BasicType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_h&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;
    &lt;span class="vi"&gt;@result&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="src/jbuilder/embed.cr"&gt;src/jbuilder/embed.cr&lt;/h4&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Jbuilder&lt;/span&gt;
  &lt;span class="c1"&gt;# 支持布局&lt;/span&gt;
  &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;layout_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;layout_file&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
      &lt;span class="c1"&gt;# 布局文件的yield_content在编译时会被子文件的源代码覆盖&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;io_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layout_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/yield_content/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;io_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
      &lt;span class="c1"&gt;# 不使用布局文件，嵌套一层Jbuilder.new&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;io_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./embedder.cr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;io_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="src/jbuilder/embedder.cr"&gt;src/jbuilder/embedder.cr&lt;/h4&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;Crystal&lt;/span&gt;&lt;span class="sh"&gt;
  # 读取文件
  Jbuilder.new do |json|
    &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
  end.to_json
&lt;/span&gt;&lt;span class="no"&gt;Crystal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="五、实际生产书写的代码如下"&gt;五、实际生产书写的代码如下&lt;/h3&gt;&lt;h4 id="布局文件  src/views/layouts/application.jbuilder"&gt;布局文件  src/views/layouts/application.jbuilder&lt;/h4&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="no"&gt;Jbuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;msg&lt;/span&gt;  &lt;span class="s2"&gt;"ok"&lt;/span&gt;
  &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;yield_content&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="主视图 src/views/applies/my_apply.jbuilder"&gt;主视图 src/views/applies/my_apply.jbuilder&lt;/h4&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array!&lt;/span&gt; &lt;span class="s2"&gt;"applies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;applies&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"src/views/applies/_base_apply.jbuilder"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local_updated_at&lt;/span&gt; &lt;span class="n"&gt;local_updated_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="局部视图 src/views/applies/_base_apply.jbuilder"&gt;局部视图 src/views/applies/_base_apply.jbuilder&lt;/h4&gt;&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_id&lt;/span&gt;     &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;         &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;         &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtitle&lt;/span&gt;     &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rand_number&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uri&lt;/span&gt;          &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uri&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;common_flag&lt;/span&gt;  &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;common_flag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目地址： &lt;a href="https://github.com/shootingfly/jbuilder" rel="nofollow" target="_blank"&gt;https://github.com/shootingfly/jbuilder&lt;/a&gt;&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Sun, 05 Jul 2020 23:56:03 +0800</pubDate>
      <link>https://ruby-china.org/topics/40115</link>
      <guid>https://ruby-china.org/topics/40115</guid>
    </item>
    <item>
      <title>如何看待 Ruby 自带的 http 库? Net::HTTP</title>
      <description>&lt;p&gt;乱七八糟的 API, Ruby 语言中最大的毒瘤 &lt;img title=":smiley:" alt="😃" src="https://twemoji.ruby-china.com/2/svg/1f603.svg" class="twemoji"&gt; &lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Mon, 29 Jun 2020 19:24:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/40043</link>
      <guid>https://ruby-china.org/topics/40043</guid>
    </item>
    <item>
      <title>gems.ruby-china.com 挂了</title>
      <description>&lt;p&gt;&lt;a href="/huacnlee" class="user-mention" title="@huacnlee"&gt;&lt;i&gt;@&lt;/i&gt;huacnlee&lt;/a&gt;  &lt;a href="/Rei" class="user-mention" title="@Rei"&gt;&lt;i&gt;@&lt;/i&gt;Rei&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;gems.ruby-china.com 挂了&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2020/4fb98f74-3360-49f6-a0fb-156ffd654e7d.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Thu, 09 Apr 2020 17:17:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/39726</link>
      <guid>https://ruby-china.org/topics/39726</guid>
    </item>
    <item>
      <title>微服务如何做聚合接口?</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;最近，APP 那边要求我们提供一个聚合接口来下发各种增量数据，从而避免进入 APP 首页需要调用众多获取数据接口。&lt;br&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;综合评估后，认为 APP 的要求合情合理。&lt;/p&gt;

&lt;p&gt;目前能考虑的想法，有两个。&lt;/p&gt;
&lt;h4 id="第一种"&gt;第一种&lt;/h4&gt;
&lt;p&gt;将该接口做在网关层，通过内部 http 请求，请求多个微服务，将数据聚合下发。&lt;/p&gt;
&lt;h4 id="第二种"&gt;第二种&lt;/h4&gt;
&lt;p&gt;目前我司的管理后台已单独提取出来做成了一个报表微服务，从而能访问多个数据库的数据。考虑在这个微服务里提供这个接口。&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;hr&gt;

&lt;p&gt;大家有什么建议？&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Thu, 09 Apr 2020 14:02:06 +0800</pubDate>
      <link>https://ruby-china.org/topics/39725</link>
      <guid>https://ruby-china.org/topics/39725</guid>
    </item>
    <item>
      <title>讨人厌的后缀表达式</title>
      <description>&lt;p&gt;写了几年 Ruby 了，最烦遇上一长串的代码后面给你来个 if 条件 &lt;img title=":disappointed:" alt="😞" src="https://twemoji.ruby-china.com/2/svg/1f61e.svg" class="twemoji"&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;check_form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;archive_score: &lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;higher_content: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:higher_content&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'WAIT_COMMENT'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家是怎么看待后缀表达式的？&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Thu, 26 Mar 2020 22:00:50 +0800</pubDate>
      <link>https://ruby-china.org/topics/39674</link>
      <guid>https://ruby-china.org/topics/39674</guid>
    </item>
    <item>
      <title>.com 域名搜索和分页都有 bug</title>
      <description>&lt;p&gt;ruby-china.com 的搜索和分页都是坏的，而 ruby-china.org 都是好的。两份代码吗？&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Fri, 23 Nov 2018 22:26:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/37814</link>
      <guid>https://ruby-china.org/topics/37814</guid>
    </item>
    <item>
      <title>通过测试代码, 自动生成 postman 文件</title>
      <description>&lt;h2 id="0 最终成果预览"&gt;0 最终成果预览&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2018/0bc1af85-56e5-4d95-b0f9-1c6b9152241b.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="1 实现思路"&gt;1 实现思路&lt;/h2&gt;
&lt;p&gt;自动化接口测试的同时，通过打日志的形式构建 postman 格式的文本文件&lt;/p&gt;
&lt;h2 id="2. 编写rake任务"&gt;2. 编写 rake 任务&lt;/h2&gt;&lt;h3 id="lib/postman.rake"&gt;lib/postman.rake&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:postman&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;admin: :environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'doc/admin_postman.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:admin_postman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'轻牛+后台管理接口'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;
    {
      "variables": [],
      "info": {
        "name": "&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
        "_postman_id": "bce94da3-8aa9-c38e-bdb5-fe8835715887",
        "description": "",
        "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
      },
      "item": [
&lt;/span&gt;&lt;span class="no"&gt;  JSON&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;admin_postman&lt;/span&gt;
  &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'GENERATE_POSTMAN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;
  &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test/controllers/admin/'&lt;/span&gt;
  &lt;span class="n"&gt;order_controllers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[
    sessions_controller_test.rb
    managers_controller_test.rb
  ]&lt;/span&gt;
  &lt;span class="n"&gt;order_controllers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="sb"&gt;`rails test &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="si"&gt;}#{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="3.编写测试helper"&gt;3.编写测试 helper&lt;/h2&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'base_helper'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'timecop'&lt;/span&gt;

&lt;span class="no"&gt;Timecop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2018&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# 冻结测试时的时间&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AdminTestHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;postman_logger&lt;/span&gt;
    &lt;span class="vi"&gt;@postman_logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/doc/admin_postman.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@postman_logger.formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;proc&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_progname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@postman_logger&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Add more helper methods to be used by all tests here...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;generate_doc_flag: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;code: &lt;/span&gt;&lt;span class="s1"&gt;'200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;login_flag: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'authorization'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_gw_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;auth_token: &lt;/span&gt;&lt;span class="s1"&gt;'m2TfHdaADecAEscTT4FzJ48sz'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;login_flag&lt;/span&gt;
    &lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;headers: &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="s1"&gt;'200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@response.body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;generate_postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'GENERATE_POSTMAN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;generate_doc_flag&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@controller.action_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'admin.action_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locale: &lt;/span&gt;&lt;span class="s1"&gt;'zh-CN'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;postman_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pretty_generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;request: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;raw: &lt;/span&gt;&lt;span class="vi"&gt;@request.original_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'http://www.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{{WEBSITE}}'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'{{HOST}}'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;port: &lt;/span&gt;&lt;span class="s1"&gt;'{{PORT}}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="vi"&gt;@request.path.split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="ss"&gt;query: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;equals: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="ss"&gt;method: &lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;header: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s1"&gt;'AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s1"&gt;'{{token}}'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s1"&gt;'application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;body:
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt;
          &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="s1"&gt;'urlencoded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;urlencoded: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;enabled: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;response: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;postman_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admin::BaseControllerTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IntegrationTest&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AdminTestHelper&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="4. 编写测试代码"&gt;4. 编写测试代码&lt;/h2&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'admin_test_helper'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admin::SessionsControllerTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Admin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseControllerTest&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s1"&gt;'admin_login'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin_login_admin_sessions_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;username: &lt;/span&gt;&lt;span class="n"&gt;managers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:one&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'123456'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="5.运行rake任务, 在postman里导入生成的postman.json"&gt;5.运行 rake 任务，在 postman 里导入生成的 postman.json&lt;/h2&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rake postman:admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;p.s 最后把文档导入 Insomnia(更简洁的 api 工具，兼容 postman), 不是 postman&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Wed, 17 Oct 2018 19:45:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/37640</link>
      <guid>https://ruby-china.org/topics/37640</guid>
    </item>
  </channel>
</rss>
