部分信息已脱敏。持有访问密钥可查看完整版本。
新能源与建设器材租赁企业
租赁企业官网与 CMS 的全面重建
把受模板限制的 Wix 官网重建为自研 CMS 加预渲染前台,从数据库设计到部署配置一人完成。
概要
客户是日本一家新能源与建设现场器材的租赁及销售企业,经营太阳能、蓄电池与保安机材。原官网建在 Wix 上,商品详情页受模板限制,无法按业务需要自由编排。
新版不沿用任何现成 CMS,直接用 Nuxt 与 Astro 从零搭建后台与前台。目标是把商品详情页的编排能力交给运营,同时保住访客侧的静态站性能。从数据库设计到部署配置由我一人完成。
架构设计
两个 Cloudflare Worker 共用同一个 D1 数据库:CMS 侧负责写入,前台侧只读。媒体文件存 R2,浏览器拿 CMS 签发的预签名 URL 直传,不经 Worker 中转,绕开请求体大小与 CPU 时间限制。
官方 AWS SDK 在 Cloudflare 运行时加载阶段就会崩,报错信息完全不指向真实原因。改用纯 fetch 实现的签名库解决,并把这段排查结论写进源码注释,避免下一个人重踩。
前台 19 个页面在构建期预渲染,商品详情的 section 组件用 Vue 做零 JS 服务端渲染。需要交互的筛选功能单独用 Solid 写成 island,两个框架的编译边界靠目录约定隔离。
技术难点
商品详情页的 Section 构建器
商品详情页要复刻一套固定视觉规范:15 类 section、41 个变体。难点不在数量,而在编辑器表单、编辑器预览、线上渲染这三处会各自漂移,任何一个变体加字段,三处都要同步改。
解法是把 section 抽成独立 workspace 包,用一份 zod schema 作为唯一契约,后台校验和前台渲染读同一批类型。同一批 Vue 组件在 CMS 里做实时预览,在前台做零 JS 渲染,从源头上消灭两套实现。
数据形状不同的变体用可辨识联合来表达,而不是把字段全设成可选。这样漏填会在保存时的校验阶段报错,而不是等到访客打开页面才暴露成运行时错误。

预渲染与未发布变更
全量预渲染带来一个必然副作用:编辑者在后台改完内容,网站上什么都不会变,直到下一次构建。单人运营的场景下,没有人会记得去控制台手动触发。
解法是把未发布做成系统里的一等状态。所有影响访客渲染的写入成功后置位一张单行状态表,用 COALESCE 保证记录的是第一次修改的时刻而非最近一次,后台顶部才能显示积压了多久。
发布按钮由服务端代理 Deploy Hook,含密钥的 URL 绝不下发浏览器。没有选每次保存自动构建,因为连改十个字段会触发十次构建,改成人工确认的一次批量发布。

删图前的全库引用扫描
媒体表被四个地方引用,但只有一部分是真外键。JSON 列里存的裸 id 和 section 里存的对象 key 都不受数据库约束保护。删一张图,数据库只能拦住其中一类,漏检就是线上图片 404 且不可逆。
- 外键列直接比对 id
- 数字数组用 json_each 展开后逐项匹配
- 对象数组用 json_extract 取出其中的 media id
- 形状不定的配置列先判断 JSON 合法性再展开
- section 内容列存的是对象 key,改用带转义的子串匹配
- 命中即返回 409 并列出占用位置,告诉运营是哪几条内容在用这张图
子串匹配不精确,是刻意的取舍:误报只是一次可撤销的拦截,漏报则是已删除的文件无法找回。旧实现按固定属性名精确查找,而代码库里从来没有任何地方写过那个属性,扫描静默失效过一次。
没有把这些引用规范化成关联表,虽然那是教科书答案。规范化会给每次内容保存都加上一组同步逻辑,而删除媒体是低频操作,用一次全表扫描换掉长期的同步负担更划算。
上线站点