# 个人项目展示网站 Go + SQLite 后端,Vite + React 前端,单二进制部署。 ## 技术栈 | 层 | 技术 | |----|------| | 后端 | Go 1.26+,stdlib `net/http`(1.22 原生路由) | | 数据库 | SQLite(`modernc.org/sqlite`,纯 Go,WAL 模式) | | 认证 | bcrypt + JWT(HMAC-SHA256) | | 前端 | Vite + React 19 + React Router v7 + TanStack Query v5 | | CSS | Tailwind CSS v4(`@tailwindcss/vite`) | | CI | Drone CI(type: exec,宿主机执行) | | 部署 | Caddy 反代 + systemd | 依赖数量:Go 3 个外部包,npm 8 个外部包。 ## 目录结构 ``` ├── main.go # 入口:数据库初始化、HTTP 启动、优雅退出 ├── embed_prod.go # //go:embed frontend/dist(生产构建) ├── embed_dev.go # dev tag:不 embed(使用 Vite dev server) ├── Makefile / Caddyfile / .drone.yml ├── internal/ │ ├── auth/ # bcrypt 校验 + JWT 签发/验证 + AdminOnly 中间件 │ ├── db/ # SQLite 初始化、迁移执行、admin 种子、Project CRUD │ ├── handlers/ # HTTP 处理器(公开接口 + 管理接口 + 图片上传 + 导出) │ ├── models/ # 数据模型与请求/响应类型 │ ├── router/ # 路由组装、CORS 中间件、Slog 日志、SPA fallback │ └── storage/ # 图片本地存储(UUID 文件名) └── frontend/ └── src/ ├── api/ # fetch 封装(JWT 注入、错误处理) ├── hooks/ # useAuth(context + localStorage)、useProjects(TanStack Query) ├── components/ # Layout、Navbar、ProjectCard、ProjectGrid、TagFilter、 │ # LoginForm、ProjectForm、ImageUpload、AdminGuard ├── pages/ # HomePage、LoginPage、AdminDashboard、ProjectEditPage、NotFoundPage └── types/ # TypeScript 类型定义 ``` ## 开发 ### 环境要求 - Go 1.26+ - Node 24+ - 可选:Caddy(生产部署) ### 启动 分别在两个终端中执行: ```bash # 终端 1:Go 后端(:8882) make dev-backend # 等价:ADMIN_PASSWORD=yourpass JWT_SECRET=your-secret go run -tags dev person-home ``` ```bash # 终端 2:Vite dev server(:5173,自动代理 /api → :8882) make dev-frontend # 等价:cd frontend && npm run dev ``` 浏览器访问 `http://localhost:5173`。 ### 环境变量 | 变量 | 必需 | 默认值 | 说明 | |------|------|--------|------| | `ADMIN_PASSWORD` | 首次运行 | — | admin 用户密码(bcrypt 哈希后存库) | | `JWT_SECRET` | 是 | `change-me-in-production` | JWT 签名密钥(HMAC-SHA256) | | `ADDR` | 否 | `:8882` | 服务监听地址 | | `DB_PATH` | 否 | `data/person-home.db` | SQLite 数据库路径 | | `UPLOAD_DIR` | 否 | `uploads` | 图片上传目录 | | `CORS_ORIGIN` | 否 | `http://localhost:5173` | 允许的跨域来源(开发用) | 首次启动时,若 `users` 表为空,自动创建 `admin` 用户并使用 `ADMIN_PASSWORD` 哈希入库。后续重启会跳过种子步骤。 ### 数据库 SQLite 文件保存在 `data/` 目录(已 gitignore),启用 WAL 模式,`busy_timeout=5000ms`。迁移文件在 `internal/db/migrations/`,按文件名字典序执行,通过 `embed` 内嵌到二进制。 ### 前端开发 - Vite 代理配置在 `vite.config.ts`,所有 `/api` 和 `/uploads` 请求转发到 `:8882` - 环境变量以 `VITE_` 前缀暴露给客户端,敏感配置不放前端 - Tailwind v4 通过 `@tailwindcss/vite` 插件集成,无需 PostCSS ## API 参考 ### 公开接口 | Method | Path | 查询参数 | 说明 | |--------|------|----------|------| | `GET` | `/api/projects` | `?tag=go&featured=true&limit=10` | 项目列表(按 sort_order ASC, created_at DESC 排序) | | `GET` | `/api/projects/{id}` | — | 单个项目详情 | | `GET` | `/api/tags` | — | 所有去重标签列表 | ### 认证 | Method | Path | Body | 响应 | |--------|------|------|------| | `POST` | `/api/auth/login` | `{"username":"admin","password":"..."}` | `{"token":"eyJ...","expires_at":"..."}` | Token 有效期 24 小时。 ### 管理接口(需 `Authorization: Bearer `) | Method | Path | Body | 说明 | |--------|------|------|------| | `GET` | `/api/admin/projects` | — | 全部项目(含非精选) | | `POST` | `/api/admin/projects` | Project JSON | 创建项目 | | `PUT` | `/api/admin/projects/{id}` | Partial Project JSON | 更新项目(支持部分字段) | | `DELETE` | `/api/admin/projects/{id}` | — | 删除项目(同时清理图片文件) | | `POST` | `/api/admin/upload` | `multipart/form-data`(字段名 `image`) | 上传图片(最大 10MB) | | `GET` | `/api/admin/export` | — | 导出全部项目为 JSON(`Content-Disposition: attachment`) | ### 错误响应 统一格式:`{"error": "描述信息"}`,HTTP 状态码 4xx/5xx。 ## 构建与部署 ### 生产构建 ```bash make build ``` 等效于: ```bash cd frontend && npm ci && npm run build go build -ldflags="-s -w" -o person-home person-home ``` 产出单二进制 `person-home`(约 11MB,stripped),内嵌前端 `dist/` 和 SQLite 迁移。 ### systemd 服务 ```ini # /etc/systemd/system/person-home.service [Unit] Description=Person Home Portfolio After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/person-home Environment=ADMIN_PASSWORD= Environment=JWT_SECRET= ExecStart=/usr/local/bin/person-home Restart=on-failure [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable --now person-home ``` ### Caddy 配置 ```caddy # /etc/caddy/Caddyfile person-home.example.com { reverse_proxy localhost:8882 } ``` Go 二进制同时提供 API 和 SPA 静态文件,Caddy 仅负责 TLS 终止和反代。 ## CI/CD(Drone) `.drone.yml` 配置了 `type: exec` 流水线,构建产物通过 `nohup` 直接在宿主机启动: ```yaml kind: pipeline type: exec name: build-and-deploy steps: - name: build commands: - cd frontend && npm ci && npm run build - go build -ldflags="-s -w" -o person-home . - name: deploy environment: ADMIN_PASSWORD: from_secret: admin_password JWT_SECRET: from_secret: jwt_secret commands: - mkdir -p /opt/person-home - sudo cp person-home /opt/person-home/person-home - sudo sh -c 'pkill -x person-home || true; nohup env ADMIN_PASSWORD=$ADMIN_PASSWORD JWT_SECRET=$JWT_SECRET /opt/person-home/person-home >> /tmp/person-home.log 2>&1 &' when: branch: main ``` ### 密钥管理 密钥通过 Drone 的 `from_secret` 机制注入,不在仓库文件中明文存储。exec runner 从宿主机环境变量或 secrets 文件读取: ```bash # 在 Drone exec runner 宿主机上设置 export ADMIN_PASSWORD=your-password export JWT_SECRET=your-secret # 或写入 ~/.drone-secrets.yml ``` 流水线中通过 `environment` + `from_secret` 引用,Drone 在运行时将值注入 `$ADMIN_PASSWORD`、`$JWT_SECRET`,再由 `nohup env ...` 传递给二进制。 **前置条件**:Drone exec runner 主机需预装 Go 1.26+、Node 24+。