System architecture · evidence-aware

LEDYi 多语言产品官网
系统架构说明

这是一个“单生产环境、单 VPS、内容与公开投影分离”的官网系统:Strapi / Postgres 管事实,Next.js 生成公开页面,Cloudflare 提供边缘与唯一公网回源,Tailscale 承载全部管理面,R2 管媒体,B2 管备份与加密 IaC state。

22 项能力账本全部 Proposed Phase 2:内容建模进行中 363 个跟踪文件 74 条决策 / 6 个 ADR / 45 份证据文档

先说结论

架构方向已经稳定,当前还不是完整上线站点。基础设施和内容地基有真实生产证据;公开应用、发布缓存闭环、多语言、询盘、真实媒体恢复仍未完成。

稳定轴

一个事实,一个所有者

产品语义、路径、发布状态、缓存策略、ReleaseManifest、备份集合各有唯一真源;生成物只做投影。

变化轴

内容与发布持续变化

产品族、locale、slug、schema、镜像 digest、备份点都可变化,但必须经过同一边界或持久状态机。

核心不变量

公开面只能从安全投影派生

草稿不能泄漏;跨对象变化必须同事务/快照;“备份成功”必须等于可恢复的完整集合。

当前真实状态:仓库与已落账证据仍处于 construction / Phase 2 语境。生产内容地基(Postgres + Strapi)曾完成激活,基础设施有局部生产回读;完整 frontend application 尚无激活闭环证据,能力账本没有任何一项提升到 Accepted、Implemented 或 Live Verified。本次也没有把历史证据冒充实时外部回读。

一张图看懂整个系统

主链从左到右;管理面始终走 Tailscale;底部两条链分别是签名发布与 BackupSet 恢复。移动端可横向拖动查看整张图。

LEDYi 官网系统总架构图 用户经 Cloudflare、Tunnel、Caddy 到 Next,Next 只经 PublicContentGateway 读取 Strapi;Strapi 使用 Postgres 与 R2。CI 经签名制品和 ReleaseManifest 发布;Postgres 与媒体引用由 BackupCoordinator 归档到 B2。 PUBLIC READ PATH · 目标完整链(当前未形成公开应用闭环) 访客 / 爬虫HTTPS CloudflareTLS · Edge · WAF局部生产证据 cloudflared → Caddy唯一 Web 回源Compose 已定义 Next.js 15SSR / ISR / SEO功能仅局部 PublicContentGatewaypublished · fields · locale集合页切片已实现 Strapi 5内容事实 / D&Pfoundation 激活证据 PRIVATE DATA & CONTROL · 无公网管理面 编辑者 / CI身份主体 TailscaleCMS · Preview · SSH Strapi AdminDocument Service Postgres 16事务 / 状态属主 R2 媒体hash key · 禁覆盖 三族规格生成器定义 → schema / zod / facet SIGNED DELIVERY · 候选准入与激活分权 GitHub Actionsmain + protected env GHCR + cosigndigest + OIDC signature Forced command只准入 prepared ReleaseManifest Storeappend-only + selector Compose application activation完整状态机尚未接通 RECOVERY · 完整集合而非“上传成功” Postgres dump + self-check生产 DB 路径有证据 Media closure运行入口未接 R2 BackupCoordinator同序作业 + manifest B2 Object Lock90 天 · versionId Drill restore解密/媒体/删除账本未闭环
已有代码或证据支撑局部实现 / 局部生产证据目标架构,尚未闭环

五层边界:谁拥有哪一种事实

系统不是按“前端 / 后端 / 运维”简单切开,而是按事实所有权分层。变化只能经所属边界发生;下游只能保存投影,不能再造第二份真源。

Layer 01 · domain

领域事实

属主:ProductAggregate、PublicContentGateway、AvailableLocales。

不变量:产品族只由 specs dynamic-zone 组件 UID 判别;公开内容只能由已发布且完整的内容派生。

Layer 02 · identity

路径与查询

属主:CanonicalPathRegistry、FilterKind、ProductFilterProjection。

不变量:slug 是 URL 身份而非型号;同字段 OR、跨字段 AND,facet 排除自身选择。

Layer 03 · time

时间与恢复

