站点架构导入1668 字·2026/11/14·被检索 0 次

站群架构联系与演进史

站群不是四个独立网站,而是「一个门户 + 三个子站 + 两条隧道 + 七个计划任务」的单机系统。实测四站端口 3000/3001/3102/3103、四个独立 SQLite、3+16+20+8 张表。本文给出 ASCII 拓扑、共享层、六条耦合关系、从 10 月两站到 11-13 C-Lab 的演进时间线,以及端口硬编码、无配置中心等架构债与改进顺序。

挂靠在《四、网站架构总览》之下

版本
使用说明
v1reused-preview2026/11/14 11:16:49

站群架构联系与演进史

一句话:这不是四个独立网站,而是一个「一个门户 + 三个子站 + 一条隧道 + 一套计划任务」的单机站群;所有公网流量从 cag.quest:443 进来,落到 127.0.0.1:3000,再由 portal 按路径分发给 3001、3102、3103。

一、定位与入口

站点公网地址端口basePath计划任务
portal 门户https://cag.quest/3000无KaoyanPortal
kaoyan-vocab 词汇站https://cag.quest/vocab/3001/vocabKaoyanNextServer
notes 笔记站https://cag.quest/notes/3102/notesKaoyanNotes
c-lab 实验室https://cag.quest/c-lab/3103/c-labKaoyanCLab
prob 概率游戏https://cag.quest/prob/无独立进程静态 /prob由 portal 直接读盘
隧道cag.odn.cc 备用3000无KaoyanTunnel(Cloudflare)/ PassNAT frpc

实测监听:0.0.0.0:3000(portal)、::3001(vocab)、::3102(notes)、127.0.0.1:3103(c-lab)。任务描述里提过 3200,实际不存在;C-Lab 也不是 3102,而是 3103。

二、技术栈与目录结构

2026-10-11-06-00-16/
  portal/          原生 Node http + node:sqlite,入口与身份中枢
  kaoyan-vocab/    Next.js 14 + Prisma + SQLite,单词记忆
  notes/           Next.js 14 App Router + Prisma + SQLite,最复杂的一站
  c-lab/           零依赖 Node http + node:sqlite + Python 沙箱
  ops/
    launch.ps1     所有计划任务的统一启动器(按 -Name 分支)
    kaoyannotes.task.xml / KaoyanNotes.backup.xml
    cf/            Cloudflare Tunnel 配置与启停脚本
    bin/           cloudflared.exe 等二进制
  C语言学习网站-方案.md      c-lab 总方案(2026-11-13)
  题目制作台-方案.md         制作台二期方案(2026-11-13)

四个站的技术选择并不统一:两个 Next、两个原生 Node;三个 Prisma/node:sqlite 各管各的库。这种「按站选型」让每站都能独立演进,但也意味着没有共享代码层。

三、数据模型(跨站视角)

站群没有统一数据库,四个 SQLite 文件彼此独立:

库路径实测表数关键内容
portalportal/data/portal.db6users 5、sessions 7、login_log 8、公告、已读、反馈
vocabkaoyan-vocab/prisma/dev.db16Word 528、WordSense 145、WordVersion 41、易混组 52
notesnotes/prisma/notes.db20Note 91、NoteVersion 150、Whitebook 18、SheetDoc 34、PageManual 14
c-labc-lab/data/clab.db8problems 2、test_points 24、runs 1、code_notes 0

跨库没有外键,只有「约定 ID」:

  • notes.Note.ownerId 存 portal 的 users.id,ownerName 冗余登录名;
  • c-lab.problems.note_id 存 notes 的 Note.id;
  • c-lab.code_notes.note_id 存 notes 中代码笔记的 Note.id;
  • notes.PageManual.noteId 是站内自关联,不出站。

这类「软关联」让每站可以单独备份/重建,但代价是删除、改名、迁移时没有数据库级约束,只能靠应用层补偿。

四、请求链路

浏览器
  │  https://cag.quest
  ▼
