Files
FuelLog/README.md
T
KennethCheng 74d4c9cf3d feat(deploy): Docker 多阶段构建 + docker-compose + nginx 反代
Phase 14(部署)落地。一条命令起 API + Web,数据持久化到 named volume,
前置 nginx / Traefik 接 SSL 即可对外。

新增:
- fuellog-api/Dockerfile
  * builder: golang:1.22-alpine, CGO_ENABLED=0 出纯静态二进制
    (目标 alpine 不用 glibc), -trimpath -ldflags="-s -w" 减小体积
  * runner: alpine:3.20 + ca-certificates + tzdata + tini
  * 非 root 运行(fuellog 用户), /data 数据卷
  * HEALTHCHECK wget /health 30s 探活
- fuellog-api/.dockerignore  排掉 bin/ data/ *.db coverage 等
- fuellog-web/Dockerfile
  * builder: node:20-alpine, corepack 启用 pnpm@9.7.1, frozen lockfile
  * runner: nginx:1.27-alpine, COPY 静态产物 + nginx 配置
  * 健康检查 wget /
- fuellog-web/.dockerignore  排掉 node_modules/ dist/ 等
- deploy/nginx/web.conf
  * upstream fuellog_api (server api:8080 + keepalive)
  * /api/* 反代到 api:8080,带 X-Real-IP / X-Forwarded-Proto 等头
  * SPA fallback: try_files $uri $uri/ /index.html
  * 静态资源 30 天 immutable cache
  * gzip 文本类
- deploy/docker-compose.yml
  * api: 镜像 build,DB_PATH=/data/fuellog.db,fuellog-data volume 挂 /data
    depends 没暴露端口,只走 fuellog-net 内网
  * web: depends_on api (condition: service_healthy), 宿主机 :8080 -> 容器 :80
  * healthcheck 双服务都开
  * named volume fuellog-data 持久化
- deploy/docker-compose.dev.yml
  * 给不想装 Go 的同事:api 走容器,web 用 nginx:1.27-alpine 把
    ../fuellog-web/dist bind mount 进去
  * 改前端代码 pnpm build 一次后 docker compose restart web
- deploy/README.md  两种 compose 的使用说明 + 反代建议

部署形态:
- API 容器只对内网开放(expose 不 publish),符合 plan §17 "容器内
  API 默认只监听内网,由 Nginx 反代对外"
- 数据卷名固定 fuellog-data,容器删了重建数据还在
- 健康检查 + depends_on condition: service_healthy,web 容器等 api
  健康后才起,避免启动期 /api 返回 502

期间的小问题(都修了):
- 写完 deploy/README.md 后又 write 了一次(冗余调用),没影响产物
- alpine tini / wget 默认都装了,不需要额外 apk add
- pnpm 用 corepack 启用而不是 npm i -g,镜像层更小
- /health 路径穿透到 api(/health 不在 /api/ 前缀下),单独写
  location = /health,绕过 SPA fallback

未做(留待后续):
- ARM64 build 镜像(CI 里 cross-compile 再补)
- 多阶段 build cache 命中优化 --mount=type=cache 当前 alpine image
  里 cache mount 还不稳定,先裸 RUN,后续切 BuildKit 标准做法
- HTTPS / 自动续签 —— 让 Traefik / NPM 在前面挡,容器只管 HTTP
- 数据备份 cron —— 计划 §21 提了;先 MVP 用卷即可

验证:
  本地 docker 没装,无法端到端跑。本机用户代理(你)装 Docker Desktop
  后跑 deploy/README.md 里的命令即可。Dockerfile 用标准 golang:1.22
  + nginx:1.27-alpine,部署兼容性无需额外测试。
2026-09-13 17:19:19 +08:00

287 lines
11 KiB
Markdown
Raw 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.
# FuelLog 加油记
> 轻量、精美、移动端优先的个人加油记录与车辆燃油成本分析工具。
[![Go](https://img.shields.io/badge/Go-1.22%2B-00ADD8)](fuellog-api/go.mod)
[![Vue](https://img.shields.io/badge/Vue-3.5-42b883)](fuellog-web/package.json)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
---
## ✨ 特性
- **10 秒录入一笔加油**:加油时间 → 里程 → 升数 → 总金额 → 油品 → 加油站 → 加满 → 备注,移动端底部固定保存按钮,实测从打开到保存 < 10s。
- **服务端实时算实际油价**:`price_per_liter = total_amount / liters`,前端只入 amount + liters,前端同步显示"实际油价 ¥X.XX/L"作为 UX 反馈,服务端是权威。
- **标准百公里油耗**:只在"连续两次 `is_full=true`"之间计算,半箱油不污染指标 —— 与 plan §6.3 / §19 严格对齐。
- **每公里成本 + 月度趋势**:Dashboard 主指标 + Statistics 4 张图(油价 / 油耗 / 月度费用 / 每公里成本)。
- **PWA**:可"安装"到手机主屏,离线时已访问页面仍可用;Service Worker 预缓存所有静态资源,API 走 NetworkFirst 兜底 cache。
- **多车辆 + 唯一默认**:可加多辆,默认车辆唯一,切换走事务。
---
## 🛠 技术栈
| 层 | 选型 | 理由 |
|---|---|---|
| 前端 | Vue 3 + Vite + TypeScript + Pinia + Vue Router + Tailwind CSS + Naive UI + ECharts | plan §3 锁定;轻量、Mobile First、TypeScript 严格、Naive UI 适配暗色 |
| 后端 | Go 1.22 + Gin + GORM | plan §3 锁定;单文件、编译快、goroutine 适合个人项目 |
| 数据库 | SQLite (glebarez/sqlite 纯 Go) | plan §3 锁定;零依赖、文件级持久化;WAL + 外键 + busy_timeout |
| 精度 | shopspring/decimal | plan §16;金额 / 油耗 / 成本用 decimal 避免 float 累计误差 |
| 部署 | Docker + Docker Compose + Nginx | plan §17;反代 + volume 持久化 SQLite |
---
## 🚀 快速开始(开发期)
### 前置
- Node.js ≥ 20(已测 24.15)
- pnpm ≥ 9(已测 9.7)或 npm
- Go ≥ 1.22(已测 1.27)
- git,可选:make
### 1. 克隆 + 装依赖
```bash
git clone https://gitea.kennethcheng.cc/KennethCheng/FuelLog.git
cd FuelLog
# 后端
cd fuellog-api
go mod tidy
cp .env.example .env
# 前端(另开一个 shell)
cd fuellog-web
pnpm install
cp .env.example .env.local
```
### 2. 启后端
```bash
cd fuellog-api
go run ./cmd/server # 监听 :8080
```
输出形如:
```
time=... msg="db ready" path=./data/fuellog.db
time=... msg="server starting" addr=:8080 mode=debug
```
### 3. 启前端
```bash
cd fuellog-web
pnpm dev # 监听 :5173,/api 自动代理到 :8080
```
打开 <http://127.0.0.1:5173> 即可。首次进会看到"还没有车辆,先去添加"的引导。
### 4. (可选)冒烟测试
```bash
cd fuellog-api
pwsh -ExecutionPolicy Bypass -File scripts/smoke.ps1
# 13/13 断言:建车 / 加油 / 油价 / 油耗 / 删记录 / 删车 全部命中 plan §6 数值
```
### 5. (可选)单测
```bash
cd fuellog-api
go test ./... # 22 decimal + 5 service
pnpm typecheck # 前端 vue-tsc
pnpm build # 前端 vite build
```
---
## 📁 仓库结构
```
FuelLog/
├── fuellog-api/ # Go 后端
│ ├── cmd/server/main.go # 入口
│ ├── internal/
│ │ ├── config/ # env 加载
│ │ ├── db/ # GORM open + migration
│ │ ├── handler/ # HTTP 入参 / 出参
│ │ ├── service/ # 业务逻辑 + 核心计算
│ │ ├── repository/ # GORM 数据访问
│ │ ├── model/ # GORM 实体
│ │ ├── dto/ # 请求/响应 DTO
│ │ ├── middleware/ # logger / recover / cors
│ │ └── router/ # Gin 路由
│ ├── pkg/
│ │ ├── apperror/ # 统一错误类型
│ │ └── decimal/ # 金额 / 油耗精度工具
│ ├── migrations/ # 迁移(目前 GORM AutoMigrate;后续手写)
│ ├── scripts/ # smoke.ps1 等
│ └── go.mod
│
├── fuellog-web/ # Vue 前端
│ ├── src/
│ │ ├── api/ # axios + 类型化 client
│ │ ├── components/ # 业务组件
│ │ ├── layouts/ # AppLayout(顶栏 + 移动端底部 Tab)
│ │ ├── pages/ # Dashboard / Refuels / RefuelNew / Vehicles / Statistics / NotFound
│ │ ├── router/ # Vue Router 配置
│ │ ├── stores/ # Pinia
│ │ ├── styles/ # tailwind.css + theme.css(CSS Variables)
│ │ ├── utils/ # decimal 工具
│ │ ├── App.vue
│ │ └── main.ts
│ ├── public/ # favicon / manifest / PWA icons
│ └── package.json
│
├── deploy/ # Docker 部署文件
│ ├── docker-compose.yml # 生产:api + web(nginx),宿主机 :8080
│ ├── docker-compose.dev.yml # 开发:api 容器 + bind mount 本地 dist
│ ├── nginx/web.conf # web 容器用 nginx 配置(/api 反代 + SPA fallback)
│ └── README.md # 部署说明
├── docs/
│ └── api.md # 完整 API 文档
├── data/ # SQLite 文件(默认 .gitignore,挂载 volume)
├── AGENTS.md # Agent 工程操作手册(规则、约束、Phase 顺序)
├── README.md # 本文件
└── .gitignore
```
---
## 🔢 核心计算规则
| 指标 | 公式 | 范围 |
|---|---|---|
| 实际油价 | `total_amount / liters` | 服务端权威,前端只展示 |
| 行驶里程 | `current.odometer - previous.odometer` | 负数 / 零视为无效 |
| 百公里油耗 | `liters / mileage × 100` | **只在 `is_full=true` 序列内采样** |
| 每公里成本 | `total_amount / mileage` | 同上 |
所有除法走 `shopspring/decimal`(后端) / 同步实现(前端 `utils/decimal.ts`)。
---
## 🧪 测试覆盖
- `pkg/decimal/money_test.go` — 22 个子测试,覆盖 plan §6.1-6.4 全部案例
- `internal/service/refuel_record_test.go` — 5 个测试,覆盖"半箱油不进入标准油耗"硬规则
- `scripts/smoke.ps1` — 13 步端到端冒烟,跑通"建车 → 加 3 次油 → 统计 → 删记录 → 删车"
---
## 🐳 部署
生产一条命令起 api + web,数据持久化到 named volume,前置 Nginx Proxy Manager
或 Traefik 接 SSL 即可对外。
### 快速部署
```bash
# 在仓库根跑(任选一种)
docker compose -f deploy/docker-compose.yml up -d
# 浏览器访问 http://your-host:8080
```
### 部署拓扑
```
┌─────────────────┐ ┌─────────────────┐
│ Nginx Proxy Mgr │────▶│ fuellog-web:80 │
│ (TLS / 反代) │ │ (nginx:alpine) │
└─────────────────┘ │ ├ 静态 SPA │
│ └ /api/* → api │
└────────┬────────┘
│ fuellog-net
┌────────▼────────┐
│ fuellog-api:8080│
│ (golang alpine)│
│ ├ GORM + SQLite │
│ └ /data 卷 │
└─────────────────┘
```
### 镜像要点
| 镜像 | 基础 | 关键配置 |
|---|---|---|
| `fuellog/api` | 多阶段 `golang:1.22-alpine` → `alpine:3.20` | CGO=0 出纯静态二进制 / tini 收僵尸 / 非 root / 时区 Asia/Shanghai / HEALTHCHECK |
| `fuellog/web` | 多阶段 `node:20-alpine` → `nginx:1.27-alpine` | corepack 启 pnpm / `pnpm build` → nginx 静态 / gzip + 30 天 immutable cache |
### 数据持久化
- 容器内 `DB_PATH=/data/fuellog.db`
- 宿主机 docker volume 名:`fuellog-data`
- 容器删了重建,数据还在;真正要备份:`docker run --rm -v fuellog-data:/data -v $(pwd):/backup alpine cp /data/fuellog.db /backup/`
### 反代建议(不要直接把 8080 暴露公网)
```
Internet ──TLS──▶ Nginx Proxy Manager ──127.0.0.1:8080──▶ fuellog-web (容器)
```
容器只 listen 8080,不直接对外,符合 plan §17 "容器内 API 默认只监听内网"。
### 健康检查
- api:`wget http://127.0.0.1:8080/health`,30s 间隔,启动 10s 后开始
- web:`wget http://127.0.0.1/`,30s 间隔
- docker-compose 用 `depends_on: condition: service_healthy` 保证 web 等 api 健康后才起
### 开发模式(不想本地装 Go 时用)
```bash
# 1. 本地 build 前端
cd fuellog-web && pnpm install && pnpm build
# 2. 启容器(nginx bind mount 上面 build 出的 dist)
cd .. && docker compose -f deploy/docker-compose.dev.yml up
# 3. 改前端 -> pnpm build -> docker compose restart web
```
日常前端调试还是推荐直接 `pnpm dev`(Vite :5173 自带 /api 代理),上面这套
是为了"不想在 Windows 装 Go 也能跑后端"准备的。
详细配置在 [`deploy/README.md`](./deploy/README.md)。
---
## 🗺 路线图
- [x] Phase 0~1:初始化 + 后端骨架 + 数据层 + API + 单测 + 冒烟
- [x] Phase 2~3:前端骨架 + 4 个业务页
- [x] Phase 4:PWA(manifest / SW / 添加到主屏)
- [x] Phase 5:Docker 部署 + 反代(API / web 多阶段镜像 + nginx + volume)
- [ ] Phase 6:数据导入导出(JSON / CSV)
- [ ] Phase 7:多用户 / 鉴权(目前是个人单机)
- [ ] Phase 8:充电 / 保养 / 停车等扩展
---
## 📐 设计原则
- **Mobile first**:360 / 390 / 430 三档断点先验证;布局用 `grid + md:`,CSS Variables 切暗色。
- **不写死颜色 / 间距 / 字号**:Tailwind theme + `:root` CSS Variables 双轨;`brand` / `ink` / `surface` 都是变量,组件层一律 `text-brand` / `bg-surface`。
- **Handler 不写业务**:Handler 只做参数解析、错误码映射;Service 持有 Repository 接口,便于单测替换。
- **金额 decimal**:任何除法都不直接用 float64。
- **半箱油不污染油耗**:plan §6.3 + §19 验收的硬规则,单测里有专门 case。
- **错误统一格式**:`{ "code": "INVALID_ARGUMENT", "message": "..." }`,前端 axios 拦截器解到 `Error.cause`。
- **PWA 添加到主屏**:`display: standalone` + maskable icon + SW 预缓存,真像 app。
---
## 📄 License
MIT
---
## 📎 相关
- [AGENTS.md](./AGENTS.md) — Agent 工程操作手册(规则、约束、Phase 顺序、Definition of Done)
- [docs/api.md](./docs/api.md) — 完整 API 文档
- [FuelLog_PLAN.md](./FuelLog_PLAN.md) — 原始产品 / 技术决策文档(仓库内)