# M3 双语内容模型到公开展示：为什么先收内容闭环

如果说 `M2` 解决的是“谁能进入系统、谁能做什么”，那么 `M3` 才真正开始回答另一个核心问题：

这个简历项目，到底在管理什么内容，又是怎样把这些内容安全地对外展示出去的？

很多人做简历后台时，会直接从表单、数据库和页面开始，先把“能编辑”做出来。但对于教程型项目来说，这样很容易失焦，因为读者会同时被这些问题拉扯：

- 简历内容应该长什么结构
- 双语字段怎么组织
- 后台草稿和公开展示是不是同一份数据
- 公开站应该读草稿还是读发布快照
- 主题和模板到底算内容，还是算展示配置

`M3` 的价值，就是把这些边界一次收清楚。它不是先追求“功能很多”，而是先把“标准双语简历从内容模型到公开展示”的闭环跑通。

## 一、为什么 `M3` 是整个项目第一次真正进入产品核心

`M1` 建的是工程骨架，`M2` 解决的是登录和角色。到 `M3` 之前，系统已经有了后台壳、角色边界和最小会话闭环，但它还没有一个真正的“产品对象”。

简历项目真正的产品对象，不是登录页，也不是后台壳，而是这份标准简历本身：

- 它要有稳定的数据结构
- 它要支持 `zh/en` 双语
- 它要区分后台正在编辑的草稿和公开可见的版本
- 它还要为后续的 AI 分析、导出、SEO 和模板扩展留下干净边界

所以 `M3` 很重要，不是因为它开始“做页面”，而是因为它第一次把“内容流”作为系统主线立住了。

如果这一步不先收口，后面会很容易出现这些问题：

- 简历结构一边做接口一边改，导致前后端都不稳定
- 后台编辑和公开读取混在一起，草稿可能被外部直接看见
- 主题和模板逻辑直接写进内容模型，后续导出和 AI 会越来越难讲

换句话说，`M3` 不只是做内容，而是在定义后续所有功能要围绕哪条主线继续长。

## 二、`M3` 这轮到底要交付什么

这一轮的目标可以概括成一句话：

让读者能看到一份标准双语简历如何从领域模型出发，经过草稿 / 发布态，最后被公开展示端稳定读取。

这句话里其实包含了四个非常关键的约束。

### 1. 只维护一份标准双语简历

这一版项目不做按 JD 派生的多版本简历，也不做商业版的变体系统。原因不是这些能力不重要，而是它们会显著抬高内容模型的复杂度。

`M3` 要先把一份标准版讲清楚：

- 简历有哪些模块
- 每个模块怎么表达双语内容
- 系统如何围绕这份标准版做编辑、发布和展示

只要这条主线是稳的，后面再去加变体和派生版本就有了基础。

### 2. 先做字段级双语结构

双语不是后面补一个翻译层就能自然长出来的能力。对于简历这种结构化内容来说，最稳的做法是从一开始就把 `zh/en` 当成内容字段的一部分。

这样做的好处是：

- 后台编辑时结构明确
- 公开站切语言时不需要临时拼装
- 导出和 AI 都能围绕同一份结构工作

相比之下，如果一开始只存单语言，后面再补双语，往往会让字段组织、校验和展示层都一起返工。

### 3. 必须区分草稿和已发布内容

这是 `M3` 里最关键的边界之一。

后台编辑态和公开展示态如果不拆开，会直接带来两个问题：

- 管理员还没确认的草稿可能被公开站读取
- 后续导出、AI、SEO 都不知道应该基于哪一份内容工作

所以 `M3` 明确把两者拆开：

- 草稿：后台当前正在编辑的内容
- 已发布：公开站和后续对外链路读取的稳定快照

这个边界看起来简单，但它决定了后面很多功能能不能继续保持单一职责。

### 4. 展示配置要和内容模型分离

简历站是内容展示型产品，所以 `light / dark`、未来模板切换、布局风格这些能力确实很重要。但它们不应该混进内容模型里。

`M3` 的做法是先把这个架构约束讲清楚：

- 简历内容是数据
- 主题和模板是展示策略

这样后面不管做导出、做 AI 还是做多模板，都不会被“内容里夹着大量展示判断”拖垮。

## 三、`M3` 为什么要按这 4 个 issue 来拆

这一轮的拆法比看起来更重要，因为它体现的是一个很典型的教学顺序：

先定义内容，后定义状态，再定义公开读取，最后补充展示边界。

### issue-10：先把标准双语内容模型立起来

第一步不是接口，不是数据库，而是先回答“什么叫一份合法的标准双语简历”。

这一轮主要收住的是：

- 模块边界
- 双语字段结构
- 空白骨架
- 示例数据
- 运行时校验

