Files
janus/.claude/plans/phase-1-infrastructure.md
yangzhaohan b7cc3c4265 feat(web): 初始化前端基础架构和样式
- 新增.gitignore,忽略日志、node_modules、dist及编辑器配置文件
- 创建基础React应用及组件App,集成React、Vite和样式
- 设计响应式CSS变量和全局样式,支持暗黑模式
- 新增图标SVG符号定义,用于社交及文档链接展示
- 配置TypeScript项目(tsconfig)和ESLint规则保障代码质量
- 配置Vite开发服务器,支持代理接口与模块别名
- 增加前端API客户端封装与健康检查接口定义
- 增加类型定义文件,映射后端数据模型接口
- 创建index.html作为前端入口页面
- 新增package.json定义依赖和运行脚本,包含React及Ant Design等库
2026-07-21 21:53:34 +08:00

367 lines
12 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.
# Phase 1: 项目骨架 & 基础设施 — 详细计划
> **所属主计划**: [main-plan.md](main-plan.md#phase-1-项目骨架--基础设施第-1-2-天)
> **预计时间**: 第 1-2 天
> **核心学习**: Go 项目组织、package 管理、Gin 基础
---
## 学习目标
| Go 概念 | 说明 | Java 对照 |
|----------|------|-----------|
| `go mod init` / `go mod tidy` | 依赖管理 | Maven `pom.xml` / Gradle `build.gradle`,但更轻量 |
| package 组织 | 按功能分目录,每个目录一个 package | Java package 按目录组织 |
| `func main()` | 程序入口 | `public static void main(String[])` |
| `init()` | 包初始化函数,main 之前自动执行 | Spring `@PostConstruct` / static initializer |
| pointer `*` | 指针类型,传递引用而非拷贝 | Java 中除了基本类型,对象默认是引用 |
| value `&` | 取地址 | Java 无直接对应 |
| `defer` | 函数返回前执行,常用于清理资源 | `try-finally``finally` 块 |
| error 返回值 | 函数返回 error,调用方检查 | Java checked exception,但更显式 |
| `:=` vs `var` | 短声明 vs 显式声明 | `var` 类型推断 |
| struct tag | 结构体字段上的元数据注解 | Java annotation`@JsonProperty` 等) |
---
## 任务分解
### Step 1.0: 初始化 Go Module
**任务**: 在项目根目录执行 `go mod init`,建立 Go 模块。
**关键点**:
- 模块路径已经在 CLAUDE.md 中定义为 `synoth.com/janus`
- 不要用 `github.com/xxx` —— 这是学习项目,不需要托管到 GitHub
- `go.mod` 相当于 Java Maven 的 `pom.xml`,记录模块名、Go 版本、依赖
**验证**: `cat go.mod` 确认模块声明正确
---
### Step 1.1: 安装核心依赖
**任务**: 通过 `go get` 安装以下依赖包:
| 包 | 用途 | Java 类比 |
|----|------|-----------|
| `github.com/gin-gonic/gin` | Web 框架,路由 + 中间件 | Spring MVC |
| `gorm.io/gorm` | ORM 核心库 | JPA / Hibernate |
| `gorm.io/driver/postgres` | GORM PostgreSQL 驱动 | PostgreSQL JDBC Driver |
| `github.com/spf13/viper` | 配置管理(YAML + 环境变量) | Spring `@ConfigurationProperties` |
| `github.com/joho/godotenv` | 加载 `.env` 文件到环境变量 | dotenv-java |
**关键点**:
- `go get` 会自动更新 `go.mod` 和生成 `go.sum`(类似 `pom.xml` + 依赖锁)
- `go.sum` 记录每个依赖的校验和,确保可复现构建
- 间接依赖(indirect)会被自动标记
**验证**: `go.mod` 中出现所有依赖,`go.sum` 非空
---
### Step 1.2: 实现配置加载 — `internal/config/config.go`
**任务**: 用 Viper 加载 `config.yaml` + 环境变量覆盖。
**需要定义的结构体**:
```go
// Config —— 顶层配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
}
// ServerConfig —— HTTP 服务器配置
type ServerConfig struct {
Port int `mapstructure:"port"` // 默认 8080
Mode string `mapstructure:"mode"` // debug / release / test
}
// DatabaseConfig —— PostgreSQL 连接配置
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
}
```
**需要实现的函数**:
- `Load(path string) (*Config, error)` — 加载配置文件
- 支持环境变量覆盖(如 `DATABASE_PASSWORD` 覆盖 YAML 中的明文密码)
**Go 学习点**:
- `mapstructure` tag 是 Viper 专用的 struct tag,类似 Java `@ConfigurationProperties(prefix="...")`
- `*Config` 返回指针而非值 — Go 中大型 struct 传指针避免拷贝
- error 作为返回值,调用方必须处理
**Viper 配置方式**:
```go
v := viper.New()
v.SetConfigFile(path) // 指定配置文件路径
v.AutomaticEnv() // 自动读取环境变量
v.SetEnvKeyReplacer(...) // 将 "." 替换为 "_"database.host → DATABASE_HOST
```
**验证**: 写一个简单的测试(可选),或者直接在 main 中调用 Load 打印结果
---
### Step 1.3: 编写配置文件 — `config.yaml`
**任务**: 在项目根目录创建默认配置文件。
**内容要点**:
```yaml
server:
port: 8080
mode: debug # Gin 模式:debug(带日志)/ release(生产)/ test
database:
host: localhost
port: 5432
user: janus
password: "" # 留空,通过环境变量或 .env 传入
dbname: janus
sslmode: disable # 本地开发关 SSL,生产用 require
```
**Go 学习点**:
- YAML 用缩进表示层级,没有 XML/JSON 的括号
- Viper 默认支持 YAML、JSON、TOML 等格式
- 敏感信息(密码)不写死在 YAML 中,通过环境变量注入
**验证**: 文件语法正确(可以用在线 YAML 验证器或 `yamllint`
---
### Step 1.4: 添加 `.env` 支持
**任务**: 创建 `.env.example` 模板文件,并在 `main.go` 中用 `godotenv.Load()` 加载。
**`.env.example` 内容**:
```
DATABASE_PASSWORD=your_password_here
```
**关键点**:
- `.env` 加入 `.gitignore`,避免敏感信息提交
- `.env.example` 提交到仓库,作为模板
- `godotenv.Load()``main()` 最开始调用,失败不阻断启动(`.env` 可能不存在)
**Go 学习点**:
- `godotenv.Load()` 读取 `.env` 并设置到进程环境变量
- Viper 的 `AutomaticEnv()` 自动读取环境变量,两者配合工作
**验证**: 创建 `.env` 文件后,`os.Getenv("DATABASE_PASSWORD")` 能读到值
---
### Step 1.5: 实现 GORM 数据库连接
**任务**: 在 `internal/config/` 或单独的 `internal/database/` 中实现 DB 连接初始化。
**需要实现的函数**:
```go
func NewDatabase(cfg *DatabaseConfig) (*gorm.DB, error)
```
**实现要点**:
1. 构造 PostgreSQL DSNData Source Name:
```
host=localhost user=janus password=xxx dbname=janus port=5432 sslmode=disable
```
2. `gorm.Open(postgres.Open(dsn), &gorm.Config{})` 打开连接
3. 获取底层 `*sql.DB`,配置连接池参数:
- `SetMaxOpenConns(25)` — 最大打开连接数
- `SetMaxIdleConns(10)` — 最大空闲连接数
- `SetConnMaxLifetime(5 * time.Minute)` — 连接最大存活时间
4. `db.Ping()` 验证连接是否可用
**Go 学习点**:
- GORM 的 `gorm.Open()` 返回 `(*gorm.DB, error)`,惯例返回 error
- `*sql.DB` 是 Go 标准库的数据库句柄,GORM 在其上构建
- 连接池配置对标 Java HikariCP 的 `maximumPoolSize`、`minimumIdle`、`maxLifetime`
- `defer` 不在这里用(连接需要在整个应用生命周期保持),而是在 `main.go` 中管理
**验证**: 启动程序,GORM 打印连接日志无报错
---
### Step 1.6: 实现健康检查 Handler — `internal/handler/health_handler.go`
**任务**: 实现一个简单的健康检查端点。
**代码结构**:
```go
package handler
import "github.com/gin-gonic/gin"
type HealthHandler struct {
db *gorm.DB // 用于检查数据库连接
}
func NewHealthHandler(db *gorm.DB) *HealthHandler { ... }
func (h *HealthHandler) Check(c *gin.Context) {
// 1. ping 数据库
// 2. 返回 JSON: { "status": "ok", "db": "connected" }
}
```
**返回格式**:
```json
{
"status": "ok",
"timestamp": "2026-07-08T12:00:00Z",
"db": "connected"
}
```
**Go 学习点**:
- `(h *HealthHandler)` 是指针接收者(pointer receiver)— 方法可以修改 h 的状态
- `gin.Context` 封装了 HTTP 请求和响应,类似 Spring 的 `HttpServletRequest` + `HttpServletResponse`
- `c.JSON(200, obj)` 自动设置 Content-Type 并序列化 JSON
- Gin 的 handler 签名是 `func(c *gin.Context)`,不使用返回值 — 通过 `c` 写响应
**验证**: `curl http://localhost:8080/health` 返回 JSON
---
### Step 1.7: 实现路由注册 — `internal/index/index.go`
**任务**: 集中管理所有路由注册。
**代码结构**:
```go
package index
func Setup(r *gin.Engine, handler *handler.HealthHandler) {
r.GET("/health", handler.Check)
}
```
**Go 学习点**:
- 函数接收 `*gin.Engine`(Gin 的路由引擎),在其上注册路由
- 这是"依赖注入"的最简形式 —— 手动传参,不用框架的 DI 容器
- 路由分离到单独文件,避免 `main.go` 膨胀
**验证**: 路由文件编译通过,程序运行后 `/health` 可访问
---
### Step 1.8: 实现程序入口 — `cmd/server/main.go`
**任务**: 组装所有依赖,启动 HTTP 服务器。
**流程**:
```
main()
├── godotenv.Load(".env") // 加载环境变量
├── config.Load("config.yaml") // 加载配置
├── database.NewDatabase(&cfg.Database) // 初始化 DB
├── handler.NewHealthHandler(db) // 创建 handler
├── gin.New() / gin.Default() // 创建 Gin 引擎
├── index.Setup(r, healthHandler) // 注册路由
└── r.Run(":8080") // 启动服务器
```
**Gin 模式选择**:
- `gin.Default()` — 带 Logger 和 Recovery 中间件(开发推荐)
- `gin.New()` — 空白引擎,手动添加中间件
**Go 学习点**:
- `main` 函数必须在 `package main` 中,否则无法编译成可执行文件
- `main()` 无参数、无返回值 — 退出用 `os.Exit(code)`
- `defer` 用于在 main 返回前关闭数据库连接等资源
- 包的 `init()` 函数在 main 之前自动执行(GORM 驱动注册就是通过 init)
**验证**: `go run ./cmd/server/main.go` 启动成功,访问 `http://localhost:8080/health`
---
### Step 1.9: 创建 `.gitignore`
**任务**: 确保敏感文件和构建产物不提交。
**必须忽略的内容**:
```
# 环境变量
.env
# 构建产物
/server
/janus
*.exe
# IDE
.idea/
.vscode/
*.swp
# 依赖
vendor/
# 临时文件
tmp/
temp/
```
---
## 文件清单(Phase 1 产出)
```
janus/
├── cmd/server/main.go ← Step 1.8 实现
├── internal/
│ ├── config/
│ │ └── config.go ← Step 1.2 实现
│ ├── handler/
│ │ └── health_handler.go ← Step 1.6 实现
│ └── index/
│ └── index.go ← Step 1.7 实现
├── config.yaml ← Step 1.3 编写
├── .env.example ← Step 1.4 编写
├── .gitignore ← Step 1.9 编写
├── go.mod ← Step 1.0 生成
└── go.sum ← Step 1.1 生成
```
---
## Phase 1 完成标准
- [ ] `go build ./...` 编译通过,无错误
- [ ] `go vet ./...` 静态分析无警告
- [ ] 程序启动后 `curl http://localhost:8080/health` 返回 `{"status":"ok",...}`
- [ ] GORM 成功连接 PostgreSQL(或优雅报错"数据库未启动"而不 panic
- [ ] 配置文件中的值被正确读取(改端口后服务在新端口启动)
- [ ] `.env` 中的密码能覆盖 `config.yaml` 中的空值
---
## 常见问题 & 排错
| 问题 | 可能原因 | 解决 |
|------|----------|------|
| `go get` 失败/慢 | 网络问题 | 设置 `GOPROXY=https://goproxy.cn,direct` |
| `cannot find package` | 模块路径不对 | 检查 `go.mod` 的 module 名和 import 路径一致 |
| GORM 连接报错 | PostgreSQL 未启动 | `pg_isready` 检查,或用 Docker 启动 PostgreSQL |
| `import cycle not allowed` | 循环依赖 | Go 禁止包之间循环引用,检查 import 关系 |
| Viper 读不到配置 | 路径问题 | 用绝对路径或相对于执行目录的路径 |
| `:=` vs `=` 报错 | 短声明只能用于新变量 | 已声明的变量用 `=` 赋值 |
---
## 关键设计决策
1. **手动依赖注入** — 不使用 wire/di 框架。Phase 1 依赖少,手动传参最清晰,也是 Go 社区的常见做法
2. **Config 用指针返回** — `func Load() (*Config, error)` 而非 `(Config, error)`,避免大 struct 拷贝
3. **health check 注入 `*gorm.DB`** — 真实检查数据库连通性,而非返回假 OK
4. **`gin.Default()` 而非 `gin.New()`** — 开发阶段带 Logger 和 Recovery 中间件更方便