构建实录 / 2026-06-07
funtest:自研零依赖紫微排盘引擎,和一条完整的内容变现链路

funtest 是一个测评平台:古代人生穿越、世界杯女主人格、紫微斗数排盘,这类适合发小红书的"测一测"。到现在做了 16 款产品,跑在一套 JSON 配置引擎上,核心是一个零依赖手写的紫微排盘算法。
题材轻,很多人觉得工程也轻——套个模板几个页面。做到一半发现完全不是。这个项目真正验证的是:一个人能不能把内容型产品从选题、视觉、算法、结果页,一直做到兑换码、分销、后台,整条商业链路跑通。测试是入口,链路才是产品。这篇按链路的三层——排盘引擎、配置引擎、变现体系——逐层拆代码。
一、为什么排盘引擎要自己手写
紫微斗数系列是重头。做这类内容有条捷径:结果页写一堆放之四海皆准的话,谁测都觉得"有点准"。用户分得出来,这种看两个就腻。
我要结果由用户真实命盘驱动:输入生辰,实打实排出命宫、身宫、五行局、十四主星、本命四化、大限。市面上有现成排盘库,但我决定自己写,理由有三:依赖——现成库拖一串依赖进来、部署排查都变重,自己写能做到运行时零依赖(公历转农历用离线数据表,不调任何第三方);可控——规则在自己手里,出错定位到行,调库出错只能猜;规则本身不难,难的是细致——紫微排盘是一套确定性历法计算,值得亲手过一遍。
安星层的核心公式
整个排盘核心 ziwei-cast.js 只有 144 行,仅用 JS 内置,零 npm 包。几个关键步骤:
五虎遁定寅宫天干、推十二宫。 年干经五虎遁映射到寅宫天干,再逐宫推:
function yinPalaceStem(yearStem) {
return [2, 4, 6, 8, 0][yearStem % 5]; // 甲己丙·乙庚戊·丙辛庚·丁壬壬·戊癸甲
}
function palaceStem(branch, yearStem) {
return mod(yinPalaceStem(yearStem) + mod(branch - 2, 12), 10);
}
纳音定五行局。 60 甲子的纳音五行查表,映射到水二/木三/金四/土五/火六局——五行局决定紫微起点的步长。
紫微安星:五行局 + 农历日 → 紫微地支。 这是最核心的一步,商余定位:
function ziweiPosition(ju, day) {
const quotient = Math.ceil(day / ju);
const remainder = quotient * ju - day;
const base = mod(2 + quotient - 1, 12);
return remainder % 2 === 0 ? mod(base + remainder, 12) : mod(base - remainder, 12);
}
十四主星对称布列。 紫微系六星逆布、天府系八星顺布,天府与紫微以寅申为轴对称(fu = mod(16 - ziwei, 12)):
put(ziwei, '紫微'); put(mod(ziwei-1,12), '天机'); put(mod(ziwei-3,12), '太阳');
put(mod(ziwei-4,12), '武曲'); put(mod(ziwei-5,12), '天同'); put(mod(ziwei-8,12), '廉贞');
const fu = mod(16 - ziwei, 12);
put(fu, '天府'); put(mod(fu+1,12), '太阴'); put(mod(fu+2,12), '贪狼'); /* …七杀、破军 */
本命四化由年干查 SI_HUA_TABLE(10 个年干 → 化禄/权/科/忌 四星),大限含宫干四化。规则约束参考倪海厦体系,落成常量表。
怎么知道排得对
准备了一批已知正确答案的样本校验:100 个样本全部通过;再用 1200 组随机生辰压测,跑完零异常。这两个数字不证明引擎完美,但说明基础盘稳——排盘这东西,用户拿自己生辰一验就知道有没有糊弄。
二、配置引擎:新增一款测试不是一次开发
16 款产品如果每款都写代码,平台会死在维护上。所以测试引擎是 JSON 配置驱动,每款产品一个目录、三个文件:
app/products/{id}/
questions.json # 题目、选项、多维打分 / 或生辰输入项
results.json # 结果映射与文案
theme.json # 视觉主题
打分是多维累加的。答题型每个选项往多个维度加分,最后取峰值组合出结果——比如古代穿越把选项映射到"朝代 × 身份"两个维度:
{ "id": "A", "text": "先列大纲,告诉自己这次一定优雅完成。",
"scores": { "dynasty.song_north": 2, "identity.scribe": 2 } }
生辰型则配输入项和解析维度,交给排盘引擎。加一款测试的动作因此更像"上架一个商品":写配置、传素材、后台预览、发布——不碰一行引擎代码。
产品状态机:配置定义产品,DB 决定上架
上下架有个容易踩的坑:产品状态若写死在配置文件里,每次上下架都要改文件、重新部署,生产环境迟早出事。
我在数据库里做了一层状态覆盖。有效状态的解析是一个三级 fallback:
// 产品有效状态 = DB 覆盖(若有)否则 registry.status 否则 draft
function effectiveStatus(entry, overrides) {
return overrides[entry.id] || entry.status || 'draft';
}
function setProductStatus(productId, status) {
// 写 product_status 表:INSERT OR REPLACE (product_id, status, updated_at)
}
配置文件定义产品是什么,product_status 表决定它现在是不是上架。后台点一下即时生效,代码和配置都不动。新测试默认 draft 不发布,运营用免兑换码的预览通道审完,一键上架。
三、变现体系:不是最后再加的功能
很多内容产品先做内容、火了再想变现。我反过来,把变现当成 V1 的一部分。code-service.js 有 662 行,三套东西:
批量兑换码(配合小红书发货主路径)。按批次表 + 码表两级建模,生成时循环防撞库:
let code = generateCode();
while (db.prepare('SELECT id FROM redeem_codes WHERE code = ?').get(code)) code = generateCode();
// INSERT redeem_codes (code, batch_id, status='unused', partner_id, product_id, ...)
每个码带 status(unused/used)、batch_id(按批次管理)、partner_id(分销归属)、product_id(限定可兑产品)——四个字段撑起"哪个渠道、哪一批、兑哪个产品、用没用过"的全链路追踪。
长期访问 Key(access key):给需要反复查看结果的用户和渠道活动用,带 product scope 正则校验,一个 key 能限定可访问的产品范围。
分销商后台:渠道用 partner session 登录,只看自己的 partner_id 数据和分账。
这三套加上数据分析和物料单,后台才算完整。它们不炫,但决定了这个东西是"作品"还是"生意"。
四、视觉:结果页是传播的燃料
结果页要能截图分享,这是小红书内容产品的命根子。用户测完愿不愿意发出去,就看那张卡好不好看。
所以结果页做了六维特质雷达、真排盘的命盘图,紫微系列还给十四主星每个都配了角色立绘,随用户主星变化。整套视觉走统一国风,封面、配图、立绘都是 AI 生成后人工筛。这部分花的时间远超预期,但它是传播的燃料,省不得——配套还有资产 manifest(assets.manifest.json)管理每款产品的素材清单。
技术选型与取舍
| 决策 | 取舍 |
|---|---|
| 排盘引擎手写、零依赖 | 部署轻、可定位;代价是自己实现历法计算(农历表可插拔) |
| SSR + SQLite 单机 | 一个进程 + 一个库文件,随便搬;写并发是规模上限 |
| 测试引擎 JSON 配置驱动 | 上新不写码;代价是配置协议要设计得足够通用 |
| 产品状态 DB 覆盖配置 | 上下架不改文件不重部署;生产安全 |
| 变现体系进 V1 | 一开始就能收钱;代价是 MVP 工作量更大 |
现在的状态与下一步
平台上线在 funtest.silliter.com。产品矩阵 16 款,分三大系列 + 独立款:
| 系列 | 数量 | 说明 |
|---|---|---|
| 紫微斗数 | 6 | 主星人格、正缘、搞钱、格局、流年、合盘——共用排盘引擎 |
| 森林人格 | 6 | 鹿/狐/鸮/兔/龟等,答题型多维打分 |
| 独立款 | 4 | 古代人生穿越、世界杯女主、MBTI、情绪疗愈 |
古代穿越、世界杯这类答题型已对外开放;紫微 6 款开发和压测都完了,走 draft → 预览审核 → 上架 逐款发布。技术上整站 SSR + SQLite、零框架依赖,部署就是一个进程加一个库文件。
接下来的工作不在代码里。测试数量暂时够了,缺的是内容运营:选题上新节奏、结果卡传播优化、兑换码渠道的转化数据。这些比写引擎慢多了,也没有"100/100 通过"这种爽快的验收——但生意的部分本来就是这样。整条链路已经证明能跑通,剩下的是把它跑起来。