属主:CachePolicy、BackupCoordinator、ErasureLedger。

不变量:缓存行为从一处派生;BackupSet 必须封闭同一数据库快照所引用的媒体。

Layer 04 · workflow

持久状态机

属主:ReleaseManifest、InquirySubmissionKey、NotificationOutbox。

不变量:跨步骤流程必须持久化、可重试、可收敛;不能把进程内存当业务事实。

Layer 05 · operations

运维适配

属主:构建契约、Alloy、版本与签名清单、OpenTofu 与凭证清单。

不变量:平台差异停留在适配器;共享业务逻辑不能感知具体基础设施实现。

边界判断:一个值若需要在两个地方手工同步,说明属主不唯一;一个长流程若只活在内存,说明恢复语义不存在;一个产品族若要求修改共享筛选器,说明生成器边界失效。

候选目标架构 vs 当前仓库现实

下面的“目标”来自 docs/architecture.md v1.3;“当前”来自机器真源、测试与已落账证据。代码存在、历史生产证据和本次实时回读是三种不同强度的结论。

范围候选目标 / 不变量当前实现与证据判定
公网暴露一个 production OpenTofu state、一个 VPS、一个 Tunnel;ExposurePhase 唯一决定 construction / soft-launch / public。Cloudflare、Lighthouse、Tunnel 与旧站重定向有局部生产回读;仓库证据仍指向 construction 的旧站重定向基线。本次未做外部实时复核。局部证据
CMS / 数据库Strapi 5 管内容事实,Postgres 16 管事务;生产 Strapi 不自动改结构。Postgres + Strapi 的 content-foundation 有激活证据;当前 CMS 有 Product、Category、Series、CanonicalPathRegistry。地基已证
FrontendNext.js 15 App Router 输出完整产品站、博客、SEO/GEO 与公开 API 投影。已有四个 category 路由、动态 series 路由、最小 landing renderer 与 health;主页刻意返回 notFound(),完整应用激活未闭环。局部实现
产品模型 / 生成器一个定义生成 CMS、运行校验、前端判别联合、筛选描述符和 fixtures;新增产品族时共享引擎零修改。三族定义与多类生成物已存在,嵌套规格有本地/真机探针证据;相关能力账本仍是 Proposed。代码已实现
多语言所有投影只问 AvailableLocales,且英文完整发布是其他语言出现的前提。目标代数已定义;Gateway 尚未端到端统一 locale 门禁,hreflang / sitemap / canonical / x-default 闭环未完成。目标为主
缓存与发布单一 CachePolicy;HMAC webhook 入库后依次 revalidateTag 与按 public-html tag purge。策略与部分运行骨架存在;事件表、双层缓存失效和 soft-launch 边缘实测未形成完整证据。未闭环
询盘Inquiry、SubmissionKey、Outbox 同一数据库事务;worker 以 SKIP LOCKED 认领。目标模型已规范化,当前 CMS 尚无 inquiries、submission keys、outbox 实体与全链路实现。尚未实现
媒体R2 不可变对象;SHA-256 进入 key 与 metadata;白名单、magic bytes 和语义化 ProductImage。设计、生成接口与校验方向存在;真实生产媒体源尚未接入 BackupSet 运行入口。未接真实源
备份 / 恢复数据库快照、其媒体闭包、manifest、Object Lock 和删除账本组成可验证 BackupSet。生产 DB dump、age、B2、字节回读、隔离恢复自检与 heartbeat 有证据;媒体闭包默认为 UnconfiguredMediaSource,完整解密/媒体恢复 drill 未闭环。DB 路径已证
部署签名 digest + ReleaseManifest 准入;foundation 与 application 分状态机激活;CMS schema 变化后只 forward-fix。制品签名、准入和 foundation 状态机有实现/证据;frontend application 的 backup → migration → smoke → active 仍缺闭环证据。部分接通
可观测性exporters → Alloy → Grafana Cloud / Loki;六类故障能独立告警。生产 Compose 与 Alloy 配置已定义采集拓扑;完整 Drill 故障注入、告警区分及 production readback 尚未齐全。拓扑存在

状态规则:全部 22 项 capability ledger 条目在本快照仍为 Proposed;“代码已实现”只描述局部代码事实,不提升能力状态。

