静态站接账号系统 · 内容归 git、状态归库的双源切分
入档:2026-07-28 来源:蛛网之上(tiaozhuxiansheng.com)账号系统上线 + 任务书状态完全动态化,单会话从零到线上(above-the-web 仓库
a179002…64c1044) 状态:已上线并线上验证;后端流转回归 11 项、本地真浏览器 15 项、线上访客 9 项全过
一句话总结
静态站要「动起来」,第一步不是把渲染模式升级成 SSR,而是把内容源和状态源切开——正文继续归 git 走构建期,只把真正会变的那几个字段交给数据库,前端做客户端注水。切对了,部署链路一行都不用改,服务没上线之前站点也照常可读。
背景与事实
- 需求:账号从单个版块推广到全局,但不强制注册登录——读站零门槛,只有认领任务书要登录,因为那头连着报酬和打款,得能把人对上。同时要求任务状态「一步到位完全动态化」(认领、定人、交付、打款在站内点,不再靠改 markdown 重新构建)。
- 起点:主站是 Astro SSG,双部署(GitHub Pages + 香港服务器 nginx),静态托管跑不了账号系统。站内唯一已有的账号系统是 VACAT 协作板块(
/vacat/,独立服务、邀请码封闭),任务书版块当时一点账号都没有,认领方式是「微信直接联系」。 - 交付:
platform/服务(Node 22 +node:sqlite,零第三方依赖,auth 内核与 vacat-platform / pb-arena 同构)、/account/个人中心、/admin/管理台、任务列表与详情页的状态注水与认领/交付面板。 - 验证链:后端
node:test起真 http + 临时 sqlite 跑全流转 11 项 → puppeteer 驱动本机 Chrome 本地端到端 15 项(注册→管理台改状态→认领→定人→交付→列表重排→访客只读) → push → CI 自举安装 → 接 nginx → 线上 curl 权限边界 + puppeteer 访客视角 9 项。
五条可复用 insight
1. 双源切分律(核心)
「让静态站动起来」的正解是切数据源,不是升级渲染模式。 先把字段按「会不会在运行时变」分成两堆:
| 归 git 的 markdown | 归数据库的运行时状态 |
|---|---|
| 正文、要求、对标素材 | 招募中 / 进行中 / 完工待打款 / 已收官 |
| 标题、报酬、截止时间、发布日期 | 谁申请了、定给谁、交付链接、打款时间、流转记录 |
两边靠一份构建期导出的清单对接:Astro 的 src/pages/tasks/index.json.js 把每份任务书的 frontmatter 导出成 /tasks/index.json,随 dist 一起 rsync 到服务器;服务定时读同机文件(不走网络)做 upsert。
关键约束是同步只写正文类字段,运行时状态一律不碰;markdown 里的 status 只在第一次入库时当初值。md 里删掉的任务标记下架、页面不再展示,但认领与打款记录不删。
收益:发任务照旧写 md 然后 push(写作习惯与 git 留痕都保住),认领流转在站内实时生效;而 CI、Pages、rsync 这套部署链路一行没改。转 SSR 要重做 CI、放弃 Pages、让 Node 扛全站流量,代价和收益完全不成比例。
2. 降级要设计成默认路径,不是异常分支
注水脚本的 catch 里什么都不做:拉不到 API,页面就保持构建快照,正文照读、状态显示的是上次构建时的值。
这条的价值在上线当天兑现:代码 push 完、服务在服务器上跑起来了,但 nginx 还没接,/api/ 全是 404——站点在那段时间里完全正常。这不是运气,是”失败=回到静态”这个设计换来的。同理适用于一切给静态站挂后端的场景:先让页面在后端不存在时也成立,再让后端锦上添花。
3. 能搬运的 DOM 才能重排
状态动态化意味着卡片要在「招募中 / 进行中 / 已收官」之间移动。原设计里招募中是卡片(<a class="task-card"> 带标题摘要),已收官是 <ol><li> 细线目录——两种 DOM,跨组移动就得重建节点,注水脚本立刻复杂化。
正解:统一成一份卡片 DOM,视觉差异全部交给 CSS——归档组用 display: contents 把 .tc-top / .tc-meta 的嵌套摊平进父级 grid,再隐藏摘要与 CTA,同一份 DOM 就压成了细线目录体,和原来几乎一样。
视觉差异该由 CSS 承担,不要由 DOM 结构承担。 结构一分叉,运行时重排就寸步难行。这条在任何「同一批数据要按状态分区展示」的页面上都成立。
4. scoped CSS 不覆盖运行时注入的 DOM ⚠️
Astro 的 <style> 是 scoped 的,实现方式是给构建期渲染的元素打 data-astro-cid-* 属性、选择器带上同样的属性。JS 用 innerHTML 注入的元素没有这个属性,scoped 样式对它整段失效。(Vue / Svelte 的 scoped 同理。)
坑的形态:不报错、不告警,只是”样式莫名其妙没生效”,很容易误判成选择器写错或缓存问题。
处置:凡是会被 JS 注入的类名,样式一律写进全局表,并在全局表里就地写明原因,免得后来者按”组件样式该跟组件走”的直觉搬回去。本次把认领面板、时间线、我的认领列表、管理台卡片这四组样式全部下沉到 global.css,组件里只留纯静态部分。
5. 登录墙架在钱前面,不是架在门口
产品判断:全站开放注册、读站不拦任何人,只有认领任务这一个动作要求登录。理由不是”降低门槛”这种泛泛之谈,而是登录墙的位置应该对齐它真正保护的东西——这里要保护的是”钱付给谁”的对应关系,那么墙就架在认领动作前,而不是架在网站入口。
配套的隐私边界:联系方式与收款方式由用户自己填,只有本人和发布方可见,不进任何公开接口;打款仍走微信手动转账,站内不接支付、不存支付凭证,只记「什么时候标记了打款」。
与 内测反馈渠道上线_匿名兜底与422探针验证_v1 的「入口可用性要求必须低于它所服务的故障面」是同一族判断的两个方向:反馈入口要比故障面更低,支付相关入口要比资金流更高。
部署自举:CI 做幂等安装,人只做机器不该做的那一步
首次部署不该要求人去服务器敲一串命令,但也不该把改线上 nginx 塞进 CI。本次的切法:
- CI 幂等 bootstrap:
if [ ! -f /opt/xxx/server/.env ]→ 跑安装脚本(建服务账号、目录、systemd、生成口令);否则 →rsync --delete(排除.env与data/)+systemctl restart。同一条 workflow 首装和增量都能跑,两条路径本次都实测过。 - 安装脚本不许把口令打到 stdout:脚本会被 CI 调用,打出来就永久留在 Actions 日志里。正解=写进
/root/xxx-first-run.txt并chmod 600,stdout 只提示去哪看。 - nginx 那一步留给人:改线上 nginx 属于「改错了整站下线」的操作,且现有配置形态无法预知(见下面的坑 c/d)。CI 只负责把配置片段同步到服务器固定位置,人来 include。
四个只有真部署才会暴露的坑
本地 26 项测试全绿,推上去照样连挂两次。这四个坑本地一个都碰不到:
a. .gitignore 的 .env.* 会吞掉 .env.example。 本地跑得好好的(文件就在工作区),服务器首次安装失败在”没有模板可 cp”。凡是要进库的配置模板,必须显式加否定规则:!platform/server/.env.example。
b. cp -a src/server dst/ 在 dst/server 已存在时会复制成 dst/server/server。 安装脚本第一次跑正常,第二次就套娃——幂等性要按”重跑一次”来验,不能按”跑一次成功”来验。正解是复制目录内容:cp -a src/server/. dst/server/。
c. nginx sites-enabled/ 下的任何文件都会被加载,不看后缀。 把配置备份写在那个目录里,备份副本里的 listen 443 会和正本撞车:duplicate listen options for [::]:443,校验直接失败。备份要放到目录外(/root/)。注意 sites-available/ 里放备份是安全的——那个目录不被加载,所以”以前一直这么备份没事”会给人错误的安全感。
d. sites-enabled/ 里可能是实体文件而不是软链,且与 sites-available/ 同名文件早已不同步。 本次现场:同目录另外三个站点都是软链,唯独主站是 -rw-r--r-- 实体文件;sites-available/ 里那份是旧副本,连一个月前上线的 /vacat/ 都没有。按常规去改 available 会白改一轮,且 nginx -t 还会通过(它校验的是真正生效的那份)。判据一眼可见:ls -l /etc/nginx/sites-enabled/ 看有没有 ->。
两条顺手教训
- 多行 shell 脚本别整段粘进远程终端。 本次把一段带
if/else的安装脚本让人整段粘贴,终端把echo行和后续命令粘连成了一个字符串,输出错乱到看不出真实原因。远程手工操作给单行命令,一条一条来,每条都能独立看到结果——排障时的可观测性比少敲几次回车值钱得多。 - auth 内核值得跨项目同构复用。 本次的
security.js(scrypt + 每账号随机盐、会话令牌只存 sha256 摘要)与 vacat-platform、pb-arena 三份一模一样,注释里互相点名。同构的好处是新项目账号层的架构决策成本≈0,且将来要做单点登录时,从这三份收敛最省事。token 走Authorization: Bearer存 localStorage 而非 cookie,顺带天然免疫 CSRF,也让跨域镜像能用同一套 API。
关联文档
- ⭐ 账号系统对标一线_多账号抽屉与外部通道半开陷阱_v1 —— 直接续篇(同日):本篇搭起账号系统,那篇把它做到一线水准(登录注册分板块 / 本机多账号「一柜多抽屉」/ 退出与登录设备 / 自助重置密码),并沉淀「配置填了≠通道能用,半开比关着更糟」
- 站内新版块五件套模式_三次复用与起刊两坑_v1 —— 直接上篇:任务书版块正是那篇「五件套」的第三次复用,本篇把它的状态从「构建期快照」升成「运行时真值」,并给出 md 与数据库共存的切分方式
- 内测反馈渠道上线_匿名兜底与422探针验证_v1 —— 同族产品判断:登录墙该架在哪(那篇是”反馈入口要比故障面更低”,本篇是”支付入口要比资金流更高”)
- ⚠️ 赛事状态机到点不切换_写入方不等于推进方_v1 —— 状态机同族反例:那篇的病根是”没人在到点推进状态”;本篇四个状态各有明确推进方(发布方定人/承接人交付/发布方标打款),且每次变更强制写一条流转记录
- ⚠️ 双部署目标的base路径陷阱_根路径拼出双斜杠_v1 —— 同一站点的双部署结构;本篇的注水脚本同样要处理”两个域名各自登录态独立”这类只在某个镜像现形的差异
- ⚠️ 云端定时内容生产连环坑复盘_全绿不等于已发_v1 —— 同一验证纪律:本地全绿≠线上已通,本次四个坑全在 push 之后才现形
- 知识库网站免备案上线_香港轻量服务器方案 —— 本次服务落地的就是那台香港轻量服务器,与主站 nginx、VACAT 板块、Maieutic 代理共存
- 任务系统演进_站内新建管理台拆分首页瘦身_v1 —— 本轮续篇(2026-07-29):在「双源切分」基础上长出新东西——任务书也能站内直接写(
source列分流同步)、调价从真相源拆出(fee_override同步不覆盖)、管理台六页、首页搬走 362 篇笔记