Files
lingo/docs/04-architecture.md
ilaandClaude Opus 4.8 89503ee2f0 docs: 新增 go-app 避坑表,记录"未注册路由 404"等坑
- 04-architecture 第四节:新增「go-app 注意事项(避坑表)」,收录
  Handler 仅为已注册路由返回 app shell(否则 404)、前后端同包需 //go:build
  隔离、wasm 由 app.js 引导、web/ 读 app.wasm、Name 不进 title 等坑
- 05-coding-rules 第 5 节:加指引——写 go-app 代码前先扫避坑表,踩到新坑往那补

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:00:56 +08:00

197 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
> 本文讲"怎么把技术栈搭起来":系统结构、前后端职责、数据模型、技术难点、开发顺序。
> 具体用了哪些框架 / 库 / 数据库 / 部署方式,见 [技术栈](03-tech-stack.md)。
## 一、系统结构
```
┐
│ 用户浏览器 (大屏手机 / 平板)
│ │
│ ▼ 安装为 PWA,离线缓存
│ 前端 go-app (app.wasm)
│ - 课程列表 / 播放器 / 字幕同步 / 生词本 UI
│ - Service Worker 缓存音频与文稿
│ │
│ ▼ REST API (JSON over HTTPS)
│ 后端 Go net/http
│ - 账号鉴权 / 进度 / 生词本
│ │
│ ▼
│ SQLite 数据库
┘
```
## 二、前后端职责划分
**前端(go-app / wasm)**
- 课程列表、音频播放器
- 字幕同步高亮 + 点击跳转(核心,自写)
- 生词本界面
- PWA manifest + Service Worker
**后端(net/http)**
- 用户注册 / 登录 / 鉴权(httpOnly Session Cookie)
- 生词本增删查
- 学习进度读写
- (静态音频与文稿可由 go-app Handler 或 CDN 提供)
## 三、数据模型
数据分两类:**内容数据(静态、只读)** 与 **用户数据(动态、需后端)**。
### 3.1 内容数据 — 静态 `episodes.json`(前端直接读,不入库)
实际素材已存在:`family-album-usa/episodes.json`(约 700KB)+ `family-album-usa/audio/*.mp3`。
结构为 **episode(集)→ act(幕)→ line(句)** 三级(以下为真实结构):
```json
{
"meta": { "version": "1.0.0", "title": "走遍美国 Family Album U.S.A.",
"totalEpisodes": 26, "totalActs": 78, "audioActs": 49, "lastUpdated": "2026-05-29" },
"episodes": [
{
"id": 1, "title": "46 Linden Street", "titleCn": "林登大街46号",
"acts": [
{
"act": 1, "label": "Act I",
"audio": "/family-album/audio/u0101.mp3",
"hasAudio": true, "lineCount": 86,
"lines": [
{ "t": 34.18, "en": "Excuse me. My name is Richard Stewart." }
]
}
]
}
]
}
```
要点(**以实际数据为准,勿臆测字段**):
- **集 episode**:`id` / `title` / `titleCn`(26 集**全部**有中文标题)/ `acts[]`
- **幕 act**:`act` 序号 / `label` / `audio`(服务路径 `/family-album/audio/u{集}{幕}.mp3`)/ `hasAudio` / `lineCount` / `lines[]`
- **句 line**:**只有 `{ t, en }` 两个字段**。`t` 为音频时间戳(秒),`en` 为英文原句。**无 `zh` 字段**——行级中文翻译目前不存在,未来要补需扩展 schema
- 规模:26 集 / 78 个 act 条目,其中 49 个 `hasAudio=true`;`audio/` 目录共 50 个 mp3,合计 6335 句
- ⚠️ **路径映射**:JSON 里的 `/family-album/audio/...` 是**对外服务路径**,对应磁盘目录 `family-album-usa/audio/`,后端路由需做这层映射
- ⚠️ `audio/episode12_act2.mp3` 命名不符合 `u####.mp3` 规则且未被 JSON 引用,属游离文件,接入时需核对
> 多教材扩展:未来接入新概念英语等,可沿用「教材 → 单元 → 句子」三级抽象——「走遍美国」即一个教材。新教材未必每句有时间戳,line 的 `t` 允许缺省。第一版无需为此提前建库,前端按 JSON 渲染即可。
### 3.2 用户数据 — SQLite(需账号,后端读写)
仅存「用户态」,**内容数据不入库**:
**users** — id, email, password_hash, created_at
**progress** — user_id, **course_id**, episode_id, act, last_position(秒), completed, updated_at
**vocab** — id, user_id, **course_id**, word, episode_id, act, context_sentence, created_at
> **`course_id` 是前向兼容的预留列**:MVP 只有「走遍美国」一个教材,建表时即带上此列、固定默认 `"family-album"`。
> 目的是避免将来接入新概念英语等时,对**已产生的用户进度/生词数据**做痛苦的 schema 迁移(用户数据不可重生,内容数据可重生)。
> ⚠️ MVP 阶段**仅留列、填默认值,不写任何多教材逻辑**(不建 courses 表、不改内容 JSON、不做按教材过滤)——多教材接入是 V2+,见 [任务看板](06-tasks.md) 待办池。
建表草案(SQLite,T-301 实现时以此为基准,可按需微调约束):
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE progress (
user_id INTEGER NOT NULL,
course_id TEXT NOT NULL DEFAULT 'family-album',
episode_id INTEGER NOT NULL,
act INTEGER NOT NULL,
last_position REAL NOT NULL DEFAULT 0, -- 秒
completed INTEGER NOT NULL DEFAULT 0, -- 0/1
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, course_id, episode_id, act)
);
CREATE TABLE vocab (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id TEXT NOT NULL DEFAULT 'family-album',
word TEXT NOT NULL,
episode_id INTEGER NOT NULL,
act INTEGER NOT NULL,
context_sentence TEXT,
created_at TEXT NOT NULL
);
```
**功能模块化**:字幕同步、生词本、播放器做成可复用组件,不同教材按需启用(走遍美国用字幕同步;新概念可能更重单词表与语法)。
## 四、关键技术难点
| 难点 | 说明 | 应对 |
| --- | --- | --- |
| 字幕同步 | 音频 currentTime 匹配 `t`,逐句高亮 + 点句跳转 | 先出最小原型验证 |
| Audio API 互操 | go-app 需通过 syscall/js 调浏览器 | 封装成可复用组件 |
| wasm 首屏体积 | Go wasm 最小约 2MB,首屏较慢 | 离线缓存后续访问快 |
| 离线音频 | 大体积音频缓存策略 | Service Worker 按需缓存 |
### go-app 注意事项(避坑表)
实际写代码踩到的 go-app 框架行为,**持续累积**;动手前扫一眼,再踩到新坑往这里补:
| 坑 | 现象 | 规避 |
| --- | --- | --- |
| Handler 只为**已注册路由**返回 app shell | 未注册的路径返回 `404 page not found`(不是页面) | 路由用 `app.Route` 注册,且放在 `init()` 里,确保「服务器渲染 / wasm 入口 / 测试」三处都生效(见 `main.go`) |
| 前端/后端同包共存 | server-only 依赖(`net/http`、SQLite、`os`)误进 wasm 会构建失败或体积暴涨 | 后端代码用 `//go:build !wasm` 或单独包隔离,只被 server 入口引用(见第六节) |
| wasm 由 `app.js` 引导 | app shell 里**没有**直接的 `app.wasm` script 标签,而是 `/wasm_exec.js` + `/app.js` | 验证引导是否注入时看这两个脚本,别找 `app.wasm` 标签 |
| 服务器默认从 `web/` 读 `app.wasm` | 没把 wasm 构建到 `web/app.wasm` 时页面空白 | 起 server 前先 `GOOS=js GOARCH=wasm go build -o web/app.wasm` |
| `Handler.Name` 不进 `<title>` | 设了 `Name` 但页面标题为空 | 标题由组件的 `Title()` 等设置,不要指望 `Name` 自动成为 title |
## 五、推荐开发顺序
1. **先写「音频 + 时间戳字幕同步」最小原型**(验证最难一环)
2. 搭课程列表 + 播放页骨架
3. 后端账号体系 + 鉴权
4. 生词本(前端 UI + 后端 API)
5. 进度同步
6. PWA manifest + Service Worker 离线化
7. (V2)复读 / 测验 / 打卡
> 原则:先验证最难的字幕同步,跑通再往上搭架子,避免先搭一堆架构最后卡在核心功能。
## 六、项目结构(建议包布局)
go-app 同一份代码两处运行:编译成 **wasm 在浏览器**跑(`app.RunWhenOnBrowser()`),编译成**普通二进制在服务器**跑(起 `http.Server` + `app.Handler`)。因此前端组件包必须能进 wasm,而后端包(`net/http`、SQLite、文件 IO)**不能进 wasm**——用构建标签 `//go:build !wasm`(或单独的非 js 包,只被 server 入口引用)隔离。
建议布局(T-001 起逐步建立,不必一次建全):
```
lingo/
├── go.mod
├── main.go # 装配入口:注册页面组件 + RunWhenOnBrowser;server 侧起 http + app.Handler
├── pages/ # go-app 前端组件(进 wasm)
│ ├── list.go # 课程列表页(EpisodeList)
│ ├── play.go # 播放页(TranscriptView)
│ ├── player.go # AudioPlayer:syscall/js 封装浏览器 Audio API
│ ├── vocab.go # 生词本页
│ └── auth.go # 登录/注册(AuthForm)
├── content/ # episodes.json 加载与类型定义(T-003,前后端共用,纯数据可进 wasm)
│ └── content.go
├── server/ # 仅服务器端,//go:build !wasm 隔离,不进 wasm
│ ├── api.go # /api/*(账号 / 生词 / 进度),合约见 docs/api.md
│ ├── auth.go # httpOnly Session Cookie
│ ├── db.go # SQLite 建表与读写(见 3.2 建表草案)
│ └── static.go # /family-album/* → 磁盘 family-album-usa/ 的路径映射
└── web/ # 静态资源
├── app.css # 纯 CSS(可迁移自 design_mockups/ 的 :root + 组件样式)
├── manifest.webmanifest
└── sw.js # Service Worker(Phase 6)
```
- 页面路由与组件归属详见 [`routes.md`](routes.md);后端接口形状详见 [`api.md`](api.md)。
- `content/` 只放数据结构与加载,保持可进 wasm(前端列表/播放页要用),**不要**在其中引入 server-only 依赖。