微信答题小程序判题本地化与自建后端迁移:零漂移双跑对拍的工程复盘

我做前端十年,最近在业余做一个宝可梦答题小程序(微信小程序 + 云开发)。它最早 9 个每日玩法的判题全走云函数,每次猜都打服务端。体量上来后两个问题绕不开:一是 QQ 里的小程序走不通云开发(CloudBase 不互通),二是云函数每月有 20 万次调用上限。于是我决定做两件事——把判题本地化、把后端迁到自建的 Fastify 服务,而且要做到"用户完全无感"。

判题本地化:让取题公式成为唯一事实源

判题下沉到小程序本地时,最容易踩的坑是"前端一份池子、后端一份池子,某天答案对不上"。我的解法是把取题公式当单一事实源:本地不重算,只用云端 get 下发的 dexId / abilityId 去查包内 data/pokedex.js / data/abilities.js。数独玩法早就这么设计,这次把剪影、特性、图鉴三个玩法也对齐。

// 本地判题只查包内数据,不重算取题公式
const target = POKEDEX[dexId];        // 云端已下发 id
const board = buildClues(target);     // 逐字移植云端 buildClues 的 6 列对比板

验证做狠了一点:用 Node 把本地模块和云端 core.js 各跑一遍再逐字比对,ability 314 条、dex 722 池子 + 3480 组对比板逐字相同、silhouette 1025 池子 全部一致,零漂移。这一步后来成了后端迁移的护城河。

后端迁移:真·双跑对拍

后端迁到 Fastify 5 + TypeScript + Prisma 6 + SQLite,上线 pokemon.alleria.cn(PM2 + nginx 反代,Let's Encrypt 自动续期)。为了证明和云函数行为一致,我把对拍升级成"真·双跑":桩掉 wx-server-sdk 后直接调用云函数 main(),与后端 handler 做整对象逐字对比,覆盖 9 玩法 × common/replay × 4 种记录状态 + targetId 不一致用例,共 138 项全绿。

对拍还顺手逮到一个真 bug:云端 8 个玩法的揭晓卡都带 rec.targetId === target.id 守卫,而自建后端 Record 表没有 targetId,这句恒为 false,揭晓卡永远不出现。我把 9 个 handler 一律去掉该守卫,并在审计用例里钉死,谁改回去立刻报错。

// 迁移前(自建后端恒 false,揭晓卡不弹)
if (rec.targetId === target.id) showReveal();
// 迁移后:目标由日期确定性推导,结构不会错题,去掉守卫

两个关键取舍与踩坑

迁移有个取舍我特意写进文档:不做服务端权威重算,信任客户端上报的 correct。理由是判题已本地化、没有排行榜、战绩私有;盲目在服务端并题库只会增加攻击面。但我同时钉死一条——将来做排行榜必须把题库并入后端并在 report 加校验,不能偷这个懒。

对拍自身也踩过坑:replay 不传 replayDate 时两端各自 pickReplay() 随机挑期,表现成 clues 不一致(像真 bug);ts 为本次猜测时间,两端 Date.now() 差毫秒,比较前必须归一。还有个隐蔽点——item 冲刺不是 pickBySeed 而是 UNIQUE_POOL,小 seed 撞不出差异,seed=12345 才暴露,所以对拍要取多个且有大值的 seed。

部署亦有两个典型坑:prisma migrate deploy 不会重新生成 Client,加了 SprintRecord 模型后必须补 npx prisma generate;nginx 逐条列举 location = /api/xxx,新接口漏加就"本机 curl 正常、公网 404",且 reload 后旧 worker 还服务数十秒,差点误判成配置错误。

方法论沉淀

做"换心脏不换脸"的迁移,最值钱的能力不是写得快,而是有一套把"看起来一样"收敛为"逐字一致"的办法:判题本地化让取题公式成为单一事实源,后端迁移靠可复跑的双跑对拍 + 守门断言锁死行为。最终 test-guess 32 项、test-sprint 241 项全绿,前端切 self 模式后库记录自动增长,确认端到端跑通。这类工程素养,比功能本身更能说明问题。