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