内容与产品架构

产品不是一张万能字段表,也不是 EAV。三种产品族共享一个生成代数;各族差异只进入定义,生成器把定义投影到 CMS、校验、前端、筛选与测试。

唯一输入config/product-family-definitions.ts
字段、分组、profile、规则
pnpm generate同一概念只经过同一个生成器
CMS 投影spec components
deep population
write validation
Frontend 投影Zod 判别联合
transport schema
规格表 / JSON-LD 描述符
查询与测试FilterKind / facets
synthetic fixtures
一致性门禁
Family 01

neon-flex

13 × 128

示例含 13 个 performance profiles、128 行 measurement。定义含 12 个稳定字段、11 个 profile 字段、10 个 measurement 字段与 6 条规则。

Family 02

smd-strip

17 + 7

17 个稳定字段;waterproof group 有 5 字段 / 7 行;profile 5 字段、measurement 13 字段、7 条规则。

Family 03

cob-strip

16 + 5

16 个稳定字段;waterproof group 有 5 字段 / 5 行;profile 5 字段、measurement 10 字段、6 条规则。

ProductAggregate

一个产品只能属于一个族

Family(product) = UID(product.specs[0]) len(product.specs) = 1 writable(productFamily) = false

型号 modelNumber 保留原始业务事实;URL 身份 slug 独立规范化。profile / measurement 保留相关工作点,不能把多个“up to”极值拍平成互不相关枚举。

Filter algebra

共享筛选器只解释 FilterKind

match(product, query) = AND over selected fields (OR over values of the same field) facetCount(f) = count(distinct products) under query excluding f itself

profile / group 条件是存在量词:同一命中的 profile 行必须同时满足其相关字段。新增族若要求修改 filter.ts,说明变化泄漏进共享逻辑。

Canonical paths

路径身份由 Registry 统一管理

Product /products/<slug> Category /<root>[/<child>] Series /series/<slug>

所有 slug 变更只经 ChangeCanonicalSlug:同事务写 registry、压成单跳;禁止自环、任意环、历史路径复用和死目标。直接改内容实体的 slug 字段属于边界绕过。

Locale algebra

语言可用性只有一个答案

C = PublishedLocales(entry) ∩ CompleteLocales(policy[type], entry) AvailableLocales(entry) = en ∈ C ? C : ∅

切换器、hreflang、sitemap、canonical、x-default 必须消费同一结果;不可用 locale 返回 404 并给英文链接,不能静默跳转。

PublicContentGateway · 当前已有

公开读取的唯一 Strapi client

category / series landing 读取、published 过滤、字段白名单、documentId 稳定分页、去重与 total 复核、有限重试、生成的 Zod 校验,以及 build / runtime transport 分离。

PublicContentGateway · 仍缺

完整公开内容边界尚未形成

完整 locale 门禁、独立 no-store PreviewGateway、产品详情、posts / tags / pages、sitemap / hreflang / canonical / JSON-LD,以及询盘与 webhook 相关边界尚未端到端接通。

构建、供应链与发布状态机

CI 只制造并准入不可变候选,不能顺手把它变成线上事实。激活权在独立管理通道;每一步由持久 manifest 记录,重试必须收敛到唯一合法状态。

GitHub ActionsPR 用 synthetic fixture;生产构建只在 protected environment
BuildKit构建 secret 只经 secret mount,不进 ARG / layer / log
GHCR + cosign推送精确 digest,并验证 OIDC 身份签名
Forced commandSSH 端只接受受限准入协议
ReleaseManifestappend-only record + 一个相对 symlink selector
Management activateTailscale 管理面触发状态机
content-foundation · 有激活证据

内容地基状态机

prepared → containers_started → health_passed → active

用于先建立 Postgres + Strapi 的生产内容底座。它证明的是 foundation 激活,不等于公开网站已经完成。

application · 目标完整链

应用激活状态机

prepared → backup_complete → migration_applied → containers_started → health_passed → public_smoke_passed → active

完整 frontend application 尚无闭环激活证据。任何一步失败只能从 manifest 状态继续,不能凭进程内猜测“上次做到哪里”。