这样做的价值在于，后面不管接后台编辑还是公开展示，都不会一边写功能一边猜内容结构。

对教程读者来说，这一步也很重要。因为如果内容模型不先稳定，后面看到的所有接口和页面都像是在追逐一个会变化的目标。

### issue-11：再把草稿 / 发布态跑通

内容模型固定以后，下一步不是马上做 `web` 页面，而是先建立内容状态的最小闭环。

这一步要证明三件事：

- 管理员可以更新草稿
- 管理员可以执行发布动作
- 公开接口只返回已发布快照

这里先用内存态服务完成闭环，而不是直接接数据库，也是一个教学上的取舍。因为本轮真正要讲的是“草稿和已发布为什么要分开”，不是持久化技术细节。

### issue-23：提前把主题模式与展示配置边界讲清楚

这一步不是功能实现，而是架构约束补位。

因为简历是展示型产品，如果不在这个阶段先讲清楚主题和模板属于展示层，后面很容易把颜色、布局、模板选择这些判断直接塞进内容模型或业务接口里。

所以 `issue-23` 的作用，不是把主题系统做出来，而是先明确两件事：

- `light / dark` 是 v1 的基础体验能力
- 多模板当前不实现，但必须保留扩展钩子

这一步看似“文档味”更重，但实际上是在替后面的 `web` 展示和 `packages/ui` 抽象铺路。

### issue-12：最后落公开展示端

等内容模型、发布态和展示边界都清楚以后，再去创建 `apps/web`，事情就会顺很多。

因为这时候公开站的职责已经很明确了：

- 只读取已发布内容
- 提供最小 `zh/en` 切换
- 提供最小 `light / dark` 切换钩子
- 不承担任何业务后端职责

这一步其实是在验证一件非常重要的事：

一条完整的内容闭环已经成立了。

后台更新草稿、执行发布，公开站再稳定读取发布内容。这样读者看到的不再只是局部功能，而是一条真实可走通的产品主线。

## 四、这轮最重要的设计取舍

### 1. 为什么不先接数据库

这是 `M3` 最容易被误解的一个点。

看起来内容系统最自然的下一步应该是建表、上 ORM、写持久化，但如果一开始就这么做，读者会同时面对：

- 内容结构设计
- 状态边界设计
- 数据库存储设计
- 接口设计
- 公开读取链路

信息量会一下子变得很重。

而 `M3` 真正要先讲清楚的是：

- 一份标准双语简历长什么样
- 草稿和发布为什么必须拆开
- 公开端为什么只能读发布态

这些边界一旦成立，后面再进入数据库改造时，读者会更容易理解“我们是在把既有行为持久化”，而不是重新设计一套系统。

### 2. 为什么只做字段级 `zh/en`

双语能力本来就会影响建模方式，所以最稳的做法是先把它放进结构里，而不是留到后面再补。

字段级 `zh/en` 的优势在于：

- 不依赖额外翻译映射
- 每一块内容的双语对照关系很直观
- 对表单、接口、导出、展示都足够稳定

对于教程型项目，这种结构也更容易解释，因为读者能直接看见双语字段在哪里，而不是去追一层抽象配置。

### 3. 为什么公开站绝不直接读草稿

这是整个 `M3` 的公开链路原则。

只要公开站直接读草稿，整个系统就会失去一个非常重要的稳定点：对外输出内容到底是哪一份。

一旦这个点不稳，后面这些能力都会跟着模糊：

- PDF / Markdown 导出应该基于哪一份内容
- AI 分析应该分析草稿还是公开版
- SEO 和对外访问看到的是不是管理员还没确认的内容

所以 `M3` 提前把这个规则钉死，是在给后面很多能力减负。

### 4. 为什么主题和模板不能塞回内容层

对于简历站来说，展示配置迟早要长出来，这是必然的。

但如果把主题字段、布局判断甚至模板结构直接揉进内容模型，后面会很快出现两个问题：

- 内容对象越来越像页面配置对象
- 导出、AI、公开 API 都要跟着理解展示细节

`M3` 的选择是先把这个边界说清楚，再只做最小展示钩子。这样既不拖延主题能力，也不会提前污染内容层。

## 五、关键文件可以怎么读

如果你想顺着代码复盘 `M3`，建议优先看这几组文件。

### 内容模型

- `apps/server/src/modules/resume/domain/standard-resume.ts`
- `apps/server/src/modules/resume/domain/standard-resume.spec.ts`

这里解决的是“一份合法标准双语简历长什么样”。

### 草稿 / 发布态

- `apps/server/src/modules/resume/resume-publication.service.ts`
- `apps/server/src/modules/resume/resume.controller.ts`
- `apps/server/test/resume-publication.e2e-spec.ts`

