NAS 部署时 POST /api/vehicles 返 502 Bad Gateway,根因是 compose 改
SERVER_ADDR: ":8081" 但 fuellog-web/nginx/web.conf 里 upstream 还写死
server api:8080 → nginx upstream 连不上,web 容器把请求反代失败,返 502。
修法:把端口做成 build arg + sed 替换:
- fuellog-web/Dockerfile 加 `ARG API_PORT=8080` + `sed` 在 build 时把
web.conf 里的 __API_PORT__ 占位替换为真实端口
- deploy/docker-compose.yml:
* api.SERVER_ADDR 改成 `:${API_PORT:-8080}`(默认仍是 8080)
* api.expose / healthcheck 端口都改用 ${API_PORT:-8080}
* web build args 传 API_PORT,容器内 web 镜像就有了对的 upstream
* 新增 WEB_PORT 控制宿主机 → web 容器 :80 映射
- README 新增"改端口"小节,说明 API_PORT / WEB_PORT 用法;
新增"排错:502 Bad Gateway"小节,把这次踩的坑(改 SERVER_ADDR 忘
改 web upstream、忘重 build)写明,下次再撞就有现成排查路径
期间的小坑(都修了):
- web.conf 占位用 __API_PORT__(双下划线)而不是 ${API_PORT},避免
nginx 把它当成变量语法尝试解析
- docker-compose 用 ${API_PORT:-8080} 而不是裸 ${API_PORT},这样
用户不传环境变量也能跑(默认 8080)
- README 里 web/api 表格里"golang:1.22-alpine"也顺手改成 1.27-alpine
跟实际 Dockerfile 一致(上次 f81047f 提过但 README 漏了)
未做(留待后续):
- 端口改成可改 web upstream 后,理论上仍然可能出现 build-time / run-time
不一致(build args vs 环境变量);更稳的方案是给 web 加个 entrypoint 脚本,
启动时再 sed 一次 env 变量到 nginx conf —— 留到真有"频繁换端口"的
场景再做
- `.env.example` 没改,只加了 README 文档化的 .env 用法;compose 默认值
已经是 8080,本地 dev 不需要 .env
FuelLog 加油记
轻量、精美、移动端优先的个人加油记录与车辆燃油成本分析工具。
✨ 特性
- 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. 克隆 + 装依赖
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. 启后端
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. 启前端
cd fuellog-web
pnpm dev # 监听 :5173,/api 自动代理到 :8080
打开 http://127.0.0.1:5173 即可。首次进会看到"还没有车辆,先去添加"的引导。
4. (可选)冒烟测试
cd fuellog-api
pwsh -ExecutionPolicy Bypass -File scripts/smoke.ps1
# 13/13 断言:建车 / 加油 / 油价 / 油耗 / 删记录 / 删车 全部命中 plan §6 数值
5. (可选)单测
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 即可对外。
快速部署
# 在仓库根跑(任选一种)
docker compose -f deploy/docker-compose.yml up -d
# 浏览器访问 http://your-host:8080
改端口(默认 8080 冲突时)
如果宿主机 8080 已被占用(NAS、其它服务),用一个环境变量一起改 api 内部监听 + web 容器 upstream + 宿主机映射:
# 例:把整套改成 8081
API_PORT=8081 WEB_PORT=8081 \
docker compose -f deploy/docker-compose.yml up -d --build
API_PORT 控制:
api.SERVER_ADDR(api 容器内监听)api.expose/healthcheck(容器间网络)webbuild 阶段的 nginx upstream(web.conf里server api:__API_PORT__在 build 时 sed 替换)
WEB_PORT 控制宿主机到 web 容器 80 端口的映射。
.env 文件也能放(放在仓库根,自动被 compose 读):
API_PORT=8081
WEB_PORT=8081
改完端口不重 build 是不生效的,因为 web 镜像里的 nginx conf 是 build 时定的。
up -d --build才能拿到新镜像。
部署拓扑
┌─────────────────┐ ┌─────────────────┐
│ 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.27-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 / API_PORT build arg 注入 upstream |
数据持久化
- 容器内
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/
反代建议(不要直接把对外端口暴露公网)
Internet ──TLS──▶ Nginx Proxy Manager ──127.0.0.1:WEB_PORT──▶ fuellog-web (容器)
容器内 API 不直接对外,符合 plan §17 "容器内 API 默认只监听内网"。
WEB_PORT 是宿主机 listen 端口(默认 8080),跟 API_PORT(容器内 api
监听,默认 8080)是两个独立变量,大部分场景保持一致。
健康检查
- api:
wget http://127.0.0.1:${API_PORT}/health,30s 间隔,启动 10s 后开始 - web:
wget http://127.0.0.1/,30s 间隔 - docker-compose 用
depends_on: condition: service_healthy保证 web 等 api 健康后才起
排错:502 Bad Gateway
最常见原因:API_PORT 与 api 容器实际 listen 端口不一致。
fuellog-web/nginx/web.conf 里 upstream 写的是 server api:__API_PORT__,
由 build 阶段用 docker compose 传入的 API_PORT 替换。如果改了
SERVER_ADDR 端口但忘改 build arg,或者改了 build arg 但没重 build,
nginx 就会 upstream 连不上返 502。
排查步骤:
# 1. 看 api 实际 listen 端口
docker exec fuellog-api sh -c 'wget -qO- http://127.0.0.1:8080/health 2>&1 || echo 8080_FAIL'
docker exec fuellog-api sh -c 'wget -qO- http://127.0.0.1:8081/health 2>&1 || echo 8081_FAIL'
# 2. 看 web 容器内 nginx conf 写的是哪个端口
docker exec fuellog-web cat /etc/nginx/conf.d/web.conf | grep 'server api:'
# 3. 不一致就重 build(只 up -d 不会重 build)
API_PORT=8081 docker compose -f deploy/docker-compose.yml up -d --build
开发模式(不想本地装 Go 时用)
# 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。
🗺 路线图
- Phase 0~1:初始化 + 后端骨架 + 数据层 + API + 单测 + 冒烟
- Phase 2~3:前端骨架 + 4 个业务页
- Phase 4:PWA(manifest / SW / 添加到主屏)
- 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 +
:rootCSS 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 — Agent 工程操作手册(规则、约束、Phase 顺序、Definition of Done)
- docs/api.md — 完整 API 文档
- FuelLog_PLAN.md — 原始产品 / 技术决策文档(仓库内)