Schema 变化:四步、四次发布

A · expand只增加,保持 N / N+1 双兼容
B · backfillappend-only migration,在备份克隆库先跑通
C · switch切换读取;不与删除同发布
D · contract单独批准后删除旧结构
关键恢复约束:Strapi 启动 schema sync 已被 PoC 证明可能用旧模型收缩新结构。因此 CMS 镜像在任何 schema 变化后永不自动回滚,只能 forward-fix;frontend 也仅在 schemaPhase = none 或本次只有 expand 时允许回到上一 digest。

BackupSet、恢复闭包与删除语义

“备份任务成功”不是上传了一个 dump。一个有效 BackupSet 必须能证明:数据库快照可恢复、该快照引用的媒体版本都在、manifest 未被篡改、保留期有效、被法律删除的数据不会因恢复重新出现。

Postgres snapshotpg_dump 产生 DB@s
Isolated self-check在隔离 Postgres 恢复并检查 marker / 结构
Media closure枚举 DB@s 引用,绑定精确 R2 version
Manifest + age记录 hash、versionId、retention;加密数据库工件
B2 Object Lock上传后做字节回读与保留验证
Heartbeat只有整集完成才发布成功心跳
Closure invariant

同一快照的完整集合

BackupSet(s) = DB@s + closure(mediaRefs(DB@s)) + manifest。任何一个对象缺失或字节不符,整集失败。

Incremental media

按稳定身份复用版本

匹配既有 version、延长 compliance retention、验证前一 manifest 的首次绑定,并按 168 个滚动槽做字节抽检。

ErasureLedger

恢复后重放删除

删除事实独立持久化。恢复旧 BackupSet 后,必须在开放一次性 hostname 之前重放账本,防止已删除主体复活。

已有生产证据

数据库路径可观察、可自检

pg_dump、age 加密、B2 上传、对象字节回读、隔离 restore self-check 与成功 heartbeat 均有历史生产证据。这些证据只覆盖 DB 路径和相应控制面。

当前决定性缺口

真实媒体闭包没有接入运行入口

backup-run-cli.ts 默认仍使用 UnconfiguredMediaSource;因此当前生产 BackupSet 的媒体 closure 为空。尚无完整 decrypt + DB + media + ErasureLedger 的 Drill 闭环。

判定:ARCH-BACKUP-001 与 ARCH-ERASURE-001 仍是 Proposed。DB 备份路径的历史成功不能外推为“整个媒体网站可灾难恢复”。

网络、安全、凭证与观测

安全模型的核心不是多开防火墙规则,而是把公开读面压成一条路径,并让所有管理、构建、CMS、诊断与 SSH 流量走身份网络。

Public plane

VPS 零公网监听

生产 Compose 服务不写公网 ports。唯一 Web 入口是 Cloudflare Tunnel → cloudflared → 内网 Caddy;公开 health 只允许 status 与短 SHA。

Private plane

Tailscale 身份网络

CMS、Preview、SSH、诊断、生产构建读取与激活全部只经 Tailscale。历史证据中人类 ACL 曾存在过宽全局 member grant,最小权限边界仍需闭环。

Storage plane

R2 与 B2 分责

R2 保存公开不可变媒体;B2 保存 age 加密 BackupSet 与加密 OpenTofu state。age 私钥不得进入 VPS 或仓库。

运行拓扑

cloudflared唯一外部 Web 连接
Caddy内网路由、统一缓存响应策略绑定
frontend / cmsNext 公开投影、Strapi 内容边界
postgres内容、状态机、幂等与事务事实

生产 Compose 共 9 个服务:postgres、cms、frontend、caddy、cloudflared、node-exporter、cadvisor、postgres-exporter、alloy。Local Compose 只含 postgres、cms、frontend。

Credential boundaries

凭证按主体与用途隔离

构建读 token、运行读 token、CMS 管理、R2、B2、Tailscale OAuth、Cloudflare/OpenTofu 权限均由生成式凭证清单约束,不能跨边界复用。生产 secret 不进 PR、镜像层、日志或版本库。

Logging contract

单一 Logger 与注册错误码

