)
一、为什么要做很多同学背四级单词是跟着网课或纸质词书走的老师今天讲到 Unit 2 Lesson 3课上一组一组地串讲听的时候感觉全记住了。问题出在课后背单词 App 的词表和课程进度对不上。App 有自己的顺序要么考到还没讲的词要么已经讲过的词迟迟不复习。靠顺序记不是真记住。一组词挨着讲、挨着背换个顺序或者单拎一个出来立刻就懵。所以想做一个很简单的东西课上到哪就在网页上点到哪只复习学过的词按记忆曲线安排熟了再把顺序打乱。目前上线了 68 本词书四六级、考研、专升本、中小学教材本文讲讲它的设计和技术实现以及用一台小服务器上线的踩坑过程。二、产品设计一条「游标」决定复习池整个产品的核心只有一个概念每本书一条进度游标。用户在「地图」上点一个格子一格一节表示「我课上学到这里了」。游标之前的小节 → 已学 → 进入复习池交给记忆算法调度。游标之后的小节 → 未学 →完全不参与任何调度。这比「每天给你推 N 个新词」的模式更贴合跟课学习进度永远由课程决定App 只负责把学过的东西巩固住。打乱程度4 档为了治「靠顺序记」复习顺序分 4 档档位规则适合不打乱课本顺序刚学完跟住老师的串讲逻辑1 级每节内洗牌打破背诵顺序但同一节还在一起2 级单元内跨节打乱语义相近、形近的词被拆开3 级已学范围全部交错考前检验实现上有一个关键约束打乱只改呈现顺序不改调度优先级。到期的词永远先出打乱只决定同优先级内部怎么排。洗牌用带种子的 Fisher–Yates同一轮复习刷新页面不会换序避免「重开一次换一批」TypeScriptexport function mulberry32(seed: number) { let a seed 0 return () { a (a 0x6d2b79f5) 0 let t Math.imul(a ^ (a 15), 1 | a) t (t Math.imul(t ^ (t 7), 61 | t)) ^ t return ((t ^ (t 14)) 0) / 4294967296 } } export function shuffleSeededT(arr: T[], seed: number): T[] { const r mulberry32(seed) const a arr.slice() for (let i a.length - 1; i 0; i--) { const j Math.floor(r() * (i 1)) ;[a[i], a[j]] [a[j], a[i]] } return a }三、记忆曲线FSRS而不是固定的「艾宾浩斯表」很多背词产品宣传「艾宾浩斯记忆曲线」实际上是一张固定间隔表1、2、4、7、15 天……。但研究表明最优间隔取决于每个人、每个词的记忆强度固定表只是近似。这里用的是开源的FSRSAnki 新版默认算法前端用ts-fsrs。它为每张卡维护 stability稳定性和 difficulty难度预测「此刻还能想起来的概率」在概率降到目标保持率时安排复习TypeScriptfsrs(generatorParameters({ request_retention: 0.9, // 目标保持率0.90 → 0.95 复习量大约翻倍 maximum_interval: 365, enable_fuzz: true, // 打散到期日避免某天复习量尖峰 enable_short_term: true, learning_steps: [1m, 10m], relearning_steps: [10m], }))两个按钮 思考时间自动细分FSRS 原本有 4 个评分忘记 / 吃力 / 记得 / 很熟。实际观察发现学生面对 4 个按钮会犹豫「吃力」和「记得」分不清甚至忘了也按「吃力」这是唯一真正会损害算法的误用。FSRS 作者和社区的结论是只用「忘记 / 记得」两档算法也能正常工作代价是少一点难度信息。我们把这点信息从「思考时间」里补回来并且不在界面上展示规则用户只需判断记得还是不记得TypeScript// 思考时间 出卡到点「显示释义」的时间 const autoKey computedRatingKey(() { const t thinkMs.value if (t 60_000) return Good // 多半中途走开了不冤枉成吃力 if (t 12_000) return Hard // 想了很久 // 只有已进入长期复习的词才给「很熟」新词、刚忘过的不给那是短时记忆 if (t 800 t 3000 card.state State.Review) return Easy return Good // 0.8 秒是来不及回想就翻了按「记得」 })中小学词书默认用「四选一」选对 1 秒后自动记「记得」选错立刻标红、亮出正确答案点继续记「忘记」并在本轮稍后重练。四、架构本地优先 一个零依赖的 Node 服务前端uni-appVue 3 Pinia TypeScript目前只发 H5。选 uni-app 是给小程序留后路存储、请求都包了一层platform/。本地优先不登录也能用试用所有学习数据存在本机。页面顶部常驻提示「数据只在本机」。卡片存储做了按课程分片 紧凑编码。H5 的 uni storage 底层是 localStorage整站只有约 5M 字符最初每张卡约 250 字节一本 4592 词的四级书只开回忆题就逼近上限。改成定长数组时间戳存秒、浮点保留 4 位后每张卡约 60–70 字符。textcards:index string[] 有卡片的课程 id cards:courseId { v: 1, m: { word|r: Packed } } 一门课一片 cards:logs [key, rating, at, ms][] 复习日志封顶账号与同步用户名 密码登录。登录后本机存储变成缓存服务器才是准。每次本地写入同步 key → 记入 dirty带修改时间→1.5 秒防抖后推送。启动时先推送没推完的 dirty再拉服务器数据覆盖本机。冲突策略很朴素按修改时间新的覆盖旧的。背单词这种场景几乎不会两台设备同时改同一本书够用。会话 cookie HttpOnly SameSiteLax写操作另外要求自定义请求头防 CSRF。后端零依赖后端只有一个文件不装任何 npm 包node:httpnode:cryptonode:zlib数据库用 Node 22.13 自带的node:sqliteDatabaseSync。部署就是复制一个文件、配一个 systemd service升级 Node 版本也不用担心原生模块重编译。JavaScript// node:sqlite 仍标记为 experimental屏蔽启动告警 const { DatabaseSync } await import(node:sqlite) const db new DatabaseSync(DB_PATH)五、部署一台小服务器 Cloudflare Tunnel公益项目预算就是能省则省域名选了 6–9 位纯数字的 .xyz所谓 1.111B 系列注册和续费都是每年 0.99 美元。服务器一台入门级云主机。DNS HTTPSCloudflare 免费版。关键是Cloudflare Tunnel服务器上的cloudflared主动向 Cloudflare 建立出站连接外部流量经隧道进来。所以服务器不需要开放任何入站端口nginx 只监听127.0.0.1:3000API 只监听127.0.0.1:8787。证书由 Cloudflare 管不用自己续 Lets Encrypt。YAMLtunnel: TUNNEL_ID credentials-file: /etc/cloudflared/TUNNEL_ID.json # 部分网络下 UDP(QUIC) 容易被限速用 http2(TCP) 更稳 protocol: http2 ingress: - hostname: 00345678.xyz service: http://127.0.0.1:3000 - service: http_status:404备份systemd 定时器 坚果云 WebDAV用户的学习记录是最值钱的东西。每天凌晨用 systemd timer比 cron 好查日志、能补跑做两件事VACUUM INTO导出一份一致的 SQLite 快照本地保留 30 天用curl通过 WebDAV 上传到坚果云远端也只保留最近 30 份。ini# lxy-backup.timer [Timer] OnCalendar*-*-* 04:17:00 Persistenttrue一个小坑VACUUM INTO不允许覆盖已有文件同一天重复执行会失败。先写临时文件再mv替换就好。六、词库防护提高门槛而不是假装万无一失词库是整理了很久的数据如果是公开的静态 JSON抓个包就能整套拖走。我们做了几层词库不再是静态文件nginx 对数据目录一律 404只能经 API 获取。设备票据取书要带 HMAC 签名的 HttpOnly cookie外加自定义请求头。按天限量分别按设备、账号、IP 限制每天能打开的书数IP 维度放宽照顾学校、手机网络多人共用出口新票据的签发频率也有上限防止靠清 cookie 绕过。按需下发只下发这本书用到的词条不再整包给全局词条库。响应加扰返回按设备派生密钥做过字节置换的二进制抓包工具里看到的是乱码前端用同一算法还原。需要坦白的是页面最终要把词显示给用户前端防护只能提高门槛挡不住愿意逐页手抄或逆向前端的人。目标是挡住「写个脚本十分钟拖走」这种低成本行为。七、PWA添加到桌面的各种坑手机上用户更习惯点图标所以做了 PWAmanifest.webmanifest、几套图标、一个最小的 service worker。踩过的坑sw.js 必须在站点根目录才能管住整个站点不能放在static/里。service worker 不做离线缓存发版后用户立即拿到新版词库也不落浏览器缓存不削弱上面的防护。各浏览器的安装方式完全不同安卓 Chrome / Edge 有一键安装事件iPhone 只能 Safari「分享 → 添加到主屏幕」微信、QQ 内置浏览器根本不能装只能引导「在浏览器打开」国产安卓浏览器各有各的菜单叫法还可能需要「桌面快捷方式」权限。最后做成按 UA 显示不同引导。从桌面图标打开standalone时隐藏「添加到桌面」的横幅和入口。可以用 Chrome DevTools 协议的Page.getInstallabilityErrors在线上检查可安装性。八、一些小结先想清楚核心模型。「游标 复习池边界」定下来之后地图、每日复习、小节速练、打乱程度都是围绕它的自然延伸。算法用成熟的开源实现。FSRS 有论文、有大规模数据验证比自己拍脑袋定间隔靠谱得多。交互比算法更影响效果。两个按钮 后台用思考时间细分用户负担小算法拿到的信号也不差。小项目也要做备份。每天自动异地备份成本几乎为零。零依赖后端很省心。node:sqlite出来之后小项目的 Node 后端可以一个文件搞定。欢迎试用和拍砖特别是交互和记忆算法方面的建议https://00345678.xyz本站由刘晓艳老师的粉丝制作非官方产品。所有词汇均为个人从互联网收集整理仅供学习交流如有侵权请联系删除。