这里解决的是“后台编辑内容如何安全进入公开态”。

### 公开展示端

- `apps/web/app/page.tsx`
- `apps/web/components/published-resume-shell.tsx`
- `apps/web/lib/published-resume-api.ts`

这里解决的是“公开站如何只做展示壳，而不承担业务逻辑”。

### 展示边界文档

- `docs/10-架构设计/01-个人简历-monorepo-重构总方案-v1学习版.md`
- `docs/20-研发流程/01-GitHub-标准开发流程.md`
- `docs/30-开发日志/M3-issue-23-主题模式与展示配置边界.md`

这里解决的是“主题与模板为什么先作为展示层约束存在”。

## 六、`M3` 的测试应该关注什么

`M3` 的测试重点不是“页面长得多漂亮”，而是这条内容链路有没有被锁稳。

### 1. 先锁内容模型

内容模型测试至少要回答这些问题：

- 双语字段结构是不是稳定
- 空白简历骨架是不是完整
- 示例数据是不是能通过校验
- 非法结构是不是会被明确拦住

这一层先稳住，后面的接口和页面才不会踩在流沙上。

### 2. 再锁草稿 / 发布态行为

发布流测试要证明：

- 草稿可以被更新
- 发布动作可以生成公开快照
- 草稿变化不会立刻影响当前已发布内容
- `viewer` 依旧不能越过角色边界做敏感动作

这里真正要锁的是行为边界，而不只是接口通不通。

### 3. 最后锁公开站展示壳

`web` 端这轮的测试重点应该是：

- 公开 API 正常返回时能渲染内容
- 没有发布内容时能显示空状态
- `zh/en` 可以切换
- `light / dark` 最小切换钩子是成立的

也就是说，`web` 在 `M3` 的职责是“验证内容闭环成立”，不是“完成一套复杂设计稿”。

## 七、这一轮最容易踩的坑

### 1. 一开始就把数据库、表单和页面一起做

这会让 `M3` 的重点从“内容流”变成“很多模块一起搭”，读者很难看出真正的主线。

### 2. 让公开站直接读取草稿

这看起来能少做一步发布逻辑，但会让整个系统失去“对外稳定版本”的概念，后面一定要返工。

### 3. 把主题切换写成内容字段

一旦这么做，后面的模板系统、导出和 AI 都会开始承受展示层的复杂度。

### 4. 过早进入多版本简历设计

JD 派生版、多模板、商业版都很有吸引力，但它们不适合在 `M3` 这一轮进入。因为当前读者最需要先看懂的是一份标准版简历如何形成闭环。

## 八、为什么 `M3` 完成后，`M4` 和 `M5` 会顺很多

`M3` 最重要的成果，不只是有了 `apps/web` 页面，而是多出了一份全项目都能围绕它工作的“公开已发布内容”。

一旦这个点成立，后面的链路就开始自然了：

- `M4` 可以基于已发布内容和上传文本做 AI 分析
- `M5` 可以基于已发布内容做 Markdown / PDF 导出
- `M6` 可以把这条内容闭环纳入测试、CI 和部署验证

也就是说，`M3` 不是在单独做一个简历模块，而是在为后续 AI、导出和上线准备统一输入源。

## 九、如果你要自己复现 `M3`，建议按这个顺序

1. 先定义标准双语简历的内容模型
2. 再实现草稿 / 发布态服务
3. 再把“主题与展示配置属于展示层”这个边界讲清楚
4. 最后再创建公开展示端，只读取已发布内容

这个顺序的好处是，读者每一步都知道自己在收什么边界，而不是一头扎进页面和接口细节里。

## 十、这篇文章对应的原始材料

如果你想继续看每个 issue 的实现细节，可以配合下面这些记录一起阅读：

- `docs/30-开发日志/M3-issue-10-双语内容模型.md`
- `docs/30-开发日志/M3-issue-11-草稿与发布态.md`
- `docs/30-开发日志/M3-issue-12-web-公开读取已发布版本.md`
- `docs/30-开发日志/M3-issue-23-主题模式与展示配置边界.md`
- `docs/40-教程与博客/M3-双语内容模型到公开展示闭环-教程大纲.md`

这些日志更适合追“每一步怎么做”，而本文更希望回答“为什么这样拆，为什么这条内容闭环必须先收住”。

## 小结

`M3` 做的事情可以概括成四个词：

- 定义内容
- 分离状态
- 稳定公开读取
- 约束展示边界

这四步看起来比“直接做一个后台编辑器”慢一点，但它们让整个简历项目第一次拥有了可持续演进的内容主线。

后面的 AI、导出、主题、多模板，都是在这条主线之上继续长出来的，而不是重新找一套内容入口。
