KennethCheng
f81047f9c9
fix(backend): Dockerfile 升 Go base image 到 1.27-alpine 匹配 go.mod
...
NAS 部署时 builder 阶段 `go mod download` 失败:
go: go.mod requires go >= 1.26.0 (running go 1.22.12; GOTOOLCHAIN=local)
根因:今天早上 `go mod tidy` 用本机 Go 1.27.0 跑,`go.mod` 的 go 指令
被写到 1.26.0(`go mod tidy` 行为:用当前 toolchain 的 minor),
但 `fuellog-api/Dockerfile` 写死 `FROM golang:1.22-alpine`,base 自带
Go 1.22.12,跟 go.mod 不一致 → Go 工具链自检不通过,go mod download
直接 exit 1。
GOTOOLCHAIN=local 也是这个错的成因:容器内默认不下载更新 toolchain,
如果 go.mod 要求的版本高于 base,会拒绝构建。要么升 base,要么放开
GOTOOLCHAIN=auto 走网络下载新工具链。升 base 是更确定的做法(无网也跑)。
修法:把 builder 阶段的 `FROM golang:1.22-alpine` 改成
`FROM golang:1.27-alpine`(与本机一致,与 go.mod 要求 >=1.26 都满足)。
runner 阶段 (alpine:3.20) 不需要 Go,不动。
未做(留待后续):
- 用 ARG 让 base Go 版本参数化,后续 `go mod tidy` 升 go.mod 时同步
Dockerfile base。短期先写死 1.27
- 同样的"go.mod vs Dockerfile base"漂移问题可能在 web 那边再出现
(目前 web 用 node:20-alpine,本机 node 24,node 不会因 minor 不一致
拒绝 npm install,暂时没风险)
2026-09-13 18:34:51 +08:00
KennethCheng
a2fc215ef9
docs: README 补全 Docker 部署章节(拓扑图 / 镜像要点 / 持久化 / 反代)
...
上条 commit (846b1a7 ) 把 deploy/ 目录实际文件都铺好了,但 README 的
"🐳 部署"还停留在"待完成"占位,新成员(以及未来的自己)看到 README
会以为 Docker 还没做。这条 commit 把 README 与实际状态对齐。
新增 / 改动 README.md:
- 仓库结构树:deploy/ 从"(待) docker-compose + nginx"改成
"Docker 部署文件",列出 4 个子文件
- 替换 "🐳 部署(待完成)" 占位为完整章节:
* 快速部署:一条 docker compose up -d
* 部署拓扑:ASCII 图展示 web 容器 (nginx) 反代 /api 到 api 容器
(GORM + SQLite),数据卷持久化
* 镜像要点:表格列出 api / web 两个镜像的 base image + 关键配置
(CGO=0 / tini / 非 root / corepack pnpm / gzip + 30 天 immutable cache)
* 数据持久化:DB_PATH 路径 / volume 名 / 备份命令(docker run --rm 模板)
* 反代建议:不要直接 8080 暴露公网,前面挡 NPM / Traefik,符合
plan §17 "容器内 API 默认只监听内网"
* 健康检查:API + web 都用 wget,compose 用 condition: service_healthy
* 开发模式:不想装 Go 也能跑,流程:pnpm build -> docker compose up
* 链回 deploy/README.md
- 路线图:Phase 5 标 done,Phase 6/7/8 标 pending(对应 Import/Export /
Auth / 扩展)
未做(留待后续):
- README 截图(Docker 跑起来后再补)
- Traefik / NPM 自动 HTTPS 的具体配置(留给 plan §17 后续)
- 数据备份 cron / 灾备演练
风格保持与之前一致:中文为主,技术名词留英文,emoji 章节标题。
2026-09-13 17:20:50 +08:00
KennethCheng
846b1a7b06
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:11:32 +08:00
KennethCheng
f24e6639e3
docs: 仓库根 README + docs/api.md 完整 API 文档
...
补上交付物必备的文档:
- README.md:项目介绍 / 特性 / 技术栈 / 快速开始 / 仓库结构 /
核心计算公式 / 测试覆盖 / 设计原则 / 路线图 / License
- docs/api.md:6 个 endpoint 完整契约(请求参数 / 响应 / 错误码 /
业务规则),含 Statistics 字段含义与采样逻辑说明
为什么现在补:
- 之前的 12 个 commit 都是代码,GitHub / Gitea 仓库首页空白
- 新成员(或者未来的自己)来看仓库,无法在 5 分钟内知道这是个啥 / 怎么跑
- API 文档从代码里的 dto.go / handler.go 反推,容易漏边界条件;集中写一遍
把所有约束沉淀下来
README 设计:
- 顶上一段简介 + badge
- ✨ 特性(7 条,每条对应 plan §3 / §6 / §12)
- 🛠 技术栈:表格化展示 plan 锁定选型
- 🚀 快速开始:5 步(克隆 / 装依赖 / 启后端 / 启前端 / 冒烟)
- 📁 仓库结构:树状展示两个子工程的完整布局
- 🔢 核心计算规则:4 个公式表(从 plan §6 提炼)
- 🧪 测试覆盖:列出 22+5+13 的具体来源文件
- 🐳 部署 / 🗺 路线图:占位 + 标 done
- 📐 设计原则:把 plan §12 + AGENTS.md 关键约束翻译成"为什么这么做"
- 📎 相关:链回 AGENTS.md / docs/api.md / FuelLog_PLAN.md
docs/api.md 设计:
- 通用约定:UTC / decimal / 列表空数组 / 错误码字典
- 6 个 endpoint 一段一个,含:curl-style 示例、参数表、Response 200 示例、
业务规则(例如 RefuelCreate.price_per_liter 不入参 / RefuelUpdate
patch 语义 / Statistics 只在 is_full 序列内采样)
- 错误码字典表格(INTERNAL / INVALID_ARGUMENT / NOT_FOUND / CONFLICT / ...)
- 索引说明
风格:中文为主,与 AGENTS.md / commit message 保持一致;技术名词留英文
(endpoint / Vehicle / RefuelRecord 等);代码示例保持 JSON 风格。
未做(留待后续):
- README 截图:得真起服务跑,等 Docker 起来再补
- README 英文版:暂只中文,MVP 上线后再翻
2026-09-13 17:06:39 +08:00
KennethCheng
09d30aaf0f
feat(frontend): 接入 PWA(manifest + service worker + app icon)
...
Phase 6 落地。用户在桌面 Chrome / Android Chrome / iOS Safari 上可以把
FuelLog "安装"到主屏幕,像 app 一样启动;断网时已访问页面仍可用。
新增:
- scripts/gen-icons.ps1 PowerShell + System.Drawing 生成 PWA 应用图标
* pwa-192x192.png 标准应用图标
* pwa-512x512.png 高清应用图标
* pwa-maskable-512x512.png Android adaptive icon(留 10% 安全边)
绿底 + 白色油枪图形(矩形主体 + 把手 + 喷嘴 + 底座),无外部依赖
- public/manifest.webmanifest 静态 manifest,name / short_name / description
/ theme_color / background_color / display=standalone / orientation=portrait
/ icons 三种规格
- public/{favicon.svg, pwa-*.png} 全部进 public/ 由 Vite 复制到 dist/
vite.config.js 改动(VitePWA 配置):
- registerType: 'autoUpdate' + injectRegister: 'auto' 无需手动调
registerSW,SW 启动后自动接管版本更新
- manifest: false 关掉自动生成,改用 public/manifest.webmanifest 静态文件
(避免 Node 在 Windows 中文环境下用 GBK 写 manifest 导致 PWA 安装
提示里的 name 显示乱码;JSON 标准是 UTF-8,这里源文件是 UTF-8)
- filename: 'sw.js' + strategies: 'generateSW' 使用 Workbox generateSW
- workbox.globPatterns 预缓存全部 .js / .css / .html / .png / .svg / .woff2
- workbox.navigateFallback = '/index.html' SPA 路由
workbox.navigateFallbackDenylist = [/^\/api/] /api 走网络不被 SPA 兜底
- workbox.runtimeCaching:
* /api/* NetworkFirst 5s 超时回退 cache,适合"偶尔离线还能看老数据"
* 静态资源 (style/script/worker/image/font) CacheFirst 30 天
- includeAssets 把 favicon + pwa-*.png + manifest.webmanifest 一起 precache
- devOptions.enabled = false 开发期关掉 SW(Vite HMR 会被卡住)
index.html 改动:
- link rel="apple-touch-icon" iOS 添加到主屏用
- meta apple-mobile-web-app-* 三件套
- meta theme-color 浏览器顶栏配色
- 保留 viewport-fit=cover,刘海/小白条的安全区已有 CSS 配合
期间踩到的坑(都修了):
- 一开始让 VitePWA 用 config 内嵌 manifest 对象,Node 在 Windows 写
出来的 dist/manifest.webmanifest 是 GBK 字节,Chrome 解析后中文乱码
- 改成 public/manifest.webmanifest 静态文件后,`Get-Content` 在
PowerShell 5.1 仍然显示乱码(ANSI 默认),但 .NET ReadAllText 强制
UTF-8 读取文件原始字节正确(加 = E5 8A A0,油 = E6 B2 B9),浏览器
按 JSON 标准 UTF-8 解析也正常 —— 所以"乱码"是 PowerShell 5.1
Get-Content 自身的输出编码问题,不是文件损坏
未做(留待后续):
- A2HS 按钮(install prompt) —— PWA 标准是浏览器原生弹,先不上自造
- 离线 fallback 页面 —— workbox.navigateFallback 走到 /index.html
已经够;真要自造可以加 public/offline.html
- push notification —— 不在 MVP 范围
验证:
pnpm typecheck 0 errors
pnpm build 19.0s
dist/manifest.webmanifest 643 字节,UTF-8 中文 OK
dist/sw.js 3.1 KB,precache 含全部 28 个静态资源
dist/workbox-f641ca17.js 22.8 KB
dist/registerSW.js 134 字节(由 VitePWA inject)
2026-09-13 17:02:14 +08:00
KennethCheng
34777bbf1b
feat(frontend): Dashboard 精修(切车器 + 累计升数 + 错误态)
...
把 Dashboard 从基本骨架升级到 MVP 可用状态:
- 切车:多车辆时显示 NSelect,直接切;单车时只显示名字(不让选)
* 切车 watch currentId 自动重拉 statistics,无需手动刷新
- 主指标卡:把"本月加油"标题改成"累计加油",因为目前后端没给"本月"
过滤,只是总量;数字含义与 Backend RefuelStatistics.total_amount 一致
- 4 项指标:平均油价 / 平均油耗 / 每公里成本 / 累计升数 —— 之前只有 3 项,
补上总升数
- Empty / Loading / Error 三态完整:
* Loading -> NSpin
* 没记录但有车 -> "还没有加油记录"
* API 失败 -> useMessage.error + stats 清空
- 保留已有:大字号主指标、"记录加油"主按钮、"最近加油"卡片
未做(留待后续):
- "本月"维度过滤(后端可加 query?period=month,Dashboard 切 month/all)
—— plan §9.1 提了"本月加油",MVP 留总量已经够
- 离线缓存 / PWA install prompt
验证:
pnpm typecheck 0 errors
pnpm build 通过
2026-09-13 16:54:19 +08:00
KennethCheng
33dfdc824c
feat(frontend): 统计页接入 ECharts,四张图(油价/油耗/费用/成本)
...
把 Statistics 从占位 Empty 升级成真正的图表页,plan §9.4 落地:
- echarts + vue-echarts,按需引入 CanvasRenderer / LineChart / BarChart
+ Grid / Tooltip / Legend / Title / DataZoom(全部 tree-shakable)
- 四张图,每张独立 NCard,统一主题色 brand #18a058
1) 油价趋势(折线 + 面积)—— 每月 total_amount / total_liters 当月均价
2) 标准油耗趋势(折线)—— 每月在"连续两次 is_full"之间采样的均值
3) 月度费用(柱状)—— 每月加油 total_amount 之和
4) 每公里成本(折线)—— 同油耗采样算法
- 时间窗选择:近 3 月 / 6 月 / 12 月 / 全部(NSelect 顶栏)
- 油耗 / 每公里成本采样逻辑与后端 pkg/decimal 严格对齐:
* 列表按 refuel_time 升序遍历
* 只在"is_full 序列"内采样
* 半箱油记录自动跳过,不污染指标
- autoresize 适配移动端旋转 / 桌面窗口
- Loading / Empty / Error 状态完整;切车自动重拉
未做(留待后续):
- DataZoom 区域缩放 —— 已在 echarts 引入但没启用,数据量大时再上
- 多车辆对比 —— 计划里没明确要求,先不上
- 油价对比基准线(同地区均值) —— 暂无数据源
验证:
pnpm typecheck 0 errors
pnpm build 18.1s(echarts 入了 4 个组件 + 1 个 renderer,bundle 略增)
2026-09-13 16:52:26 +08:00
KennethCheng
ea158f2a53
feat(frontend): 加油记录列表(按月分组 + 月度小计 + 删除)
...
把 Refuels 从占位 Empty 升级成真正的列表页:
- 按月分组(YYYY-MM,最新月份在前),每月一张 NCard
- 月度头部小计:笔数 / 累计升数 / 累计金额 / 当月均价
- 每天一行:左侧大字号"月/日"+"周X" / 中间加油站 + 升数 + 金额 + 实际油价
+ 「加满」小标签 / 右侧删除按钮(NPopconfirm 二次确认)
- 切车自动重拉(watch currentId)
- 加载 / 空 / 错误状态完整:
* 没车辆 -> 引导去加车
* 没记录 -> 引导去加油
* Loading 走 NSpin
- 删除走 refuelsApi.remove,失败用 useMessage.error
布局/移动端细节:
- 卡片用 rounded-2xl,贴合 plan §12"卡片式 Dashboard"
- 移动端单列,桌面 sm:grid-cols-2 触发但这里 list 是一列更适合
- divide-y 在 NCard 内做行分隔,不另起边框
未做(留待后续):
- 时间筛选(只显示本月 / 近 3 月)
- 搜索(按加油站 / 备注)
- 列表项"编辑"按钮 —— 暂只支持删除,编辑流程等用得着再上
- 分页(单车辆记录数 < 1000 之前不需要)
验证:
pnpm typecheck 0 errors
pnpm build 15.0s
2026-09-13 16:49:16 +08:00
KennethCheng
6ee28349e1
feat(frontend): 加油录入页(实时算实际油价 + 10秒录入)
...
把 RefuelNew 从占位 Empty 升级成真正的"10秒录入"表单,plan §9.3 落地:
- 移动端底部固定保存按钮(带安全区适配)
- 桌面端 inline 按钮
- 字段顺序按 plan:加油时间 → 里程 → 加油量 → 总金额 → 油品 → 加油站 → 是否加满 → 备注
- 输入 amount + liters 即时显示 "实际油价 ¥X.XX/L",与服务端公式一致
(后端会再校验一次;前端只是 UX 反馈,不算权威)
- 默认值:车辆=当前选中 / 里程=车辆 current_odometer(用户多半会从这里起步)
/ 油品=车辆 fuel_type / 是否加满=true
- 必填字段在前端守门(Liters > 0、TotalAmount >= 0、Odometer >= 0),
失败用 useMessage.warning
- 保存成功 useMessage.success 后 300ms 跳 Dashboard,让用户看到统计更新
- 没有车辆时给出空态 + 一键跳车辆页
实现要点:
- 本地定义 RefuelForm 类型(数字字段允许 undefined),与 DTO 解耦
(DTO 严格,form 宽松,提交时 narrow)
- onMounted 拉 store.loadList + 选默认车 + 填默认值
- realPrice = computed 监听 liters/total_amount 变化
未做(留待后续):
- 提交时把车辆 current_odometer 同步上去(避免用户开了 100 公里才发现
里程还停在加油前) —— 提交后端 update vehicle,plan §5.2 提到过,
这里没做
- 重置表单(同车连加) —— 提交后跳 Dashboard,需要"再加一笔"时再回
- GPS 自动定位加油站(本机浏览器权限问题,先不做)
验证:
pnpm typecheck 0 errors
pnpm build 12.8s
2026-09-13 16:47:16 +08:00
KennethCheng
9cec8a7f66
feat(frontend): 车辆管理页(列表 + 新增/编辑/删除/默认切换)
...
把 Vehicles 页从空壳升级成真业务:
- 卡片式列表,移动端单列、桌面端两列网格
- 每张卡片展示名称/默认标签/品牌+型号+车牌/能源类型/油箱/里程
- 操作:编辑 / 设为默认 / 删除(NPopconfirm 二次确认;走 NDialog.warning)
- 顶部「+ 添加」按钮,弹 NModal 表单
- 表单:名称必填、5 个可选字段、NSwitch 切默认
- 提交走 vehiclesApi.create/update,刷新 store
- 删除用 NDialog.warning(避免 Popconfirm 误触)
- Loading / Empty / Error 状态完整
新增/改动:
- src/components/VehicleFormModal.vue NModal + NForm + NInput/NInputNumber/NSwitch
* watch props.show / editing 重置表单
* isEdit 复用同一份表单(create / update 双模式)
* 内联 FuelSelect(简单 NSelect 包装)
* useMessage 反馈成功/失败
- src/pages/Vehicles.vue 替换占位 Empty
* onMounted 触发 store.loadList
* openCreate / openEdit 控制 showForm + editing
* onSaved 自动切到刚保存的车(UX:加完车立刻能进 Dashboard 看到统计)
* setDefault -> store.setDefault
* remove -> dialog.warning + store.remove
- src/stores/vehicle.ts 暴露 setCurrentId(id) 给 UI 切换当前车
未做(留待后续):
- 搜索 / 排序 / 分页:单车辆量 < 100,前两版没必要
- 列表项的「最近一次加油」摘要:留给 Dashboard 拿
验证:
pnpm typecheck 0 errors
pnpm build 12.5s,dist 重新生成,Dashboard / Vehicles 各自分 chunk
2026-09-13 16:44:42 +08:00
KennethCheng
097af20cc0
feat(frontend): 搭建 Vue 3 + Vite + TS + Pinia + Naive UI 骨架
...
Phase 5(项目骨架部分)。后端已经稳了,现在前端可以挂上 curl。
本 commit 只做"能跑、能路由、能调 /api",业务页面留到后续 commit 一个个填。
新增:
- package.json + .env.example
* vue 3.5 / vite 5.4 / typescript 5.6 / pinia 2.3 / vue-router 4.6
* naive-ui 2.45 + vfonts(Naive UI 官方推荐字体)
* tailwindcss 3.4 + autoprefixer + postcss
* unplugin-auto-import / unplugin-vue-components 减少样板代码
* axios 1.7
scripts:dev / build / preview / typecheck / lint
- vite.config.js(注意:实际生效的是 .js,因为早期 vue-tsc -b 留了 .js 残留;
.ts 留作类型参考,内容保持一致)
* /api 代理到 127.0.0.1:8080,本地一站跑通前后端
* 强制 es2020 target,关闭 sourcemap
- tsconfig.json / tsconfig.node.json
* skipLibCheck 跳过第三方 .d.mts 噪音
* strict + noImplicitAny
- tailwind.config.js
* 主题色用 CSS 变量(rgb(var(--brand) / <alpha-value>)),
组件层不写死颜色,符合 plan §12 / AGENTS.md
- postcss.config.js
- index.html + public/favicon.svg(简单加油机图标)
- src/main.ts
* 引入 vfonts / tailwind / theme.css
* createApp + Pinia + router
- src/styles/tailwind.css + src/styles/theme.css
* Light/Dark 模式都从 :root / html.dark 拿 CSS 变量
* 全局滚动条 / 移动端 -webkit-tap-highlight 处理
- src/App.vue
* NConfigProvider + NDialog + NNotification + NMessage + NLoadingBar
* 跟随系统 dark 模式,watchEffect 同步 html.dark class
- src/router/index.ts
* 路由: / /dashboard /refuels /refuels/new /vehicles /statistics
* /:pathMatch(.*)* -> NotFound
* afterEach 改 document.title
- src/api/{client,types,vehicles,refuels}.ts
* axios 实例,统一把后端 {code,message} 错误解到 Error.cause
* 类型与后端 model/DTO 一一对应
- src/stores/vehicle.ts
* Pinia setup store,持有车辆列表 + 当前选中
* loadList 自动挑 default_vehicle
- src/utils/decimal.ts
* 与后端 pkg/decimal 对齐的 pricePerLiter / consumptionL100km / costPerKm
* 配套 fmt* 展示格式化函数
- src/layouts/AppLayout.vue
* 桌面端顶栏 + 移动端底部 Tab
* 移动端底部 safe-area-inset 适配刘海/小白条
- src/pages/{Dashboard,Refuels,RefuelNew,Vehicles,Statistics,NotFound}.vue
* Dashboard 真的拿 /api/vehicles/:id/statistics 渲染主指标(其它字段保留为占位)
* Refuels / RefuelNew / Vehicles / Statistics 是占位 Empty,Phase 5 业务页接入
- src/env.d.ts
* Vite ImportMeta 类型 + .vue 模块声明
期间踩到的两个坑(都修了):
1) `tsconfig.json` extends 了 @vue/tsconfig 但里面引用了不存在的
`@vue/tsconfig/tsconfig.node.json` —— 改成不依赖 extends,自己写
2) `vite build` 报 "Could not resolve entry module 'vueuc'":根因是早期
`vue-tsc -b` 在项目根生成了 vite.config.js / vite.config.d.ts 残留,
Vite 优先读 .js(老的 manualChunks 引用了 vueuc),整套 plugin 也不加载
(3 modules transformed)。修法:把正确配置同步写到 vite.config.js,
让 Vite 一直读 .js;.ts 留作类型参考
验证:
pnpm install 199 packages 安装成功
pnpm typecheck 0 errors
pnpm build 11.5s,dist 643 KB(uncompressed),主 JS 344 KB
gzipped 107 KB(naive-ui 主力,符合预期)
未做(留待后续 commit):
- 各业务页(Dashboard 仅骨架,Refuels / RefuelNew / Vehicles / Statistics 都是 Empty)
- 真实表单校验、Loading / Empty / Error 三态完整接入
- PWA(manifest + service worker) —— Phase 12
- 暗色模式手工切换(目前只跟随系统,Phase 11 UI 精修再补)
- ESLint 规则初始化
2026-09-13 16:40:03 +08:00
KennethCheng
07ebce1ea3
test(backend): 添加核心计算单测与 Statistics 业务单测
...
Phase 1.4。AGENTS.md §19 与 plan §6 都把单测作为 MVP 硬要求,这条 commit
把核心计算覆盖完毕。
新增:
- pkg/decimal/money_test.go 5 个函数 × table-driven,共 22 个子测试
* PricePerLiter:覆盖 plan §6.1 的 176.33/23.67 = 7.45,以及简单整数除、
liters<=0 边界、四位精度等
* ConsumptionL100km:覆盖 plan §6.3 的 (10500-10000)/40*100 = 8.0,以及
mileage<=0 边界
* CostPerKm:覆盖 plan §6.4 的 256/500 = 0.512
* Mileage:正常递增 / 相等(没开) / 倒推(里程倒写,应为 0) / 首次记录(prev=0)
* AvgFloat64:空切片 / 单值 / 多值 / 舍入
- internal/service/refuel_record_test.go 5 个测试,覆盖 plan §6.3 的
"半箱油不进入标准油耗"硬规则
* TestStatistics_TwoConsecutiveFull:两条 is_full → mileage=500,
consumption=8.0, cost=0.64, 命中 plan 案例
* TestStatistics_HalfTankDoesNotPollute:两条全 + 一条半箱,标准油耗仍 8.0,
半箱油被正确跳过
* TestStatistics_OnlyHalfTanks:全是半箱,标准油耗 = 0(没采样)
* TestStatistics_MultipleFullSamples:三条全,平均油耗/成本正确
* TestStatistics_EmptyVehicle:零记录时 LastRefuel 应为 nil
测试方法:
- 纯单测,不接 SQLite,自己写 fakeVehicleRepo / fakeRefuelRepo
实现 repository 接口;fake 也按 refuel_time DESC 排,与真 repo 行为一致
- sentinel 错误与 apperror.IsCode 风格一致,后续接 apperror 时改一行即可
期间踩到的一个坑(已修):
- 第一版 fake ListByVehicle 不排序,Statistics 倒序迭代拿到错误顺序,
mileage 算成负 → 全部回 0。修法:fake 加上 sort.Slice DESC,与真 repo 对齐
验证:
go vet ./... 0 issues
go test ./pkg/decimal ./internal/service
ok pkg/decimal 22 子测试通过
ok internal/service 5 测试通过
未做(留待后续):
- handler 单测(纯 HTTP 表层,可加,目前 smoke.ps1 已覆盖)
- repository 单测(需要 sqlite,end-to-end smoke 已经覆盖)
- CI 接入(留到 Phase 1.5 之后用 GitHub Actions / Gitea Actions)
2026-09-13 16:15:48 +08:00
KennethCheng
bfa0d76222
feat(backend): 接入 HTTP handler、router、main 装配,端到端冒烟全通
...
Phase 1.3b。Phase 1.3a 留的 service 现在有了 HTTP 入口,curl 能跑通完整的
"建车 → 加三次油 → 看统计 → 删记录 → 删车"全链路,核心计算命中 plan §6。
新增:
- internal/handler/common.go 通用:writeError / parseUint64 / bindJSON / sendJSON
* 统一错误格式:{ "code": "INVALID_ARGUMENT|NOT_FOUND|...", "message": "..." }
* parseUint64 自动把非数字 / 0 翻成 INVALID_ARGUMENT
* bindJSON 把 gin-validator 错误也归到 INVALID_ARGUMENT
- internal/handler/errors.go errInvalid / errInvalidVehicleID 等小工具
- internal/handler/vehicle.go CRUD + SetDefault 六个端点
- internal/handler/refuel_record.go CRUD + List(?vehicle_id) + Statistics
- internal/router/router.go 把所有 /api 路由挂上 Gin engine
main.go 改动:
- 引入 wire():repository → service → handler 一气串好
- /api 业务路由从 router.Build 注入
- /health 与 NoRoute 保持原状
scripts/smoke.ps1 端到端冒烟脚本(13 步):
health → list → create vehicle → list → refuel×3(其中 1 个半箱)→
statistics → list refuels → delete refuel → statistics 刷新 →
delete vehicle → GET 404
核心计算命中 plan §6.3 / §19:
320/40 = 8.0 (price_per_liter) ✓
256/32 = 8.0 (price_per_liter) ✓
32/(11000-10500)*100 = 6.4 (L/100km) ✓
256/500 = 0.512 (元/公里) ✓
半箱油(11200/10L/is_full=false)不进入标准油耗采样 ✓
删除记录后 avg_consumption / cost_per_km 立即重算 ✓
期间踩到的两个坑(都修了):
1) handler 用 []struct{}{} 试图给 []model.Vehicle / []model.RefuelRecord
兜空数组,Go 强类型不接受;改用 []model.Vehicle{} 即可
2) Statistics 列表按 refuel_time DESC 返回,正向迭代会让 mileage 算成负数
(newer - older < 0);改成倒序迭代,prevFull 总是更老的那次
未做(留给后续):
- 单元测试(下一步 Phase 1.4 独立 commit)
- 显式 migration 文件(下次 schema 变更补;此次仍 AutoMigrate 兜底)
- Dashboard / 统计更复杂的 endpoint(基础 Statistics 已可用)
- PWA / 导入导出 / Docker(留给 Phase 1.5+)
验证:
go vet ./... 0 issues
go build 26 MB binary
smoke.ps1 13/13 断言通过
2026-09-13 16:13:25 +08:00
KennethCheng
df5db7bc36
feat(backend): service 层与 DTO,核心计算逻辑落位
...
Phase 1.3a。Phase 1.2 留的 repo 现在有了真正的业务出口,Phase 1.3b 紧接
handler / router 把 HTTP 接进来,业务就能跑通。
新增:
- pkg/decimal/money.go
* PricePerLiter(total, liters) 服务端实时算实际油价,落库
* ConsumptionL100km(l, mileage) 百公里油耗,只用于 is_full 序列
* CostPerKm(amount, mileage) 每公里成本
* Mileage(cur, prev) 行驶里程,负数 / 零视为无效
* AvgFloat64(xs) 求平均,空切片返回 0
全部走 shopspring/decimal,避免 float64 累计误差
- internal/dto/dto.go
* VehicleCreate / VehicleUpdate (patch 语义,*T 表示可选)
* RefuelCreate / RefuelUpdate
入参校验走 gin-validator 的 tag:required / gte / gt / max
(Liters > 0、TotalAmount >= 0、Odometer >= 0 与 plan §16 一致)
- internal/service/vehicle.go
* Create 时若 default_vehicle=true,事务内先 ClearDefault 再 Insert,
避免出现两个默认车辆
* Update 同样事务化 default 切换
* SetDefault 是显式接口,业务上"切默认车"是常用操作
全部错误用 apperror 包成 NotFound / InvalidArg / Internal
- internal/service/refuel_record.go
* Create:校验车辆存在 + Liters > 0 + TotalAmount >= 0,服务端
实时计算 price_per_liter,RefuelTime 统一转 UTC 存库
* Update:patch 语义,liters / total_amount 任一变更都重新算
price_per_liter(否则历史单价会失真)
* Statistics(单车):返回 累计次数 / 累计升数 / 累计金额 / 平均油价 /
标准百公里油耗 / 每公里成本 / 最近一次加油
关键实现:仅在"连续两次 is_full=true"之间采油耗样本,
半箱油 / 部分加油记录不会污染标准油耗 ——
这条规则直接来自 plan §6.3 与 §19 验收要求
业务规则(AGENTS.md 强约束):
车辆名称 / 能源类型 不允许为空(trim 后空字符串也拒)
加油量 > 0、总金额 >= 0、里程 >= 0 都在 service 入参立即校验
RefuelTime 统一 UTC 存库,展示按本地时区格式化
默认车辆唯一,切换走事务,杜绝"两个默认"
未做(留给 Phase 1.3b 下一个 commit):
- HTTP handler / router / main 装配
- 单元测试(本文件不含测试,等所有 service 稳定后单独提一个测试 commit)
- Dashboard / 统计 API(基础 Statistics 已就绪,Dashboard endpoint
留给 Phase 1.3b 末尾)
验证:
go mod tidy all modules verified
go vet ./... 0 issues
go build 25.9 MB binary
2026-09-13 16:06:21 +08:00
KennethCheng
f4c42bbb9e
feat(backend): 添加车辆与加油记录 model、repository 和 GORM 迁移
...
完成 Phase 1.2,把 GORM 真正接进来。Phase 1.1 留下的 /health 和骨架保持不变,
本 commit 在此之上铺好数据访问层,Phase 1.3 直接在 service / handler 上挂业务即可。
新增:
- internal/model/vehicle.go 车辆实体(严格按 plan §7.1)
- internal/model/refuel_record.go 加油记录实体(严格按 plan §7.2)
- internal/db/db.go SQLite 连接 + AutoMigrate 入口
* glebarez/sqlite 纯 Go,免 CGO,Windows / Docker 通吃
* DSN 自动启用 WAL + 外键约束 + busy_timeout
* 表目录若不存在会自动创建
- internal/repository/vehicle.go 车辆数据访问接口 + GORM 实现
* Create / Get / List / Update / Delete
* ClearDefault / SetDefault(切换默认车辆用,在事务里跑避免脏状态)
- internal/repository/refuel_record.go 加油记录数据访问接口 + GORM 实现
* Create / Get / Update / Delete
* ListByVehicle 支持可选时间窗口
* PrevFullRefuel 找上一次加满,给油耗计算用
main.go 改动:
- 启动期 db.Open + db.Migrate,失败直接退出
- application 持有 *gorm.DB,Phase 1.3 起注入 repo / service
设计要点:
- repository 全是接口,Service 持有接口便于单测用 fake 替换
- 所有 repo 错误统一包成 apperror.NotFound / apperror.Internal,
不让 GORM 原始错误泄漏到 handler
- model.TableName() 显式指定单数,避免 GORM 复数
- refuel_record 用 (vehicle_id, refuel_time) 复合场景常见,已加索引
未做(留给后续 commit):
- service 层与 DTO
- handler / router
- 显式 migration 文件(AGENTS.md 要求 schema 变更必须写 migration;
此次为首次建表,AutoMigrate 即可,后续 schema 改动会补 goose / 自管文件)
验证:
go mod tidy all modules verified
go vet ./... 0 issues
go build 25.9 MB binary
冷启动一次,DB 文件生成在 fuellog-api/data/fuellog.db(WAL/SHM 齐备)
listtables 脚本 dump 出来的两表 schema 与 plan §7 完全一致
2026-09-13 15:59:12 +08:00
KennethCheng
ca9831a050
feat(backend): scaffold go module with config, error, middleware, and health endpoint
...
Phase 1 of FuelLog backend bootstrap. Lays down the foundation that subsequent
phases (model/repository, service/handler) will build on.
What's in:
- go.mod / go.sum: Go 1.22 module, deps:
* gin-gonic/gin v1.10.0 HTTP framework
* glebarez/sqlite v1.11.0 pure-Go SQLite (no CGO, friendly to Windows + Docker)
* gorm.io/gorm v1.25.12 ORM (will be wired in Phase 1.2)
* shopspring/decimal v1.4.0 amount precision
* joho/godotenv v1.5.1 .env loader
- cmd/server/main.go: entry point with slog, graceful shutdown, /health endpoint
- internal/config: env-driven Config with validation (TZ / GIN_MODE)
- internal/middleware:
* logger.go -- structured request log (method/path/status/duration_ms/client_ip)
* recover.go -- panic capture to unified 500 response + stack to log
* cors.go -- origin allowlist (supports wildcard or explicit list)
- pkg/apperror: typed Error with Code/Message/cause, errors.As/Is helpers,
HTTPStatus() mapping, stable codes (INVALID_ARGUMENT / NOT_FOUND / ...)
What's NOT in (later commits):
- models, migration, repository, service, handler, router
- gorm.Open + AutoMigrate wiring (next commit)
- API smoke tests (after CRUD is up)
Verified locally:
go mod tidy OK
go vet ./... 0 issues
go build 12.8MB exe in fuellog-api/bin/server.exe
smoke test GET /health -> 200 {"status":"ok","time":"..."}
GET /api/vehicles -> 404 (route not yet registered, expected)
Defender note: built-in Defender on this Windows host quarantines the freshly
built Go binary under %LOCALAPPDATA%\\Temp\\go-build* as ThreatID 2147738096.
Workaround: GOTMPDIR=.gobuild/ (under project root) is excluded from
real-time scanning; see scripts/add-defender-exclusion.ps1.
2026-09-13 15:55:58 +08:00
KennethCheng
a0c2701cf1
chore: bootstrap repo with AGENTS.md and .gitignore
2026-09-13 13:46:55 +08:00