Cloudflare 边缘
  │  QUIC(cloudflared tunnel run kaoyan)
  ▼
cloudflared.exe  ──────────────►  127.0.0.1:3000  portal
  │  原始 Host 保留                    │
  │                                    ├─ /              index.html
  │                                    ├─ /login /register /me /notices /features
  │                                    ├─ /assets/*      portal.css / portal.js / guide.js
  │                                    ├─ /auth/*        portal 本地认证 API
  │                                    ├─ /api/portal/*  公告与反馈
  │                                    ├─ /prob/*        另一个项目目录的静态文件
  │                                    ├─ /vocab/*  ──► 127.0.0.1:3001  Next basePath=/vocab
  │                                    ├─ /api/*    ──► 127.0.0.1:3001  重写为 /vocab/api/*
  │                                    ├─ /notes/*  ──► 127.0.0.1:3102  Next basePath=/notes
  │                                    └─ /c-lab/*  ──► 127.0.0.1:3103  原生 Node
  ▼
子站校验 x-portal-secret,读 X-User-* 还原 viewer

身份头是整条链路的关键:portal 先删客户端自带的 x-user-id、x-user-name、x-user-username、x-user-role、x-portal-secret,再注入自己的。x-user-name 可能是中文,所以 URL 编码后传输。子站只有拿到正确 x-portal-secret 才认这些头,否则一律匿名。

备用链路是 PassNAT:cag.odn.cc 由两条 frpc 配置同时指向 127.0.0.1:3000(18093f6f7432318b.toml 是 https2http,4e57717ead3e96e0.toml 是 TCP),实测两个 frpc 进程都在跑。这条链路不经过 Cloudflare,用于主隧道故障时的兜底。

五、关键机制:六条耦合关系

耦合一:路径前缀即部署契约。 三个子站的 basePath 分别是 /vocab、/notes、/c-lab,portal 的反代规则与它们一一对应。改一个 basePath,必须同时改 next.config.js、portal server.js、页面里的链接、以及 C-Lab 的 PREFIX。这不是配置,是四处硬编码的契约。

耦合二:共享密钥把门户与子站绑在一起。 portal/data/portal-secret.txt 是 64 字符 hex。portal 每次反代都注入,notes 与 c-lab 每次请求都读文件、比 mtime、校验。密钥轮换后所有子站自动跟上;文件丢失时子站降级为「不校验」,此时直连端口即可伪造身份。vocab 完全不参与这条链,是唯一的例外。

耦合三:C-Lab 依赖笔记站,但只走白名单。 题目笔记、代码笔记、提问笔记的正文权威都在 notes 的 NoteVersion;C-Lab 只通过 lib/notes-client.js 的白名单接口读写。这条耦合是单向的:notes 不知道 c-lab 存在,c-lab 离开 notes 就只能用本地示例题。

耦合四:笔记站复用门户的静态资源。 notes 的 OnboardingGuide 加载 /assets/guide.js,这个文件由 portal 提供。直连 3102 时该请求必然 404,属于预期降级;也意味着笔记站的首次引导体验依赖门户在线。

耦合五:所有进程共用一套 Node 与启动器。 四个站都由 ops/launch.ps1 拉起,Node 固定为 C:\Users\cag\.workbuddy\binaries\node\versions\22.22.2-6\node.exe。升级 Node 要四个站一起验证;launch.ps1 里 -Name 的分支就是站群的「进程表」。

耦合六:公网入口与 Cookie 域绑定。 pf_sid 是 Path=/ 的根域 Cookie,所以 cag.quest 下所有子路径天然共享登录态。如果未来把某个站拆到子域(notes.cag.quest),Cookie 不再自动共享,必须改 Domain 或重新设计跨域认证。

六、发布与运维

站群没有统一发布系统,靠「计划任务 + 启动器 + 手工构建」:

站构建重启关键约束
portal无Stop/Start-ScheduledTask KaoyanPortal改 server.js 即生效
vocabnpm run buildStop/Start KaoyanNextServerbasePath /vocab,前端 /api/* 依赖门户重写
notesnpm run build(带 trace-guard)Stop/Start KaoyanNotesPrisma client 在 generated/db3;启动前自动 generate;BOM 与文件锁
c-lab无Stop/Start KaoyanCLabgcc/python 绝对路径;Job Object 仅 Windows

公共运维事实:

  • 七个计划任务:KaoyanPortal、KaoyanNextServer、KaoyanNotes、KaoyanCLab、KaoyanTunnel 均 Running;KaoyanSelfHeal Ready;KaoyanNatapp Disabled。
  • 任务以 S4U 登录、隐藏窗口运行;Start-ScheduledTask 对已运行任务不生效,必须 Stop 再 Start。
  • KaoyanSelfHeal 跑 kaoyan-vocab/natapp/selfheal.mjs,属于旧 natapp 时代的自愈脚本,现在 natapp 已禁用,脚本仍在。
  • 健康检查只有 portal 的 /healthz,聚合 vocab/notes/prob/clab 四项;没有集中日志、没有指标、没有告警。

七、演进时间线与现状

时间线(复盘口径 + 文件时间戳交叉验证):

  1. 2026-10-11 前后:两站起步。 项目根建立,portal 与 kaoyan-vocab 先落地。vocab 的 schema.prisma 时间戳是 10-13,README 也是 10-13,说明词库与白皮书解析在两周内成型。
  2. 2026-10-20:笔记站立项。 notes 的 .env、start-notes.bat、layout.tsx 时间戳集中在 11-09,但方案与目录结构显示它是 10-20 立项、11 月上旬完成主体。
  3. 2026-10-21:门户统一入口。 kaoyan-vocab/start-next.bat 注释明确写「2026-10-21: port 3001,3000 留给 portal」,这是从「多端口各自暴露」转为「单入口反代」的分界点。
  4. 2026-11-13:C-Lab 与题目制作台。 c-lab/server.js 12:56、handlers/notes.js 17:21、notes/prisma/schema.prisma 20:17,一天内完成从零依赖服务、题库、编译判题到制作台导入/自检的全链路。C语言学习网站-方案.md 与 题目制作台-方案.md 都标注 11-13。

当前架构债:

  • 各站独立库。 四个 SQLite、四套 schema、四套备份策略;用户、权限、收藏、统计无法跨站聚合。portal 有 5 个用户,vocab 却不知道他们是谁。
  • 端口硬编码。 portal server.js 写 3001/3102/3103,launch.ps1 再写一遍,Cloudflare config.yml 写 3000,C-Lab config.js 写 3103。没有环境变量或配置中心。
  • 身份链不完整。 notes 与 c-lab 校验密钥,vocab 完全不校验;同一站群存在两种权限模型。
  • 单机单点。 所有进程、四个库、隧道客户端都在一台 Windows 上;没有容器、没有编排、没有异地备份。
  • 可观测性缺失。 只有 /healthz 和各自 .log;故障定位靠人肉看端口与任务状态。

改进建议(按性价比排序):

  1. 建 ops/config.json:端口、上游、密钥路径、Node 路径集中一处,server.js、launch.ps1、config.yml 共同读取。这是最小改动、最大收益。
  2. 给 vocab 接入 X-User-* 与 x-portal-secret,至少把写接口保护起来;再给 Word 加 ownerId,实现按用户隔离。
  3. 把 /healthz 升级为站群健康页:逐站返回进程、端口、数据库可读性、最近错误;配一个 KaoyanWatch 任务做失败重启。
  4. 统一备份:四个 .db 每日 VACUUM INTO 到带日期的目录,保留 14 天;notes 已有 backups/,其余三站没有。
  5. 中期再把 c-lab 的判题沙箱抽成独立服务,notes 的 Prisma 生成从「换目录」改成构建期固定目录,为将来容器化留出边界。

站群的真正优势不是技术统一,而是「一个入口 + 一套身份 + 一天能加一个站」的演进速度。下一步要做的不是重写,而是把已经存在的隐式契约变成显式配置。