日志只经统一 Logger.info/warn/error(event, fields) 输出单行 JSON;error 必带 append-only 注册表错误码。外部响应正文、凭证、请求头、询盘正文和完整联系方式不得进入日志。

观测链

node-exporter主机 CPU / memory / disk
cAdvisor容器资源与重启
postgres-exporter数据库连接与健康
Alloy统一采集 metrics / structured logs
Grafana Cloud / Loki远端观测与告警
尚未证明:Next、Strapi、Postgres、cloudflared、磁盘与备份六类故障的独立告警需要 Drill 逐一注入,再以 production 非破坏性 readback 收口;当前仅能确认拓扑和部分运行证据。

路线图:当前在 Phase 2

路线图描述实施顺序,不等于能力状态。Phase 0 / 1 有多项代码和证据,但 ledger 仍全部 Proposed;当前阶段值只按 decision log 最后一条阶段决策推进。

Phase 0
地基与 PoC

仓库规则、digest 锁定、本地 Compose、CI、生成器、synthetic fixture、Strapi schema sync PoC。

已有证据
Phase 1
生产基础设施

唯一 state、旧站重定向、Tailscale、签名发布、Alloy、BackupSet、ErasureLedger、Object Lock。

治理收尾
Phase 2
内容建模

collections、D&P 边界、dynamic-zone 判别器、Registry、三实体表、10 个真实产品、构建契约。

当前阶段
Phase 3
核心功能

Gateway → 详情 → 聚合 → 筛选 → 博客 → 询盘全链路;功能与测试同 PR。

待进入
Phase 4
发布闭环

HMAC webhook、事件表、tag purge、CachePolicy 与双模式收敛实测。

待进入
Phase 5
质量与合规

SEO/GEO、a11y、privacy、全量恢复与破坏性 Drill、容量压测、同事手册。

待进入
Phase 6
多语言上线

初翻草稿、校对、英文门禁、真实域名 noindex soft-launch、public、purge、预热与回读。

待进入

资料真源与阅读地图

本说明先确定规范权威,再与机器真源、状态账本和证据交叉核对。生成文件和描述性文档不能反向覆盖它们的输入真源。

AGENTS.md 项目红线、DoD、当前路线图阶段 └── docs/architecture.md v1.3 候选目标架构;不代表现状 ├── docs/adr/*.md 架构不变量或技术栈决策;规范冲突时优先 ├── docs/decision-log.md append-only 事件与阶段决策;时间顺序真源 ├── docs/status-ledger.md 能力状态真源 │ └── docs/evidence/<TEST-ID>/ 命令、输出、截图、时间戳;证据强度分层 └── machine truth ├── config/product-family-definitions.ts ├── cms/src/api/** + cms/database/migrations/** ├── frontend/src/** ├── scripts/** + ops/runtime/** ├── infra/** + compose / workflow 定义 └── generated outputs 只读投影;修改输入后由 pnpm generate 重建
363

跟踪文件

对仓库目录、规范、代码、运行脚本、IaC、测试与证据做了全量清单盘点;另有 1 个用户未跟踪 settings 文件,未读取改写或纳入交付。

22 / 45

状态 / 证据

22 项 ledger capability 全部保持 Proposed;45 份 evidence Markdown 用于区分本地验证、历史生产回读和仍缺的门禁。

74 / 6

决策 / ADR

decision log 提供事件顺序与 Phase 2 决策,6 个 ADR 约束架构不变量;机器真源用于确认实际落地范围。

本报告能确认什么

确认 main@7069ad9c54b2 快照中的规范目标、实际代码结构、生成器、测试、运行配置、状态账本与落账证据之间的关系,并明确当前实现缺口。

本报告不能确认什么

本次没有登录 Cloudflare、VPS、Tailscale、R2、B2、Grafana 或公开域名做实时外部回读。因此文中生产结论均标为“历史证据”或“局部生产证据”,不是 Live Verified。

状态语言

Proposed 是 ledger 当前状态;代码已实现 只说明仓库切片;本地验证 需要本轮可复跑测试;Implemented 需要 Construction / SoftLaunch / Drill 门禁;Live Verified 需要进入 public 后针对真实域名、运行时、缓存、备份心跳、告警链和业务冒烟做非破坏性回读。