自研 Headless 电商架构以 Next.js 16 App Router、Puck 可视化编辑器与 PostgreSQL 76 张业务表为基础,完全自持前端展示、主题渲染与内容管理系统,同时将交易结算、合规支付与多国税务等高风险链路保留在 Shopify 托管环境。
一、架构演进阶段与事实源切分
系统采取分阶段收敛外部依赖的策略:第一阶段(当前状态,口径 A)实现前台展示面与运营后台自持;第二阶段(规划中,口径 B)推进商品与集合数据事实源迁回本地数据库。
1.1 事实源分布矩阵与系统边界
| 业务领域 | 事实源归属 | 本地存储与运行环境 | 架构权衡(Trade-off) |
|---|---|---|---|
| 页面 / 博客 / 导航 / 主题 / 媒体 | 本地自持 | PostgreSQL(76 表,83 迁移) | 获得灵活的多站点装修与秒级发布能力,承担自建 CMS 维护成本 |
| 前台渲染 / 可视化编辑器 / 发布管控 | 本地自持 | Next.js 16 + Puck(apps/storefront, apps/admin) |
消除 Liquid 嵌套限制;代价是需为 60+ 组件维护 React 服务端组件代码 |
| 商品目录 / 集合 / 变体价格 | Shopify 事实源 | 本地仅存影子表(Product, ProductVariantOverride) |
保证价格与库存单一事实源,规避双写同步冲突;前台需经 BFF 代理读取 |
| 结账 / 支付 / 税费 / 跨境物流 / 订阅 | Shopify 托管 | Shopify Checkout 托管页 | 规避 PCI-DSS 合规认证、多国复杂税务计算与物流打单集成成本;前台需跳转结账 |
1.2 优先自持前台展示层的工程考量
选择优先替换展示面而非数据源,核心原因在于风险与收益的不对称性:
- 故障爆炸半径可控:前台展示层本质上是无状态的渲染管道。若组件出现样式漂移或渲染异常,前台至多出现页面排版缺陷,加购与结账通道仍可通过备用链接完成,不会造成底层交易数据损坏。
- 规避交易资损风险:商品目录与库存属于强一致性状态机。若在未建立完善的双向对账与容灾机制前自建商品库,同步延迟或并发写冲突会导致价格超卖或库存虚占,直接产生经济资损。
- 隔离技术依赖 :前台代码强制面向
packages/commerce内部领域模型开发,UI 层严禁直接引用 Shopify GraphQL SDK 专有类型。即使未来切换底层 Commerce 引擎,仅需更换适配器实现,前台渲染层无需重写。
二、原生托管主题的四项工程瓶颈与技术解法
原生 Shopify Liquid 主题在多站点、高性能与高频发版场景下存在结构性瓶颈,系统通过以下方案重构:
2.1 动静未分离导致边缘 CDN 穿透
- 瓶颈表现:同一商品详情页,游客访问时响应速度正常;用户加购后请求携带 Cart ID 与会话 Cookie,导致 CDN 缓存规则完全穿透,请求全部回源至 Shopify 远端数据中心,跨洋网络延迟显著拉高首屏时间(TTFB)。
- 技术解法 :在
apps/storefront实施动静分离。公共页面主体保持零用户态,享受全量边缘 CDN 强缓存;加购数量、用户登录态与个性化促销标签全部降级为客户端异步微请求,确保 CDN 缓存命中率不受用户身份影响。
2.2 Liquid 嵌套层级限制与发版阻塞
- 瓶颈表现:Liquid 模板对 Section 与 Block 的嵌套深度存在固定限制,复杂网格或错位排版必须通过定制代码手写新 Section。一次样式微调需要经历 Liquid 编写、Schema 声明及整包主题发版,上线周期以天为单位。
- 技术解法 :基于 Puck 深度定制可视化拖拽内核,支持任意层级组件嵌套;内容发布通过数据库中的版本指针原子切换完成,配合
revalidateTag精准失效指定页面缓存,无需触发全量构建与部署。
2.3 多站点模板分叉导致维护成本失控
- 瓶颈表现:每个独立站点对应一份独立的 Liquid Theme。跨站点更新公共组件时依赖人工复制代码,半年内各站点的同名组件因本地改动产生非受控代码分叉,Bug 修复必须在各站点后台逐个重复上线。
- 技术解法 :在 Monorepo 体系下统一管理一套公共组件注册表(
packages/puck)。多站点视觉差异通过 CSS 变量色令牌(Color Tokens)与 JSON 主题预设(Presets)动态注入,实现全平台单一代码源支撑多品牌独立呈现。
2.4 内容数据缺乏主权无法工程化加工
- 瓶颈表现:页面富文本、多语言词条与 SEO 元数据分散存储在 Shopify 后台内部字段中,无法导出至外部自动化流水线,多站点批量替换与翻译质量管控受制于开放接口配额。
- 技术解法:将页面、文章、菜单与多语言字典收归自建 PostgreSQL。内容转化为一等公民实体,支持在本地运行基于 SHA-256 的翻译记忆库匹配、严格术语表拦截及版本快照备份。
三、系统物理拓扑与端口划分
系统划分为 4 个应用与 8 个共享包,拓扑结构如下:
scss
运营管理人员 终端消费者
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ admin (:3002) │ /api 内部代理 │ storefront (:3000) │
│ 可视化拖拽编辑 │─────────────────────►│ Next.js 16 服务端渲染│
└────────┬────────┘ └───────┬─────────────┘
│ │
│ 会话鉴权 + 业务变更 │ 只读查询 + 结账重定向
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ api (:3001) │
│ 鉴权校验 ──► 速率限制 ──► 业务编排 ──► 审计日志 │
└──────────────┬───────────────────────────────┬───────────────┘
▼ ▼
┌────────────────┐ ┌───────────────────┐
│ PostgreSQL │ │ Shopify Storefront│
│ (76 张数据表) │ │ API (商品/购物车) │
└────────────────┘ └───────────────────┘
- storefront(3000):面向终端用户的 SSR/RSC 渲染引擎,公共页面零用户态,严禁直接持有 Shopify Admin 凭据。
- admin(3002):供运营团队进行多站点装修与发布的管理后台,预览与发布态物理隔离,写操作受统一审计。
- api(3001):BFF 编排层与数据访问安全屏障,集中托管所有外部凭据(ADR-0007),提供 235 个标准化路由。
- docs(3003):基于 VitePress 构建的在线文档站,受 Guard ⑦ 自动化文档漂移守卫监控。
四、Shopify 接口读写边界与实际接入矩阵
早期设计草案(ADR-0005)曾提议在 MVP 阶段排除大部分 Admin 接口,但在实际工程落地中,为了支撑统一管理后台(apps/admin)并保持租户凭据隔离,系统通过 apps/api/src/lib/shopify-admin-resource.ts 管道代理接入了 19 个业务域的受控接口。所有 Admin API 严格收敛于服务端调用,前台 apps/storefront 严禁持有 Admin Token。
4.1 真实业务域接入分层矩阵
| 业务领域 | 接入级别 | 运行端点与所需 Scope | 架构实现事实与反面妥协(Trade-off) |
|---|---|---|---|
自定义对象与元字段 Metaobjects / Metafields |
✅ 读写镜像 | POST/PATCH/DELETE /api/admin/metaobjects 等 需 write_metaobjects, write_metafield_definitions |
本地自建镜像表支撑低延迟读取;写入时直连上游 GraphQL 突变,成功后原子更新本地库并触发 revalidatePublicCustomData 缓存失效。 |
营销折扣 Discounts |
✅ 读写接入 | GET/PATCH /api/admin/discounts/[id] POST /api/admin/discounts/bulk 需 read_discounts, write_discounts |
支持 6 种折扣类型参数修改与批量停用/启用;写操作强制记录 createAuditLog 审计日志,杜绝营销资损。 |
库存与调拨 Inventory & Transfers |
✅ 双轨接入 | GET/PATCH /api/admin/inventory POST /api/admin/inventory/sync 需 read_inventory |
变体本地覆盖写入(ProductVariantOverride.quantity)与上游库存定时同步双轨运行;实时计算 hasDrift 漂移状态;调拨单只读。 |
客户档案与客群 Customers & Segments |
✅ 受控只读 | GET /api/admin/customers GET /api/admin/customers/segments 需 read_customers |
运营后台只读查看客户总览、消费频次与客群名单;出参做敏感字段脱敏;创建、修改与密码重置全量保留在 Shopify 托管环境。 |
礼品卡台账 Gift Cards |
✅ 受控只读 | GET /api/admin/gift-cards GET /api/admin/gift-cards/[id] 需 read_gift_cards |
仅支持礼品卡台账与余额查询;出参强制执行 •••• •••• •••• ${lastCharacters} 掩码;严禁申请交易流水权限(read_gift_card_transactions)。 |
营销活动与自动化 Marketing & Automations |
✅ 只读接入 | GET /api/admin/marketing/campaigns GET /api/admin/marketing/automations 需 read_marketing_events |
统一监控营销战役状态与自动化工作流执行结果,不开放本地回写。 |
财务与结算 Finance & Payouts |
✅ 只读+跳转 | GET /api/admin/finance 需 read_shopify_payments |
代理查询账户余额与打款明细(Payouts);对 Balance/Credit 等未开放 API 规范执行 Shopify 后台安全外链(Handoff)。 |
多市场配置 Markets |
✅ 只读接入 | GET /api/admin/markets 需 read_markets |
查询各市场本位币、取整规则与货币转换配置,供前台货币切换器与价格格式化使用。 |
商品与集合目录 Products & Collections |
✅ 本地优先+同步 | GET /api/admin/products POST /api/catalog-sync/run |
后台管理默认直读本地 Product 表(全量 76 表自持体系);通过批量同步流水线拉取上游数据,并在本地变体表实施覆盖。 |
媒体素材 Files |
✅ 只读接入 | Admin API (read_files) |
仅用于后台素材库直接查看与复用 Shopify 原生图床资源。 |
菜单导航 Navigation |
✅ 只读导入 | Admin API (read_navigation) |
平台已自建 Menu 表独立发布;Shopify 菜单仅用于冷启动一次性导入。 |
订单记录 Orders |
✅ 只读镜像 | Webhook + Admin API (read_orders) |
本地建立 ShopifyOrderRecord,仅供买家中心查询履约与物流轨迹,严禁回写。 |
结账与支付结算 Checkout & Payments |
❌ 坚决排除 | 100% 重定向 Shopify 托管页 | PCI-DSS Level 1 合规红线:本地绝对不自建收银台与采集信用卡号,规避数百万合规认证与资损责任。 |
多国复杂税务计算 Taxes & Duties |
❌ 坚决排除 | 完全依托 Shopify Tax | 不在本地自建跨国税则计算引擎,关税与增值税由托管结账页动态计算。 |
承运商面单打印 Label Purchasing |
❌ 坚决排除 | 不接入承运商打单写接口 | 仅只读读取物流追踪号,不接入 FedEx/UPS/DHL 等直接购买面单扣款接口。 |
原生实时分析流 Live View Streaming |
❌ 坚决排除 | 自建埋点 + 引导跳转 | Shopify 原生 3D 地球仪未开放公开 API;业务指标依托自建 packages/metrics,实时大屏返回 getShopifyLiveHandoff 引导跳转。 |
4.2 权限动态探测与 Fail-Fast 机制
为防止因外部店铺权限未就绪导致接口静默故障或崩溃,shopify-admin-resource.ts 建立了运行时权限自省管道:
- 动态探测与 60s 内存缓存 :每次 Admin 调用前检查该租户的 Access Scopes。系统通过 GraphQL
currentAppInstallation { accessScopes { handle } }获取店铺实际授予的权限集合,以 SHA-256 哈希值作为缓存键,维持 60 秒有效期。 - 权限缺失熔断 :当检测到缺少必须权限(如操作折扣缺少
write_discounts)时,立即抛出ShopifyAdminResourceError并返回 HTTP 409(SHOPIFY_SCOPE_MISSING),在响应体中精确指明requiredScopes与missingScopes,阻断盲目调用。 - 复合权限自动降级判定 :系统支持读写权限自动推断(如拥有
write_discounts自动满足read_discounts,拥有read_shopify_payments自动覆盖打款与账户查询)。
4.3 存在性核验的 API 选型陷阱
在导入商品翻译包时,系统需验证商品 Handle 是否存在。实测发现:
- Storefront API 缺陷 :
products(query: "handle:xxx")在 Handle 不存在时不会返回空,而是静默忽略查询条件,返回默认排序的前 20 条商品,状态码为 200,导致校验逻辑假阳性通过。 - Admin API 解决方案 :Admin API GraphQL 的
handle:xxx支持精确匹配,当 Handle 不存在时严格返回空数组。系统支持使用扁平组语法handle:a OR handle:b一次批量验证 50 个 Handle。 - 反面妥协:Admin API 单次调用消耗约 10 点配额,因此商品 Handle 校验做成导入界面的显式选项,默认不启用全量同步。
五、凭据四层物理隔离(ADR-0007)
多站点配置结构中,Shopify 访问凭据(accessToken 与 adminAccessToken)存放在 Site.settings.commerce JSON 字段中。为杜绝 Token 外溢,系统在四个层面实施切断:
ts
// apps/api/src/lib/services/site-credentials.ts
export const SECRET_COMMERCE_KEYS = ["accessToken", "adminAccessToken"];
export function pruneCommerceCredentials<T extends Record<string, unknown>>(commerce: T): T {
const result = { ...commerce };
for (const key of SECRET_COMMERCE_KEYS) {
delete result[key];
}
return result;
}
- 出参剪枝(Response Pruning) :前台获取站点元信息时对敏感 Token 键名强制物理剪枝,保留
storeDomain、provider与apiVersion,并下发非敏感布尔值commerceCredentialsStored,避免因全量剔除导致多站点静默回退至全局单店。 - 内部专用换取通道与 Fail-fast :前台服务端确实需要 Token 时,必须通过内部端点
GET /api/internal/site-commerce-config?site=<key>换取,请求头校验x-internal-token(COMMERCE_INTERNAL_SECRET≥32 字符);未配置时返回 503,严禁降级放行。 - 更新单一入口与只读保护 :
PUT /api/sites/[id]/commerce-credentials为凭据修改唯一入口;通用站点更新接口执行preserveStoredCredentials,避免运营编辑文案时误清空 Token;审计日志仅记录键名存在性布尔值,严禁明文记录 Token。 - 公共只读代理收敛 :前台查询统一走
apps/api/src/app/api/commerce/*下的 10 个公共只读代理端点,加购(Cart)与客户登录态保持前端直连,避免在服务端维护庞大的有状态会话池。
六、前端 UI 防腐契约(packages/commerce)
前端界面组件与底层数据格式完全解耦,依赖领域防腐层运作:
ts
// packages/commerce/src/types/provider.ts
export interface CommerceProvider {
getProduct(handle: string): Promise<Product | null>;
listProducts(options: ProductQueryOptions): Promise<ProductListResult>;
getCollection(handle: string): Promise<Collection | null>;
createCart(): Promise<Cart>;
addToCart(cartId: string, lines: CartLineInput[]): Promise<Cart>;
}
- 统一领域实体 :
Product、Collection、Cart等实体由本地定义,屏蔽 GraphQL 嵌套结构。 - 可替换性 :当前提供
ShopifyCommerceProvider适配器。若后续接入自研数据库或第三方 ERP,仅需实现对应适配器类,前端页面组件(apps/storefront)与可视化搭建区块(packages/puck)代码零修改。 - 转换成本权衡:所有上游字段必须编写显式转换映射函数,代码量相较直接透传第三方 SDK 增加约 3 倍,但彻底杜绝了外部 API 破坏性变更导致的页面联锁故障。
七、技术选型横向对比与六维决策矩阵
基于内部归档的《企业 Java 后端接入可行性评估》(java-backend-integration-evaluation.html)与《Puck 对比报告》,系统对四大架构路线进行了定量权衡:
7.1 六维决策矩阵
| 评估维度 | 核心考量问题 | 自研 Headless | Shopify Hydrogen | Medusa / Saleor | 维护原生 Liquid |
|---|---|---|---|---|---|
| 数据与主权 | 页面、多语言与业务元数据是否完全由自建数据库掌控? | ★★★★★ | ★☆☆☆☆ | ★★★★☆ | ★☆☆☆☆ |
| 初期迁移成本 | 历史多站点组件与模板适配的工作量是否可控? | ★★☆☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 运营心智连续性 | 运营团队拖拽搭建与发布的习惯是否无需彻底重学? | ★★★☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| 多站点扩展性 | 是否支持一套组件代码库集中驱动多域名、多预设独立呈现? | ★★★★★ | ★★★☆☆ | ★★★☆☆ | ★☆☆☆☆ |
| 运维团队适配 | 现有团队技术栈与基础设施资源是否能够支撑常态化运转? | ★★☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★★★☆ |
| 技术退出自由度 | 未来再次更换底层 Commerce 引擎时,前台代码是否无需推倒? | ★★★★★ | ★☆☆☆☆ | ★★★☆☆ | ☆☆☆☆☆ |
7.2 Java 中台接入裁决(模式 A vs 模式 C)
- 模式 A(采纳) :在
packages/commerce实现JavaCommerceProvider,保留现有 Node.js BFF 处理模板与发布指针,两端职责边界清晰。 - 模式 C(否决):使用 Spring Boot 重新实现整套可视化编辑器的 AST 校验与发布指针,重写 Puck 契约与 Next.js 缓存标记成本过大,易滋生回归缺陷。
八、已知技术风险与待办清单
8.1 生产环境已知技术风险
- 跨域重定向导致的追踪上下文断裂 :
- 前台展示层(
shop.example.com)与 Shopify 托管结账页(checkout.shopify.com)跨域跳转会导致第三方 Cookie 丢失首屏来源参数(Client ID / UTM)。 - 规避方案:生成 Checkout URL 时,前台显式提取 URL 参数并通过 Storefront API 写入 Cart Attributes 进行上下文透传。
- 前台展示层(
- Shopify Storefront API 限流雪崩风险 :
- Storefront API 存在 500 点/秒的调用额度。在大促突发加购流量下,频繁创建与修改购物车容易触碰配额。
- 规避方案:购物车操作需在前端建立防抖队列,并结合局部乐观更新减少无效上游往返。
8.2 待办事项
- 待办 1 :在
packages/commerce补充跨后端适配器隔离性的自动化集成测试,验证完全剔除 Shopify 依赖时的纯本地 Mock 回执。 - 待办 2 :在
apps/storefront补充针对结账跳转中断、上游 502/504 异常时的友好错误回退页面与重试逻辑。 - 待办 3 :完成
admin-token-rotation.md凭据应急轮换手册的演练,固化自动化轮换脚本。