公司动态

Java+Spring Boot构建六爻排盘系统:算法、接口与小程序实战

📅 2026/8/29 2:50:52
Java+Spring Boot构建六爻排盘系统:算法、接口与小程序实战
简介六爻排盘是传统文化数字化中颇具代表性的场景其本质是将阴阳爻、五行生克等规则转化为可计算的数据模型。二进制表示卦象、随机数模拟铜钱正反是程序实现起卦逻辑的基础。后端采用Spring Boot构建分层服务通过静态映射表完成六十四卦、世应、六亲等复杂信息的快速检索并以RESTful接口向前端输出标准化排盘结果。微信小程序负责摇卦交互动画与卦象渲染前后端职责清晰。工程实践层面还涉及随机序列重复、日柱基准校验以及低版本iOS兼容性等问题的处理为Java开发者及传统文化类小程序项目提供了一套从算法建模到部署落地的完整参考。 启动这个项目之前我在技术社群里翻到好几条类似的提问有没有 Java 写的六爻排盘开源项目市面上的回答大多给的是 Python 脚本或者纯前端 Demo能直接在微信小程序里落地、后端还能成套复用的几乎没有。去年我花了一周时间从起卦逻辑到排盘模块再从 Spring Boot 接口写到小程序界面把整套完整源码跑通了。今天这篇就把核心设计思路和关键实现拆开来讲适合正在做传统文化类小程序、或者想用 Java 练手完整业务链的开发者参考。1. 六爻起卦的逻辑模型从三枚铜钱到二进制数据1.1 爻是什么卦又是什么六爻占卜里最小的单位是“爻”每个爻只表达两种状态阴和阳。六个爻从下往上叠加形成一个卦象。这个结构天然适合程序表达——阴是 0阳是 1一个卦就是一行六位二进制数字。但六爻不止“静态卦象”这一层。传统起卦过程中每一爻还会额外产生一个“动”或“静”的属性老阴和老阳属于动爻动爻会变变完之后得到一个新的卦叫变卦。所以一次完整的六爻起卦数据上至少包含三块内容本卦六个爻的阴阳组合动爻哪些位置发生了变化变卦本卦中动爻反转后得到的新卦这一层拆清楚之后后面的数据模型和接口设计都会顺畅很多。1.2 三枚铜钱在程序里对应什么随机过程传统摇卦用三枚铜钱摇六次每次得到一爻。三枚铜钱的正反面组合只有四种结果组合情况点数名称动变属性三个字无背6老阴动爻变阳一背两字7少阳静爻两背一字8少阴静爻三背无字9老阳动爻变阴从概率上看7 和 8 的出现概率各为 3/86 和 9 各为 1/8这正好对应传统六爻起卦的“阴阳平衡、动少静多”原则。程序里模拟一枚铜钱只需要生成一个 0 或 1 的随机数然后统计三枚的总和就可以映射到上表。1.3 从六次结果组装本卦和变卦第一次摇出来的爻放在最底下第六次摇出来的爻放在最上面。Java 里可以用一个长度为 6 的整数数组来存六爻数据数组下标 0 表示初爻下标 5 表示上爻。组装本卦的规则很简单6 和 8 视为阴爻记 07 和 9 视为阳爻记 1。变卦则看动爻值为 9 的阳爻变阴值为 6 的阴爻变阳等于做了一个异或操作。这一步的核心代码很直白public class TossResult { private int[] originalValues new int[6]; // 存 6/7/8/9 private int[] benGua new int[6]; // 本卦0阴1阳 private int[] bianGua new int[6]; // 变卦0阴1阳 private ListInteger movingPositions new ArrayList(); public void buildGua() { for (int i 0; i 6; i) { int v originalValues[i]; // 6老阴、7少阳为阴8少阴、9老阳为阳 benGua[i] (v 6 || v 8) ? 0 : 1; // 动爻反转得到变卦 if (v 6 || v 9) { bianGua[i] benGua[i] 0 ? 1 : 0; movingPositions.add(i); } else { bianGua[i] benGua[i]; } } } }到这一步起卦核心已经完成百分之四十了。接下来要处理的是“这个卦叫什么、五行属性是什么、世应落在哪里”这些信息全都来自卦象数据本身。2. 后端架构Spring Boot 起卦引擎的分层设计与实现2.1 小程序不能让 Java 直接跑前后端职责怎么切微信小程序的前端运行在 JavaScript 引擎里没法直接执行 Java 代码。所以“Java 代码实现小程序”的准确含义是Java 负责后端接口和排盘引擎小程序负责交互和渲染。我选择 Spring Boot 2.7 作为后端框架原因很简单内置 Tomcat打一个 jar 包就能部署Controller 层做接口转发非常省事后续如果需要接数据库存历史记录JPA 或 MyBatis 都能无缝整合整个后端分为三层Controller 层接收小程序请求校验参数返回封装结果Service 层起卦、排盘、计算的业务逻辑Model 层爻、卦、排盘结果的数据模型2.2 起卦引擎的接口设计前端有两种起卦交互方式一种是手动每次摇一爻另一种是一次性自动起卦。我的后端接口同时支持这两种方式。手动摇卦接口每次只接收一枚爻的结果前端摇完六次后统一请求/api/paipan完成排盘。自动起卦则直接调/api/auto后端一次性生成六爻并返回完整排盘数据。RestController RequestMapping(/api/liuyao) public class DivinationController { PostMapping(/toss) public ResultTossResponse toss() { int value DivinationUtil.tossThreeCoins(); return Result.success(new TossResponse(value)); } PostMapping(/paipan) public ResultPaipanResult paipan(RequestBody ListInteger values) { if (values null || values.size() ! 6) { return Result.fail(必须提供6个爻的数据); } return Result.success(paipanService.buildFullResult(values)); } PostMapping(/auto) public ResultPaipanResult auto() { int[] values DivinationUtil.tossSixTimes(); return Result.success(paipanService.buildFullResult(values)); } }这里注意一个设计细节/toss接口每次返回的是 6 到 9 之间的整数而不是前端直接算出阴阳这样前端只负责展示动画所有判定逻辑都收口在后端避免因为客户端版本差异导致排盘结果不一致。2.3 随机性实现这题没你想的那么简单用Math.random()生成三枚铜钱完全够用但有两个细节会影响体验。第一是线程安全。Math.random()内部用了Random的静态实例并发场景下没问题。但如果用java.util.Random实例去并发调用可能出现竞争问题。我的做法是用ThreadLocalRandom.current().nextInt(0, 2)性能和安全性都更好。第二是随机序列的用户感知问题。六爻起卦一次生成 6 个爻出现完全相同卦象的概率是 1/64其实并不低。很多用户会在短时间内反复起卦测试如果连续两次摇到同一个卦就会觉得程序“有 bug”。我在前端做了提示告诉用户连续摇到相同卦象是正常的随机结果同时后端记录每一次起卦的完整参数方便排查。摇卦的核心实现public class DivinationUtil { public static int tossThreeCoins() { int yangCount 0; for (int i 0; i 3; i) { yangCount ThreadLocalRandom.current().nextInt(0, 2); } // 0个阳面 6老阴1个阳面 7少阳2个阳面 8少阴3个阳面 9老阳 int[] mapping {6, 7, 8, 9}; return mapping[yangCount]; } public static int[] tossSixTimes() { int[] values new int[6]; for (int i 0; i 6; i) { values[i] tossThreeCoins(); } return values; } }2.4 六十四卦映射表与卦辞数据得到本卦的六个 0/1 数据之后下一步要映射到具体的卦名。这一步我强烈建议用静态映射表不要试图用算法去推算卦名。六十四卦的排列虽然有规律但其中涉及八卦相叠的规则运行时推算的代码量反而比查表更大。映射表的结构public class GuaDict { // key: 从初爻到上爻的0/1字符串value: 八卦信息字段 public static final MapString, String[] HEXAGRAM_INFO new HashMap(); static { // 数据格式: {卦名, 所属宫, 五行属性, 世爻位置, 应爻位置} HEXAGRAM_INFO.put(111111, new String[]{乾为天, 乾宫, 金, 5, 2}); HEXAGRAM_INFO.put(000000, new String[]{坤为地, 坤宫, 土, 5, 2}); // 剩余62卦类似... } public static String getGuaName(String benGuaKey) { String[] info HEXAGRAM_INFO.get(benGuaKey); return info null ? 未知卦 : info[0]; } }查表的好处有三个一是运行时性能最好二是便于核对排盘结果正确性遇到疑问直接对表检查三是后续要扩展卦辞、爻辞内容只需要在这张表里加字段即可。我在项目里把每个卦的卦名、卦辞、大象辞都存到了同一张表避免散落多处。3. 排盘不只是卦象世应、六亲、六神的计算模块3.1 世爻和应爻根据八宫卦序定位世应位置是排盘信息里最直观的两个标记但它们的定位逻辑相对隐蔽。传统上世应是根据“八宫卦序”来确定的每个宫有八个卦分别对应不同世位本宫卦上世世在第六爻一世卦世在第一爻二世卦世在第二爻三世卦世在第三爻四世卦世在第四爻五世卦世在第五爻游魂卦世在第四爻归魂卦世在第三爻应爻位置和世爻位置之间隔两个爻位也就是对应的规律世在初爻则应四爻世在二爻则应五爻世在三爻则应上爻以此类推。代码里计算应爻直接用公式(shi 3) % 6。用静态映射表存储世爻位置通过查表获取public class PaipanService { public int getShiPosition(String benGuaKey) { String[] info GuaDict.HEXAGRAM_INFO.get(benGuaKey); if (info null) { return -1; } return Integer.parseInt(info[3]); } public int getYingPosition(int shiPosition) { return (shiPosition 3) % 6; } }3.2 纳甲与五行给每个爻配上地支要算六亲必须先知道每个爻的地支五行。这里的规则是“纳甲法”不同的卦准确说是不同宫六个爻会配上特定的十天干和十二地支。以乾宫八卦和坤宫八卦为例卦宫内卦三爻初、二、三外卦三爻四、五、上乾宫子、寅、辰午、申、戌坤宫未、巳、卯丑、亥、酉震宫子、寅、辰午、申、戌巽宫丑、亥、酉未、巳、卯坎宫寅、辰、午申、戌、子离宫卯、丑、亥酉、未、巳艮宫辰、午、申戌、子、寅兑宫巳、卯、丑亥、酉、未地支五行属性是固定的子水、丑土、寅木、卯木、辰土、巳火、午火、未土、申金、酉金、戌土、亥水。这一块在项目里直接用两张静态映射表完成不需要计算public class NaJiaUtil { private static final MapString, String[] GONG_ZHI new HashMap(); static { // 乾宫: 从初爻到上爻对应的地支 GONG_ZHI.put(乾宫, new String[]{子, 寅, 辰, 午, 申, 戌}); GONG_ZHI.put(坤宫, new String[]{未, 巳, 卯, 丑, 亥, 酉}); // 其他宫的纳甲数据类似... } private static final MapString, String ZHI_WUXING new HashMap(); static { ZHI_WUXING.put(子, 水); ZHI_WUXING.put(丑, 土); ZHI_WUXING.put(寅, 木); ZHI_WUXING.put(卯, 木); ZHI_WUXING.put(辰, 土); ZHI_WUXING.put(巳, 火); ZHI_WUXING.put(午, 火); ZHI_WUXING.put(未, 土); ZHI_WUXING.put(申, 金); ZHI_WUXING.put(酉, 金); ZHI_WUXING.put(戌, 土); ZHI_WUXING.put(亥, 水); } public static String getZhi(String gong, int position) { return GONG_ZHI.get(gong)[position]; } public static String getWuXing(String zhi) { return ZHI_WUXING.get(zhi); } }3.3 六亲计算五行生克的四种关系六亲是排盘结果里最常被用户关注的一栏包括兄弟、父母、子孙、官鬼、妻财。它们的判断依据是本卦所属宫的五行和当前爻的地支五行之间的生克关系。以宫五行为“我”与我同类兄弟生我者父母我生者子孙克我者官鬼我克者妻财五行生克关系表我方五行生我我生克我我克金土水火木木水火金土水金木土火火木土水金土火金木水Java 实现时可以写成一个五行关系映射或者直接用一个二维数组判断public class LiuQinUtil { private static final MapString, Integer WUXING_INDEX Map.of( 木, 0, 火, 1, 土, 2, 金, 3, 水, 4 ); // 五行相生木生火、火生土、土生金、金生水、水生木 private static final int[] SHENG {1, 2, 3, 4, 0}; // 五行相克木克土、土克水、水克火、火克金、金克木 private static final int[] KE {2, 4, 1, 3, 0}; public static String getLiuQin(String gongWuXing, String yaoWuXing) { int gongIdx WUXING_INDEX.get(gongWuXing); int yaoIdx WUXING_INDEX.get(yaoWuXing); if (gongIdx yaoIdx) { return 兄弟; } if (SHENG[gongIdx] yaoIdx) { return 子孙; } if (SHENG[yaoIdx] gongIdx) { return 父母; } if (KE[gongIdx] yaoIdx) { return 妻财; } if (KE[yaoIdx] gongIdx) { return 官鬼; } return 未知; } }3.4 六神排布与日柱计算六神的起始位置取决于起卦当天的日干因此需要先算出日柱的天干。六神从初爻到上爻依次排列顺序是固定的青龙、朱雀、勾陈、螣蛇、白虎、玄武。起始六神由日干决定日干初爻六神甲、乙青龙丙、丁朱雀戊勾陈己螣蛇庚、辛白虎壬、癸玄武日柱的计算我采用基准日思路以 1900 年 1 月 1 日为已知基准该日为甲戌日干支序号第 11计算目标日期与基准日的天数差加上基准序号之后对 60 取模得到目标日的干支序号。public class GanZhiUtil { private static final LocalDate BASE_DATE LocalDate.of(1900, 1, 1); private static final int BASE_GANZHI_INDEX 11; // 甲戌日的六十甲子序号甲子为1 public static int getDayGanZhiIndex(LocalDate date) { long days ChronoUnit.DAYS.between(BASE_DATE, date); int index (int) ((BASE_GANZHI_INDEX days) % 60); if (index 0) { index 60; } return index; } public static String getDayGan(LocalDate date) { int index getDayGanZhiIndex(date); String[] ganzhi buildGanZhiArray(); return ganzhi[index - 1].substring(0, 1); } }需要提醒的是这类基准日推算法依赖基准点的准确性不同历法资料对同一公历日期的干支记录可能存在差异。实际项目里最好用国家标准的历法数据做一次校验或者把日柱计算直接内置到后端不允许前端传值避免数据源不一致导致的排盘偏差。4. 小程序前端摇卦交互、卦象绘制与接口对接4.1 摇卦页面一次点击摇一爻小程序端用的是原生框架没有引入额外 UI 库。摇卦页的核心是管理“已摇次数”和“爻值列表”这两个状态。页面交互逻辑用户点击铜钱区域触发一次wx.request调用/api/toss收到返回值后把爻值追加到数组中用 CSS 动画展示铜钱翻转效果摇满六次之后按钮从“摇卦中”变成“查看排盘结果”Page({ data: { tossedCount: 0, values: [], canToss: true, }, tossCoin() { if (!this.data.canToss) return; if (this.data.tossedCount 6) return; wx.request({ url: https://your.domain.com/api/liuyao/toss, method: POST, success: (res) { if (res.data.code 0) { const value res.data.data.value; this.setData({ values: [...this.data.values, value], tossedCount: this.data.tossedCount 1, }); } }, }); }, goPaipan() { if (this.data.tossedCount 6) return; wx.request({ url: https://your.domain.com/api/liuyao/paipan, method: POST, data: this.data.values, success: (res) { const result res.data.data; wx.navigateTo({ url: /pages/result/result?data${encodeURIComponent(JSON.stringify(result))}, }); }, }); }, });4.2 结果页的卦象绘制排盘结果的卦象可以用 CSS 绘制也可以用 Canvas。我用的是普通 view 组件因为阴阳爻就是一根直线和中间断开的直线用两个圆角矩形就能拼出来。绘制逻辑阳爻一个横向长条阴爻左右两个短横条中间留空动爻在爻的右侧加一个圆圈标记view classgua-panel view classgua-column view classgua-title本卦/view view classyao-row wx:for{{benGua}} wx:keyindex view classyao {{item 1 ? yang : yin}}/view view wx:if{{movingPositions.includes(index)}} classmoving-mark/view /view /view view classgua-column view classgua-title变卦/view view classyao-row wx:for{{bianGua}} wx:keyindex view classyao {{item 1 ? yang : yin}}/view /view /view /viewCSS 里对应.yao { width: 120px; height: 8px; background: #333; border-radius: 2px; } .yao.yin { width: 50px; margin-left: 35px; background: #333; }阴爻的左右两段做法可以用外层容器宽度固定内部用::before和::after生成两段短横条。不过小程序对伪元素的支持没问题直接用伪元素就行。4.3 排盘明细表的渲染排盘结果页底部是一个六行表格每行对应一个爻位展示的信息包括六神、六亲、地支五行、世应标记、爻性、变爻后六亲。我给后端返回的数据结构预留了一个按爻位组装好的列表{ position: 0, liuShen: 青龙, liuQin: 父母, zhi: 子, wuXing: 水, shiYing: 世, value: 9, bianValue: 7, bianLiuQin: 父母 }前端拿到之后直接wx:for渲染成一个表格不需要再做任何逻辑计算。这样既能保证展示一致也方便后续增加更多字段。5. 实测踩坑随机数分布、日柱基准与兼容性问题5.1 摇卦接口连续出现相同卦象用户以为程序坏了联调阶段遇到一个很有意思的反馈测试人员在手动摇卦时连续两次摇到了同样的卦象立刻提了 Bug——随机数是不是没用对从概率上讲六十四卦中任意两次结果相同的概率是 1/64算不上极低。但用户的直觉会放大这种巧合。而且如果用的是默认的Random种子在某些 JVM 版本上启动初期确实可能出现短时间内的伪随机重复序列。我做了两层处理后端改用ThreadLocalRandom.current()并且不手动设置种子交给系统随机源前端摇卦过程中加入 300ms 的最低网络过渡动画让每次摇卦之间有时间差感另外在自动起卦接口里增加了“连续生成相同卦象则重新摇”的逻辑最多重试三次这样很少会让用户连续两次看到一模一样的结果体验上舒服很多。5.2 日柱推算结果和手机日历不一致日柱计算用基准日偏移法省事但有一个问题基准日的干支必须可靠。我在初版代码里使用的是某网络博客提供的“1900 年 1 月 1 日为甲戌日”后来对比手机自带日历发现差了一天。排查过程很简单取 2024 年 1 月 1 日的日柱用基准日推算法算出结果再和多个万年历 App 校对。发现确实偏移一位之后我重新确认了标准历法数据修正了基准干支序号。这个坑要给所有做干支历法相关项目的开发者提个醒不要盲目相信网上的基准日数据至少要拿最近的 100 个日期去和多款权威日历对照。我当时写了一个单元测试直接断言 2024 年的日柱序列之后每次改动都能自动回归验证。5.3 小程序端旧版本 iOS 的 flex 布局兼容问题排盘结果页的卦象区用了display: flex横排本卦和变卦。在最新的安卓和 iOS 设备上都没问题但在 iOS 14 以下的部分机型上flex 容器里的width: 50%偶尔会不生效导致两个卦叠在一起。解决方式是给右侧变卦区域也加上flex-shrink: 0并显式指定min-width。这类兼容性 bug 不好提前发现建议在发布前用低版本 iOS 的模拟器或者真机样本过一遍核心页面。注意微信开发者工具里的渲染结果和真机有差异尤其是 flex 布局和 CSS 伪元素。项目验收一定要以真机预览为准。5.4 排盘数据正确性怎么验证排盘模块最容易写错的是世应位置和六亲关系。我的经验是产出一张“测试卦例表”至少覆盖八个宫、八种世位每个宫取一个典型卦人工标好预期结果然后跑单元测试进行断言。比如乾宫的“天风姤”世在一爻、应在四爻宫五行金初爻子水为子孙二爻寅木为妻财三爻辰土为父母四爻午火为官鬼五爻申金为兄弟上爻戌土为父母。我把这类用例直接写进测试代码每次改了排盘模块就全量跑一遍。刚开始做这类项目时很容易陷入“把功能跑通就行”的思维但排盘这种确定性计算数据表一旦写错用户看到的整个盘面都是歪的而且外行根本看不出来。自动化测试是这里最值得花时间的环节。5.5 数据表维护的顺带建议六十四卦的静态映射表很庞大手工录入容易出错。我用的方法是先写一个临时 Python 脚本从权威资料里提取数据生成 Java 代码里的三维数组字符串再贴到项目里。不要手动逐行 copy尤其是世应位置和纳甲地支错一个后面排查要花非常多的时间。如果后面要给项目加功能比如保存起卦历史、按卦名搜索建议把这份静态表迁移到数据库里。接口层不用变只是把 GuaDict 的读取改成从缓存或 DB 加载业务逻辑完全解耦这是一条非常舒服的演进路径。从起卦引擎到小程序渲染这套源码的完整链路不算复杂但每一步都踩过实实在在的坑。尤其是排盘这类文化知识密集型的模块除了编程能力更需要耐心核对传统规则。好在这些规则都能用数据和表格固化下来一旦理清后续扩展的空间非常大。本文还有配套的精品资源点击获取