作品集
RENTAL-SITE-REBUILD PUBLIC / 2026

部分信息已脱敏。持有访问密钥可查看完整版本。

新能源与建设器材租赁企业

租赁企业官网与 CMS 的全面重建

把受模板限制的 Wix 官网重建为自研 CMS 加预渲染前台,从数据库设计到部署配置一人完成。

概要

客户是日本一家新能源与建设现场器材的租赁及销售企业,经营太阳能、蓄电池与保安机材。原官网建在 Wix 上,商品详情页受模板限制,无法按业务需要自由编排。

新版不沿用任何现成 CMS,直接用 Nuxt 与 Astro 从零搭建后台与前台。目标是把商品详情页的编排能力交给运营,同时保住访客侧的静态站性能。从数据库设计到部署配置由我一人完成。

代码规模 4 万行+ TypeScript
测试 350+ 单元测试 / 28 项 E2E
开发周期 约 3 个月,单人开发

架构设计

两个 Cloudflare Worker 共用同一个 D1 数据库:CMS 侧负责写入,前台侧只读。媒体文件存 R2,浏览器拿 CMS 签发的预签名 URL 直传,不经 Worker 中转,绕开请求体大小与 CPU 时间限制。

官方 AWS SDK 在 Cloudflare 运行时加载阶段就会崩,报错信息完全不指向真实原因。改用纯 fetch 实现的签名库解决,并把这段排查结论写进源码注释,避免下一个人重踩。

前台 19 个页面在构建期预渲染,商品详情的 section 组件用 Vue 做零 JS 服务端渲染。需要交互的筛选功能单独用 Solid 写成 island,两个框架的编译边界靠目录约定隔离。

双 Worker 共享 D1 的整体架构:CMS 写入、前台预渲染读取、R2 直传与发布链路
双 Worker 共享 D1 的整体架构:CMS 写入、前台预渲染读取、R2 直传与发布链路

技术难点

商品详情页的 Section 构建器

商品详情页要复刻一套固定视觉规范:15 类 section、41 个变体。难点不在数量,而在编辑器表单、编辑器预览、线上渲染这三处会各自漂移,任何一个变体加字段,三处都要同步改。

解法是把 section 抽成独立 workspace 包,用一份 zod schema 作为唯一契约,后台校验和前台渲染读同一批类型。同一批 Vue 组件在 CMS 里做实时预览,在前台做零 JS 渲染,从源头上消灭两套实现。

数据形状不同的变体用可辨识联合来表达,而不是把字段全设成可选。这样漏填会在保存时的校验阶段报错,而不是等到访客打开页面才暴露成运行时错误。

LIVE 可以直接操作
交互演示:切换 section 变体时,编辑器表单字段随之变形
CMS 的 Section 编辑器:左侧表单、右侧实时预览,用的是与前台完全相同的组件
CMS 的 Section 编辑器:左侧表单、右侧实时预览,用的是与前台完全相同的组件

预渲染与未发布变更

全量预渲染带来一个必然副作用:编辑者在后台改完内容,网站上什么都不会变,直到下一次构建。单人运营的场景下,没有人会记得去控制台手动触发。

解法是把未发布做成系统里的一等状态。所有影响访客渲染的写入成功后置位一张单行状态表,用 COALESCE 保证记录的是第一次修改的时刻而非最近一次,后台顶部才能显示积压了多久。

发布按钮由服务端代理 Deploy Hook,含密钥的 URL 绝不下发浏览器。没有选每次保存自动构建,因为连改十个字段会触发十次构建,改成人工确认的一次批量发布。

CMS 顶部常驻的未发布提示条与发布按钮
CMS 顶部常驻的未发布提示条与发布按钮

删图前的全库引用扫描

媒体表被四个地方引用,但只有一部分是真外键。JSON 列里存的裸 id 和 section 里存的对象 key 都不受数据库约束保护。删一张图,数据库只能拦住其中一类,漏检就是线上图片 404 且不可逆。

  • 外键列直接比对 id
  • 数字数组用 json_each 展开后逐项匹配
  • 对象数组用 json_extract 取出其中的 media id
  • 形状不定的配置列先判断 JSON 合法性再展开
  • section 内容列存的是对象 key,改用带转义的子串匹配
  • 命中即返回 409 并列出占用位置,告诉运营是哪几条内容在用这张图

子串匹配不精确,是刻意的取舍:误报只是一次可撤销的拦截,漏报则是已删除的文件无法找回。旧实现按固定属性名精确查找,而代码库里从来没有任何地方写过那个属性,扫描静默失效过一次。

没有把这些引用规范化成关联表,虽然那是教科书答案。规范化会给每次内容保存都加上一组同步逻辑,而删除媒体是低频操作,用一次全表扫描换掉长期的同步负担更划算。

上线站点