**发现并已修复的文档滞后**(后期代码先于文档,审计时补齐): | 滞后点 | 修复 | |---|---| | `24.附录` users 表缺 `wechat_openid` 列(二期微信登录加的) | ✅ 已补 | | `24.附录` 缺 `refuel_records` 表(三期记账) | ✅ 已补 | | `24.附录` 错误码缺 `1012/1013`(三期) | ✅ 已补 | | `34.附录` 小程序页面目录缺 `calc/refuels`(三期) | ✅ 已补 | **核对一致项**: - API 路由 **A1-A23 + `/api/auth/wechat`** 全部注册,与 22/33/43 文档逐项对应 - `schema.sql` 11 表 = 24 附录(10 + refuel)+ 45 附录 - 小程序 8 页文件齐全(js/wxml/wxss/json × 8),`refuels.wxml` 数据绑定与 js 匹配 - H5 8 页面全部可访问 ## 2️⃣ 后期变更对前期的影响 ✅ 无破坏 综合回归 **20 项全 PASS**: - **一期**:账号密码登录 / Cookie 会话 / dashboard / provinces(31省) / history ✅ - **二期**:Bearer token 认证(与 Cookie **双通道共存**,互不冲突)✅ - **三期**:refuel 首条保存 / 区间油耗 8.25 / 统计 / 预填 ✅ - **前端**:9 个资源(8 页 + 静态 JS)全部 200 ✅ 关键:二期的 `users` 加列是**幂等增量迁移**(ALTER),未动一期任何表结构;一期 API 与前端全部正常。 ## 3️⃣ 整体可行性 ✅ 在可控范围 | 维度 | 状态 | |---|---| | 构建/静态检查 | `go build / vet` 全绿 | | 单元测试 | 6 包 17 单测全过(auth/crawler/cron/db/push/service) | | 规模 | 11 表 · 23+ 接口 · 8 H5 页面 · 8 小程序页面 · 17 份文档 · ~90 代码文件 | | 三阶段功能 | 监控预测提醒 → 微信小程序 → 加油记账/真实油耗,全部落地 | **非阻塞小项**(可后续优化,不影响运行): 1. `refuels.wxml` 第 11 行 `wx:for` 与 `wx:if` 同节点——功能正常,写法可规范 2. api 包 handler 层无独立单测——由端到端回归覆盖(service 层已有单测) --- **结论:项目整体健康、文档与代码对齐、三阶段功能完整可用。** 4 处文档滞后已修复,无功能性问题。可以进入部署/上线阶段。
11 KiB
11 KiB
⛽ 三期 · 记账功能 — API 详细设计
上游:
docs/40.记账概要设计.md、docs/24.附录.md(通用规则/错误码) 编号:A18-A23(延续一期 A1-A17) 全部 🔒登录,按user_id归属;vehicle_id必须属于当前用户,否则 404。
A18 · GET /api/refuels(🔒登录)
功能:查询某辆车的加油记录列表(倒序,含油耗计算字段)。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| vehicle_id | query | int | 是 | 车辆 id(须属于当前用户) |
| limit | query | int | 否 | 返回条数,默认 50,最大 200 |
| offset | query | int | 否 | 分页偏移,默认 0 |
响应(HTTP 200)
{
"code": 0, "message": "ok",
"data": {
"vehicle": { "id": 1, "name": "我的迈腾", "fuel_type": "95" },
"records": [
{ "id": 3, "refuel_at": "2026-09-24 18:00:00", "odometer": 12800, "price_per_liter": 8.29,
"amount": 300, "discount": 0, "net_amount": 300, "liters": 36.19, "fuel_type": "95",
"full_tank": true, "note": "", "calc_distance_km": 280, "calc_l_per_100km": 12.9,
"calc_yuan_per_km": 1.07, "anomaly": null },
{ "id": 2, "refuel_at": "2026-09-10 08:30:00", "odometer": 12520, "price_per_liter": 8.25,
"amount": 320, "discount": 20, "net_amount": 300, "liters": 36.36, "calc_l_per_100km": 7.3,
"calc_yuan_per_km": 0.58, "anomaly": null }
]
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| data.records[].id | int | 记录 id |
| data.records[].refuel_at | string | 加油时间 |
| data.records[].odometer | float | 码表里程(km) |
| data.records[].price_per_liter | float | 油价(元/升) |
| data.records[].amount / discount / net_amount | float | 支付 / 优惠 / 实付金额(元) |
| data.records[].liters | float | 加油量(升) |
| data.records[].fuel_type | string | 油号 |
| data.records[].full_tank | bool | 是否加满 |
| data.records[].calc_distance_km | float | null | 区间里程(距上次) |
| data.records[].calc_l_per_100km | float | null | 区间油耗 L/100km |
| data.records[].calc_yuan_per_km | float | null | 区间成本 元/km |
| data.records[].anomaly | string | null | odometer(里程回退)/ null |
处理逻辑:校验 vehicle_id 归属 → 按 refuel_at DESC, id DESC 分页查询。
DB 操作(SQL)
SELECT v.id, v.name, v.fuel_type FROM vehicles v WHERE v.id = ? AND v.user_id = ?; -- 不存在 → 404
SELECT id, refuel_at, odometer, price_per_liter, amount, discount, net_amount,
liters, fuel_type, full_tank, note, calc_distance_km, calc_l_per_100km,
calc_yuan_per_km, anomaly
FROM refuel_records WHERE vehicle_id = ?
ORDER BY refuel_at DESC, id DESC LIMIT ? OFFSET ?;
A19 · POST /api/refuels(🔒登录)
功能:新增加油记录。计算油耗(与上次差值)+ 漏记检测。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| vehicle_id | body | int | 是 | 车辆 id |
| odometer | body | float | 是 | 当前码表里程(km) |
| amount | body | float | 是 | 支付金额(元,>0) |
| price_per_liter | body | float | 是 | 油价(元/升,>0) |
| discount | body | float | 否 | 优惠金额,默认 0 |
| refuel_at | body | string | 否 | 加油时间,默认当前 |
| fuel_type | body | string | 否 | 油号,缺省取该车油号 |
| full_tank | body | bool | 否 | 本次是否加满(仅标签展示,不参与计算),默认 true |
| note | body | string | 否 | 备注 |
响应(HTTP 200)
{
"code": 0, "message": "ok",
"data": {
"record": { "id": 4, "refuel_at": "2026-09-24 18:00:00", "odometer": 12800,
"price_per_liter": 8.29, "amount": 300, "discount": 0, "net_amount": 300,
"liters": 36.19, "calc_distance_km": 280, "calc_l_per_100km": 12.9,
"calc_yuan_per_km": 1.07, "anomaly": null },
"possible_missing": true
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| data.record | object | 新记录(含油耗计算字段) |
| data.possible_missing | bool | 异常检测:true 提示"耗油量异常,是否漏记?" |
处理逻辑
- 校验参数与
vehicle_id归属。 net = amount - discount;liters = net / price(保留 2 位)。- 查该车最近一条
refuel_at < 本次(或odometer < 本次)的有效记录作基准。 - 若基准存在:
distance = odometer_now - last_odometerdistance > 0→ 计算calc_l_per_100km/calc_yuan_per_kmdistance <= 0→anomaly=odometer(里程回退,标黄提示)- 无基准(首条)→ 油耗 NULL
- 注:
full_tank仅作标签展示,不参与油耗计算。
- 异常检测(相比该车历史平均;无历史平均则跳过):
- 本次
price > 历史平均 price × 2 - 或
calc_l_per_100km > 历史平均 L/100km × 2→possible_missing=true("耗油量异常,是否漏记?")。
- 本次
- 入库;返回记录 +
possible_missing。
DB 操作(SQL)
-- 上次有效记录(该车,时间最接近本次的)
SELECT odometer, refuel_at FROM refuel_records
WHERE vehicle_id = ? AND (refuel_at < ? OR (refuel_at = ? AND id < ?))
ORDER BY refuel_at DESC, id DESC LIMIT 1;
INSERT INTO refuel_records(user_id, vehicle_id, refuel_at, odometer, price_per_liter,
amount, discount, net_amount, liters, fuel_type, full_tank, note,
calc_distance_km, calc_l_per_100km, calc_yuan_per_km, anomaly, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'), datetime('now','localtime'));
A20 · PUT /api/refuels/:id(🔒登录)
功能:修正加油记录。级联重算本条及受影响的下一条。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| id | path | int | 是 | 记录 id(须属于当前用户) |
| Body | body | object | 是 | 全量字段,同 A19 请求参数 |
响应
{ "code": 0, "message": "ok",
"data": { "record": { "...": "更新后的记录" }, "recalc_ids": [3] } }
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| data.record | object | 更新后的记录 |
| data.recalc_ids | array | 本次级联重算影响的记录 id 列表 |
处理逻辑
- 校验记录属于当前用户(
id + user_id)。 - 更新字段;重算本条(与上一条)。
- 若其后存在下一条,重算下一条(本条成为其基准)。
- 返回更新后的记录与
recalc_ids。
DB 操作(SQL)
SELECT id FROM refuel_records WHERE id = ? AND user_id = ?; -- 不存在 → 404
UPDATE refuel_records SET odometer=?, price_per_liter=?, amount=?, discount=?,
net_amount=?, liters=?, fuel_type=?, full_tank=?, note=?, updated_at=datetime('now','localtime')
WHERE id = ? AND user_id = ?;
-- 级联重算:见 docs/45.记账附录.md 第三节(同一事务)
A21 · DELETE /api/refuels/:id(🔒登录)
功能:删除加油记录。级联重算其后第一条(基准变为被删记录的上一条)。
处理逻辑:校验归属 → 删除 → 下一条的基准改为被删记录的前一条 → 重算下一条。
响应:{"code":0,"message":"ok"}
DB 操作(SQL)
DELETE FROM refuel_records WHERE id = ? AND user_id = ?; -- 0 行 → 404
-- 级联重算下一条:见 docs/45.记账附录.md 第三节
A22 · GET /api/refuels/prefill?vehicle_id=X(🔒登录)
功能:"记一笔"页回显:当前油价 + 上次码表里程(用户可修改)。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| vehicle_id | query | int | 是 | 车辆 id |
响应(HTTP 200)
{
"code": 0, "message": "ok",
"data": {
"vehicle": { "id": 1, "name": "我的迈腾", "province": "北京", "fuel_type": "95" },
"current_price": 8.29, // 该车省份最新 95# 价;无则 null(用户填)
"last_odometer": 12520, // 上次记录里程;无则 null
"last_refuel_at": "2026-09-10 08:30:00",
"avg_l_per_100km": 7.3 // 该车历史平均油耗(参考展示);无则 null
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| data.current_price | float | null | 回显油价(该车省份该油号最新价) |
| data.last_odometer | float | null | 回显上次里程(最近一条记录) |
| data.last_refuel_at | string | null | 上次加油时间 |
| data.avg_l_per_100km | float | null | 该车平均油耗(参考) |
DB 操作(SQL)
-- 当前油价(该车省份 + 该车油号)
SELECT CASE WHEN v.fuel_type='0' THEN pp.price_0
WHEN v.fuel_type='98' THEN pp.price_98
WHEN v.fuel_type='95' THEN pp.price_95
ELSE pp.price_92 END
FROM province_prices pp
JOIN (SELECT province, MAX(id) mid FROM province_prices GROUP BY province) t ON pp.id=t.mid
JOIN vehicles v ON v.province = pp.province
WHERE v.id = ? AND v.user_id = ?;
-- 上次里程/时间
SELECT odometer, refuel_at FROM refuel_records
WHERE vehicle_id = ? ORDER BY refuel_at DESC, id DESC LIMIT 1;
A23 · GET /api/refuels/stats?vehicle_id=X(🔒登录)
功能:油耗统计(折线图数据 + 汇总)。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| vehicle_id | query | int | 是 | 车辆 id |
响应(HTTP 200)
{
"code": 0, "message": "ok",
"data": {
"summary": { "total_records": 12, "total_amount": 3600, "total_liters": 450,
"avg_l_per_100km": 8.1, "avg_yuan_per_km": 0.55 },
"trends": {
"dates": ["2026-09-10", "2026-09-24", "2026-10-08"],
"l_per_100km": [8.2, 7.9, null],
"yuan_per_km": [0.58, 0.55, null],
"price": [8.25, 8.29, 8.29],
"cumulative_amount": [320, 650, 950]
}
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| data.summary.total_records | int | 记录条数(含无油耗的) |
| data.summary.total_amount / total_liters | float | 累计实付金额 / 累计加油量 |
| data.summary.avg_l_per_100km | float | null | 平均油耗(有 calc 的记录求均) |
| data.summary.avg_yuan_per_km | float | null | 平均元/km |
| data.trends.dates | array | 日期序列(按加油时间升序) |
| data.trends.l_per_100km | array | 对应 L/100km(无油耗为 null) |
| data.trends.yuan_per_km | array | 对应 元/km |
| data.trends.price | array | 对应油价 |
| data.trends.cumulative_amount | array | 累计实付金额(面积图) |
处理逻辑:取该车全部记录(升序),聚合汇总 + 折线序列。