b7cc3c4265
- 新增.gitignore,忽略日志、node_modules、dist及编辑器配置文件 - 创建基础React应用及组件App,集成React、Vite和样式 - 设计响应式CSS变量和全局样式,支持暗黑模式 - 新增图标SVG符号定义,用于社交及文档链接展示 - 配置TypeScript项目(tsconfig)和ESLint规则保障代码质量 - 配置Vite开发服务器,支持代理接口与模块别名 - 增加前端API客户端封装与健康检查接口定义 - 增加类型定义文件,映射后端数据模型接口 - 创建index.html作为前端入口页面 - 新增package.json定义依赖和运行脚本,包含React及Ant Design等库
367 lines
12 KiB
Markdown
367 lines
12 KiB
Markdown
# 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 DSN(Data 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 中间件更方便
|