Files
CarLog/README.md
T
wsh5485 fe17886ac4 feat: 洗车管理系统 v2.8 — 个人 detailer 单用户全栈应用
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 CRUD + 软删
- AI 截图识别(5 类型 OCR schema):OpenAI 兼容 + MiniMax M3
- 化学品 / Grocy 实例对接 + 库存镜像同步
- 仪表盘:30 天频次 + 健康度 + 同比环比 + 油价趋势 + 年均养护
- 月度报表:Excel 6 sheet + PDF
- PWA:manifest / SW / 离线缓存 / iOS 引导
- 安全:bcrypt + CSRF + 登录锁定(IP/用户/全局三级)+ 401 自动跳登录 + 表单草稿
- 高 ROI 8 功能:里程/提醒/成本/搜索/标签/通知/同比/成就
- 3 个新 migration(0016/0017/0018)+ 18 个迁移全幂等
- 101/101 测试通过(含 ipRateLimit / CSRF / retry / stats / tags / notifications)
- 部署:宝塔面板文档 + PM2 + Nginx
2026-06-20 21:11:54 +08:00

41 KiB
Raw Blame History

洗车管理系统

个人 detailer 爱好者的私家车管理系统。Express + MySQL/SQLite + Vue 3 全栈单用户应用,所有数据保存在你自己的服务器上。

适用场景:给自己洗的 / 帮朋友洗的、给车加油 / 充电 / 保养 / 上保险做完整台账,对接 Grocy 做汽美用品库存,自动抓天气。

功能特性

🌳 功能树(按领域组织)

洗车管理系统
├── 🔐 账号与权限
│   ├── 单用户登录(admin 默认账号 / bcrypt 密码)
│   ├── CSRF token 自动刷新 + 重试
│   ├── 登录失败锁定:IP 5 次锁 15 分 / 用户 5 次锁 30 分 / 全局 10 次锁 1 小时
│   ├── 锁定时显示「已错 N 次 / 还剩 N 次 / 锁定 X 分」+ 解锁倒计时
│   ├── session 过期 401 自动跳登录 + 表单草稿暂存
│   └── 修改密码 / 启用 / 停用账号
│
├── 🚗 车辆管理
│   ├── CRUD(车牌 / 品牌 / 型号 / 年款 / 颜色 / 类型)
│   ├── 软删除(is_deleted 标记,可从操作日志恢复)
│   ├── 车辆详情页:基本信息 + 健康卡片 + 6 月趋势图 + 5 tab 记录
│   ├── 车辆健康指标:油耗 / 电耗 / 洗车新鲜度 / 保养预测
│   └── 多车总览统计(总数 / 启用数 / 有洗车记录数)
│
├── 🧽 洗车记录(核心领域)
│   ├── CRUD + 类型:快速 / 标准 / 精洗 / 其他
│   ├── 自动抓天气快照(洗车当天的天气)
│   ├── 选化学品 → 自动扣减 Grocy 库存
│   ├── AI 截图识别小票 → 自动填表 + 失败兜底 modal
│   ├── 洗车前后对比照:上传 / 删除 / 并排对比
│   └── 批量删除
│
├── ⛽ 加油记录
│   ├── CRUD + 油品:92#/95#/98#/0#柴油/-10#/E92/E95/LPG
│   ├── 自动算百公里油耗(仅 is_full=1 相邻加满里程差 / 升数 × 100)
│   ├── 油价趋势图(按月聚合 derived_unit_price
│   └── AI 截图识别小票
│
├── 🔌 充电记录
│   ├── CRUD + 充电类型:home / slow / fast / public
│   ├── 自动算百公里电耗(kWh / 里程差 × 100)
│   ├── 起止 SOC(电量 %)
│   └── AI 截图识别小票
│
├── 🔧 保养记录
│   ├── CRUD + 动态项目(机油 / 机滤 / 空滤 / 刹车油 / 防冻液...
│   ├── datalist + presets 自动补全
│   ├── watch + auto calc total_cost = items Σ
│   ├── AI 截图识别小票
│   └── 下次保养里程预测
│
├── 🛡️ 保险记录
│   ├── CRUD + 类型:交强 / 商业 / 车损 / 三责 / 座位 / 不计免赔 / 玻璃 / 划痕 / 自燃 / 涉水
│   ├── 到期提醒(30 天内橙色,已过期红色)
│   ├── 附件上传(PDF 保单 / 照片)
│   └── AI 截图识别保单
│
├── 🧴 化学品 / 汽美用品
│   ├── Grocy 实例对接(session cookie / API token 双模式)
│   ├── 同步模式:手动 / 启动自动(grocy_pull_auto
│   ├── 产品镜像:拉 Grocy 全量产品 + 库存到本地
│   ├── 库存操作:入库 / 扣减 / 盘点 / 低库存预警(grocy_low_stock_pct
│   ├── 分类映射:本地分类 ↔ Grocy 分类
│   ├── 批量入库(BatchPurchase
│   └── 全局搜索(grocy-search 走 Grocy API
│
├── 📊 数据可视化
│   ├── 总览(stats/overview):今日 / 本月 / 30 天频次 / 月度成本
│   ├── 健康仪表盘:6 月趋势堆叠柱
│   ├── 油价趋势(stats/extra.fuelTrend):按月 derived_unit_price
│   ├── 年均养护成本(stats/extra.costPerVehicle):所有成本 / 持有天数 × 365
│   ├── 洗车季节频率(stats/extra.washSeason):按月 cnt + avg_cost
│   └── 各车成本明细表
│
├── 📑 报表
│   ├── 月度报表 Excel(6 sheet:车辆/洗车/加油/充电/保养/保险)
│   ├── 月度报表 PDF
│   └── 月份下拉(reports/monthly/list,过去 12-24 个月)
│
├── 🤖 AI 截图识别
│   ├── 5 种类型 OCR schemawash/refuel/charge/maint/insurance
│   ├── 多 providerOpenAI 兼容 / MiniMax M3 多模态
│   ├── Provider 切换 + API key 配置
│   ├── 「测试连接」按钮(动态选真实上传图,避免 1×1 PNG 触发敏感)
│   ├── thinking 关闭(MiniMax M3 OCR 任务)
│   └── 识别失败兜底 modal(左图右表,手动填)
│
├── ⚙️ 设置
│   ├── 个人信息:用户名 / 修改密码
│   ├── AI 配置:provider / URL / key / model / 启用
│   ├── Grocy 配置:URL / 用户名 / 密码 / token / 同步策略
│   ├── 天气:默认城市(库尔勒)/ 实时天气
│   ├── 系统:登录锁定参数 / session 有效期 / cookie secure
│   ├── 数据重置(confirm_token 强校验)
│   └── Grocy 同步日志
│
├── 📜 操作日志
│   ├── 所有写操作记录(created/updated/deleted/recovered
│   ├── 软删记录可一键恢复
│   └── 类型筛选(vehicles/washes/refuels/...
│
├── 🛡️ 安全 & 防滥用
│   ├── bcrypt 密码
│   ├── express-session + httpOnly cookie
│   ├── CSRF token(所有非 GET 请求校验)
│   ├── 登录防撞库(IP + 用户 + 全局三级)
│   ├── IP 限流(AI 60s/10 次,sync 60s/10 次)
│   ├── 422 输入校验(字段必填 / 类型 / 长度)
│   └── 操作日志审计
│
├── 📱 PWA
│   ├── manifestid / icons 192/512/maskable/apple-touch / shortcuts
│   ├── Service Workervite-plugin-pwa autoUpdate
│   ├── 离线缓存:API static30 天 CacheFirst+ uploads + imagesSWR+ fonts
│   ├── navigateFallback → /index.html(白名单 /api/、/uploads/
│   ├── iOS / Android / Desktop 安装提示(beforeinstallprompt 拦截 + 引导)
│   ├── 新版本可用提示(needRefresh toast
│   └── 离线就绪提示
│
├── 🔧 工具与脚本
│   ├── 备份:bin/backup.jsSQLite 拷贝 / MySQL mysqldump
│   ├── 导出:bin/export.jsJSON / CSV,单表 / 全量)
│   ├── 灌种子:bin/seed-demo.js
│   ├── 重置:bin/reset-all.js(强 confirm_token
│   ├── Grocy 拉取:bin/grocy-refresh-products.js
│   ├── 天气刷新:bin/weather.js
│   ├── 账号管理:bin/users.jsdisable / enable / reset pwd
│   ├── 验证:bin/verify.js
│   └── 迁移:bin/migrate.js15 个 SQL 幂等执行)
│
├── 🩺 运维
│   ├── 健康检查:`/api/health`(兼容)+ `/api/health/live`(进程活)+ `/api/health/ready`DB 通)
│   ├── 调试面板:DebugPanelAPI 调用日志 + Vue error + unhandledrejection
│   └── OpenAPI 文档:`/api/docs`Swagger UI+ `/api/openapi.json`
│
└── 🌐 部署
    ├── Express HTTP 服务(port 8787
    ├── 静态资源托管(uploads/
    ├── SPA fallbackclient/dist/
    ├── 宝塔面板部署文档(PM2 + Nginx + SSL
    └── Docker-readycarlog-init.sql 幂等)

## 🛠 技术栈

- **后端**Node.js 18+ / Express 4 / MySQL 8 (主) / SQLite (回退)
- **前端**Vue 3 + Vite + Pinia + Vue Router + Naive UI
- **外部依赖**Grocy(汽美库存,可选)/ wttr.in(天气)/ OpenAI 兼容 AI(可选)

## 📦 目录结构

洗车管理系统/ ├── client/ # Vue 3 前端 │ ├── dist/ # ← 构建产物(已包含在 zip 里,直接部署) │ ├── src/ │ │ ├── api/ # API 客户端 │ │ ├── views/ # 页面(17 个) │ │ ├── components/ # 组件 │ │ ├── stores/ # Pinia 状态 │ │ └── router.js │ └── package.json ├── server/ # Express 后端 │ ├── src/ │ │ ├── routes/ # 路由(auth/washes/chemicals/vehicles/... │ │ ├── services/ # 业务逻辑(grocyClient/weather/backup/... │ │ ├── middleware/ # auth/csrf │ │ ├── db.js # MySQL/SQLite 统一接口 │ │ ├── config.js # 配置加载(DB_URL / Grocy / AI │ │ ├── setup.js # 首次初始化向导 │ │ └── bin/ # 命令行工具 │ │ ├── serve.js # 启动服务器 │ │ ├── migrate.js # 跑迁移 │ │ ├── reset-all.js # 清空 + 可选灌种子 │ │ ├── backup.js # 备份 SQLite/MySQL │ │ ├── export.js # CSV/JSON 导出 │ │ └── ... │ └── migrations/ # SQL 迁移 │ ├── 0001_init.sql │ ├── mysql/ # MySQL 专属 │ └── ... ├── docs/ │ └── install/ │ └── INSTALL-BT-NODE.md ├── 洗车管理系统-v2.0-源码.zip # ← 部署包(492 KB,不含 node_modules/.env └── .env # 配置(DB / Grocy / AI / Session Secret,不在 zip 里)


## 🚀 安装部署

### 1. 准备环境

- Node.js **≥ 18**
- MySQL 8.x(推荐;没有的话会自动回退 SQLite)
- 一个 Grocy 实例(可选,没有也能用,只是化学品模块受限)

### 2. 克隆并安装

```bash
git clone <your-repo-url> 洗车管理系统
cd 洗车管理系统

# 后端依赖
cd server && npm install

# 前端依赖
cd ../client && npm install

3. 配置 .env

在项目根目录 洗车管理系统/.env

# ====== 数据库(MySQL 优先,缺省回退 SQLite======
DB_HOST=162.14.110.130
DB_PORT=33306
DB_USER=carlog
DB_PASSWORD=你的密码
DB_NAME=carlog

# 或使用完整 URL
# DB_URL=mysql://carlog:你的密码@162.14.110.130:33306/carlog

# ====== 服务端 ======
NODE_ENV=production
PORT=8787

# Session 加密(生产必改!)
SESSION_SECRET=你的长随机字符串

# ====== Grocy(可选)======
GROCY_URL=https://your-grocy.example.com/
GROCY_USERNAME=admin
GROCY_PASSWORD=your-password
# 也可以用 API Key
# GROCY_API_KEY=your-api-key

# ====== AI(可选)======
AI_PROVIDER_URL=https://api.openai.com/v1
AI_API_KEY=sk-xxx
AI_MODEL=gpt-4o-mini

说明:所有设置项都能在 Web UI 的「设置」页 里再改。.env 只是初始默认值。

4. 初始化数据库

cd server
node src/bin/migrate.js
# 输出:✓ 0001_init.sql ... ✓ 0014_grocy_auth.sql

5. 构建前端

cd ../client
npm run build       # 产物在 client/dist/

6. 启动服务

cd ../server
node src/bin/serve.js
# [server] http://0.0.0.0:8787
# [db] MySQL connected (carlog)

首次访问 http://localhost:8787/ 会跳到 /setup 引导你创建管理员账号。

7. (可选)灌种子数据

cd server
node src/bin/reset-all.js --seed   # 清空 + 写 2 辆车 + ~50 条记录
node src/bin/seed-demo.js          # 单独跑种子(不清空)

8. 生产部署(PM2

npm install -g pm2
cd server
pm2 start src/bin/serve.js --name carwash
pm2 save
pm2 startup

Nginx 反向代理示例:

server {
    listen 80;
    server_name carwash.your.domain;

    location / {
        proxy_pass http://127.0.0.1:8787;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

🐯 宝塔面板部署(推荐国内服务器)

宝塔自带 Node.js / MySQL / Nginx,不用自己折腾环境。

准备工作

宝塔软件商店装好:

  • Nginx 1.22+
  • MySQL 8.0 (或 5.7,文档以 8.0 为准)
  • PM2 管理器(宝塔商店搜不到就用 Node.js 版本管理器里的 PM2
  • Node.js 18+(用宝塔的 Node.js 版本管理器装)

1. 上传源码

洗车管理系统-v2.0-源码.zip492 KB,已排除 node_modules 和 .env)传到宝塔,比如:

/www/wwwroot/carwash/

宝塔文件管理器 → 上传 zip → 右键解压。解压后结构:

/www/wwwroot/carwash/
├── client/dist/        # 已构建好的前端
├── server/
├── docs/
├── package.json
└── README.md

2. 建数据库

宝塔 数据库 → 添加数据库:

  • 数据库名:carlog
  • 用户名:carlog
  • 密码:点「随机生成」,复制保存(等下要写到 .env
  • 编码:utf8mb4
  • 访问权限:本服务器

2.5 初始化数据库(任选一种方式)

方式 A:一键 SQL(推荐,宝塔友好)

直接把根目录的 carlog-init.sql28 KB)导入:

  • 宝塔:数据库 → 选 carlog 库 → 导入 → 选 carlog-init.sql → 提交
  • 或命令行:
    mysql -ucarlog -p carlog < carlog-init.sql
    

这个 SQL 已经做了完全幂等(存储过程 + try-catch),已存在的表/索引/列会自动跳过,反复重跑不会破坏数据。也无需再跑 migrate.js(migration 表会标记为已应用)。

方式 B:用源码迁移

如果偏好用源码版本(库已有数据想增量迁移):

cd /www/wwwroot/carwash/server
node src/bin/migrate.js

在项目根目录(/www/wwwroot/carwash/.env)新建文件:

# ====== 数据库 ======
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=carlog
DB_PASSWORD=刚才复制保存的密码
DB_NAME=carlog

# ====== 服务端 ======
NODE_ENV=production
PORT=8787

# Session 加密(务必改成 32 位以上随机字符串,宝塔「终端」跑 node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" 生成)
SESSION_SECRET=你的32位随机字符串

# ====== Grocy(可选)======
GROCY_URL=
GROCY_USERNAME=
GROCY_PASSWORD=

# ====== AI(可选)======
AI_PROVIDER_URL=https://api.openai.com/v1
AI_API_KEY=
AI_MODEL=gpt-4o-mini

4. 装依赖

宝塔 → 终端(确保当前在项目根目录):

cd /www/wwwroot/carwash/server
npm install --production
# 后端依赖装好(数据库已经在 2.5 步导入过了)

cd ../client
npm install
# 前端依赖装好(dist 已经预构建,不重新 build 也行;改前端代码时才需要 npm run build

注意:宝塔终端默认用 root 用户,文件权限直接就是 755。如果遇到权限问题:chown -R www:www /www/wwwroot/carwash

5. 用 PM2 启动

宝塔 PM2 管理器 → 添加项目:

项目名称 carwash
运行目录 /www/wwwroot/carwash/server
启动文件 src/bin/serve.js
启动选项 留空
端口 8787
Node 版本 18+(用版本管理器切换)

点「提交」启动。日志会显示 [server] http://0.0.0.0:8787 就是成了。

6. 配 Nginx 反代

宝塔 网站 → 添加站点 → 域名填你的(比如 carwash.your.domain)→ PHP 选「纯静态」

然后在站点设置里 配置文件,把 location / 段替换成:

location / {
    proxy_pass http://127.0.0.1:8787;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 60s;
}

保存。

7. 首次访问

浏览器打开你的域名,首次会自动跳到 /setup,跟着引导:

  1. 创建管理员账号(用户名 + 密码)
  2. 选是否启用 Grocy(也可以先跳过,回设置里填)
  3. 完成 → 登录

8. 申请 SSL(可选但强烈推荐)

宝塔站点 → SSL → Let's Encrypt → 选域名 → 申请 → 强制 HTTPS 打开。

申请完 SESSION_SECRET 不动,但 .env 里的 session.cookie_secure 改成 true(在 Web 设置页里改也行),重启 PM2。

9. 备份策略

宝塔 计划任务 加两条:

  • 每日 03:00node /www/wwwroot/carwash/server/src/bin/backup.js
  • 每周日 03:00/www/server/mysql/bin/mysqldump -ucarlog -p你的密码 carlog | gzip > /www/backup/carlog_$(date +\%F).sql.gz

10. 常见问题

现象 排查
PM2 启动后立刻 exit 看 PM2 日志;多半是 .env 拼写错误或数据库连不上
访问域名 502 PM2 没起;Nginx 反代 IP 错了;先 curl http://127.0.0.1:8787/api/health 看后端通不通
/setup 一直重定向 /www/wwwroot/carwash/.setup_done 文件存在说明已经初始化过;删了重置
Grocy 同步失败 在 Web 设置页测连接;Grocy 实例要能从服务器访问到
时区不对 服务器 date 看是 UTC 就 timedatectl set-timezone Asia/Shanghai;或保持 UTC,MySQL 内部也是 UTC,前端会按本地显示

升级步骤

cd /www/wwwroot/carwash
# 1. 备份
node server/src/bin/backup.js

# 2. 拉新代码(如果你用 git)或重新上传新 zip 解压覆盖
#    注意保留 .env 和 uploads/

# 3. 装新依赖
cd server && npm install --production
cd ../client && npm install && npm run build

# 4. 跑新迁移(自动跳过已跑过的)
cd ../server && node src/bin/migrate.js

# 5. 重启 PM2
pm2 restart carwash

🧪 测试结果(2026-06-18

每一项都用 curl 打了真请求,全过:

模块 测试 结果
登录 POST /api/auth/login 返回 {ok:true, data:{user}}
CSRF GET /api/auth/csrf
车辆列表 GET /api/vehicles 返回数组(每条带 wash_count, total_cost
车辆统计 GET /api/vehicles/stats {total:3, active:3}
车辆创建 POST /api/vehicles 返回 id
车辆详情 GET /api/vehicles/:id
车辆更新 PUT /api/vehicles/:id
车辆软删 DELETE /api/vehicles/:id
洗车列表 GET /api/washes {rows,total,page,limit}
洗车创建 POST /api/washes (字段 wash_typeservice_type
洗车详情 GET /api/washes/:id 含天气快照 + 化学品列表
洗车删除 DELETE /api/washes/:id
化学品列表 GET /api/chemicals 224 条
化学品详情 GET /api/chemicals/:id 含 Grocy stock entries
化学品更新 PUT /api/chemicals/:id
化学品创建 POST /api/chemicals 写入 Grocy + 本地
化学品扣减 POST /api/chemicals/:id/consume 调 Grocy API
Grocy 同步 POST /api/chemicals/sync 224 个产品从 Grocy 拉取
加油 CRUD /api/refuels 全部
充电 CRUD /api/chargings 全部
保养 CRUD /api/maintenances items 数组 ↔ items_json 自动转换
保险 CRUD /api/insurances (字段 companyinsurer
保险附件 POST /api/insurances/:id/upload multer
设置读 GET /api/settings 返回全部 24 项
设置写 POST /api/settings 按 group 通用 update
城市读 GET /api/settings/city {saved_city, default_city, is_auto_today}
天气 GET /api/settings/weather wttr.in 实时数据
Grocy 同步日志 GET /api/settings/grocy-logs
数据重置 POST /api/settings/reset 需要 confirm_token=RESET-ALL-DATA
操作日志 GET /api/operation-logs 软删自动记录
恢复记录 POST /api/operation-logs/:id/recover
AI 配置 GET /api/ai/config
AI 识别 POST /api/ai/recognize (需先 upload
仪表盘 GET /api/stats/overview
健康检查 GET /api/health

测试中发现并修复的 Bug

# 文件 Bug 修复
1 server/src/services/grocyWrite.js m.ensureCookie is not a functionensureCookie 未从 grocyClient 导出,却通过 import().then(m => m.ensureCookie) 改用导出函数 grocyPost
2 server/src/routes/logs.js Column 'items_json' cannot be null:保养 POST 时前端传 items: [],但 INSERT 直接拿 b.items_json,没序列化 POST/PUT 加 Array.isArray(b.items) ? JSON.stringify(b.items) : ...
3 server/src/routes/logs.js enrich()JSON.parse(r.items_json),但 MySQL JSON 列已自动解析成对象 → 抛错 → 返回空数组 Array.isArray(r.items_json) 分支
4 server/src/routes/chemicals.js You can't specify target table 'chemicals' for update in FROM clausesync 路由 UPDATE 用了 OR grocy_product_id IN (SELECT FROM chemicals) 自引用 简化为 WHERE source = 'grocy'

📝 更新日志

v2.8(当前版本 · 2026-06-20

Trae 加了 8 个新功能(高 ROI 4 个 + 中 ROI 3 个 + 长期 1 个),并修了我找到的 4 个 bug:

新功能

  • 🚗 里程表录入 + 提醒中心
    • 新增 vehicles.current_km 字段(手动校准,NULL 时按各日志表 MAX 算)
    • /api/reminders 聚合提醒:加油 > 30 天、保养 > 180 天、洗车 > 14 天
    • /api/reminders/prefs GET/PUT 阈值(按用户)
  • 💰 成本分类占比/api/stats/cost-breakdown 返 5 分类(洗车/加油/充电/保养/保险)的总金额 + 百分比 + 颜色(用于饼图)
  • 🔍 顶栏全局搜索/api/search?q=... 跨 7 个领域(车辆/洗车/加油/充电/保养/保险/化学品)搜,返带高亮匹配字段的分组结果
  • 📊 同比/环比/api/stats/compare 返本月 vs 上月 / YTD vs 去年同月,5 个领域各给 mom_pct / yoy_pct
  • 🏷️ 标签系统tags + record_tags 两张表,CRUD + toggle 挂载,5 种 record_typewash/refuel/charge/maintenance/insurance),可按 tag_id 找所有打了该标签的记录
  • 🔔 站内通知中心notifications 表 + 推送工具函数(pushNotification),OCR 完成 / 同步成功 / 备份完成等可持久化通知,GET/POST/标已读/全部标已读
  • 🏆 成就系统:14 个预置成就(洗车新手→狂魔 / 一周一洗 / 万里征程 / 十万俱乐部 / 保险达人等),自动算 progress、解锁时持久化到 user_achievements

Bug 修复Trae 引入的):

  • 🐛 MySQL pool 连接超时(生产 P0server/src/db.js 的 mysql2 pool 没开 enableKeepAlive。MySQL 默认 wait_timeout=28800s 会关掉 idle 连接,客户端不主动 ping → 下次 query 报 ETIMEDOUT → login / dashboard / 任何接口卡 60s 直到 axios timeout → 页面"转圈卡,什么也点不动"。两重修复1) mysql2 pool 开 enableKeepAlive:true + keepAliveInitialDelay:30000(每 30s 发 ping 保持连接活);2) queryWithRetry() 包装 — 一次性 ETIMEDOUT / ECONNRESET / PROTOCOL_CONNECTION_LOST 自动 retry 一次(pool 会建新连接)。
  • 🐛 4 个新路由 ok() helper 不包 {ok,data}extra.js / achievements.js / tags.js / notifications.js 全部用裸 res.json(data),导致前端 axios interceptor 解包失败拿不到 data。统一改成 res.json({ok:true, data})
  • 🐛 /api/stats/compare 时区错 8 小时:跟 v2.5 同款 buggetMonth() 等本地方法),但 dateCol 存的是 UTC 字符串。改成 getUTCMonth() + Date.UTC() 构造。
  • 🐛 user_achievements.id 没 SELECT 出来导致 UPDATE 失效achievements.js line 92 SELECT 没选 id 字段,后续 UPDATE 用 existing.id 是 undefined。加 id 到 SELECT 列表。
  • 🐛 lastInsertRowid 是 BigInt 没 Number 转换tags.js / notifications.js 返 r.lastID || r.lastInsertRowidmysql2 是 BigIntJSON 序列化会变 1n。改成 Number(r.lastInsertRowid)

数据库3 个新迁移(0016_vehicle_current_km 加 vehicles.current_km 列 + notification_prefs + notifications 表,0017_tags tags + record_tags 表,0018_achievements achievements + user_achievements 表 + 14 条预置数据)

测试

  • 新增 routes.extra.test.js7 个用例:reminders 包装 / 加油提醒 / cost-breakdown 5 分类 + 百分比 / compare 月环比同比)
  • 新增 routes.tags.test.js8 个用例:CRUD / toggle 双向 / 非法 record_type 400 / 重名 409 / 级联删除)
  • 新增 routes.notifications.test.js(6 个用例:列表 + unread 计数 / 创建 / 标已读 / 全部已读)
  • 总测试数 76 → 97 全过

v2.72026-06-20

新功能

  • 401 自动跳登录 + 表单草稿client/src/api/client.js axios interceptor catch 401 → 触发 form-draft:flush-all 事件把所有 useFormDraft 暂存到 sessionStorage → 跳 /login?reason=expired&redirect=原页;登录成功后回原页,草稿自动恢复。
  • AI OCR 失败兜底 modalclient/src/components/AiFallbackModal.vue + client/src/composables/useAiRecognize.js 重构,识别失败时不再 alert 而是打开左图右表的 modal,用户对照图填表。
  • IP 限流server/src/middleware/ipRateLimit.js 内存版限流,/api/ai/*recognize + test)每分钟 10 次,/api/chemicals/sync + /grocy-search + /refresh-ids 每分钟 10 次;429 + Retry-After + X-RateLimit-* headers。
  • CSRF 403 自动 refresh 重试:客户端 interceptor 收到 403 CSRF → 调 /api/auth/csrf 拿新 token → 重发原请求 1 次(用 _csrfRetried flag 防双发)。
  • 健康检查拆分/api/health(兼容旧)+ /api/health/live(进程活着)+ /api/health/readyDB SELECT 1 通过),k8s/宝塔监控能区分"我在启动"和"我坏了"。
  • 3 个真正有用的图表数据GET /api/stats/extrafuelTrend / costPerVehicle / washSeasonStats.vue 加油价趋势 + 每辆车年均养护 + 洗车季节频率 + 各车成本明细表。
  • OpenAPI 文档/api/docs Swagger UI + /api/openapi.json schema29 条核心路由有 JSDoc 注释。

Bug 修复Trae 引入的):

  • 🐛 WashNew.vue modal 永远显示line 14 :show="ai.showFallback.value" 模板里用 ref 的 .value → 传的是 Ref 对象本身(永远 truthy),改成 :show="ai.showFallback" 让 Vue 模板自动解包。其他 viewChargingList/InsuranceList/RefuelList/WashShow/MaintenanceList)都正确用 const aiBusy = ai.busy 别名,没踩这个坑。
  • 🐛 Swagger 0 pathsswagger.js 用相对路径 './src/routes/*.js' 但 swaggerJsdoc 跑在进程 cwd,扫不到文件。改成 path.resolve(__dirname, ...) 绝对路径,0 → 29 routes
  • 🐛 /api/stats/extra 缺包装settings.js 的 ok() helper 不包 {ok,data},前端 axios interceptor 解包失败,Stats.vue 拿到的 extraR.data 是 undefined → fallback 到空数组 → 3 张图没数据。改成显式 res.json({ok:true, data:{...}})
  • 🐛 /api/ai/test 没加 rate limitTrae 只在 /ai/recognize 加了 aiRateLimit,但 /ai/test 没加,结果限流测试 12 次都通过。补上。
  • 🐛 swagger JSDoc 缺失 + JSDoc 块复制粘贴出错Trae 加 swagger 时只注释了 6 处,我给所有 CRUD 路由补齐(共 29 个 paths)。中间出了个 JSDoc 块重复贴导致的语法错误,已修。

测试

  • 新增 middleware.ipRateLimit.test.js(7 个用例:第一次/第二次 headers/max 边界/不同 IP/XFF 优先/窗口过期/_clearBuckets
  • 新增 routes.stats.test.js(4 个用例:包装/油价字段/车辆成本/季节聚合)
  • 总测试数 64 → 76 全过

v2.62026-06-19

v2.52026-06-19

Bug 修复

  • 🐛 登录锁定时区错 8 小时server/src/services/rateLimit.jsnowIso()upsertLock()getHours() 等本地方法生成 DATETIME 字符串,但 db.js 配的是 timezone: 'Z'UTC)。在 UTC+8 时区下,5 次输错密码后会被锁 8 小时而不是 30 分钟,「locked_until」会显示「明天早上 6:50」而不是「今晚 23:05」。已改成 getUTCHours() 等 UTC 方法。这是严重 bug,所有部署在国内服务器的用户都受影响。
  • 🐛 月份列表跨时区少 1 个月server/src/routes/settings.js:624 new Date(year, month, 1) 是本地午夜,.toISOString() 转 UTC 时可能跨月(尤其在月底)。改成 Date.UTC() 构造。

v2.42026-06-19

新增 + 修复:

  • AI 测试连接智能选图POST /api/ai/test 不再用源码里内嵌的 1×1 PNG(MiniMax 内容审查会判敏感),改成动态从 uploads/ai/ 里挑最新的真实图片(>500B);没有就提示用户先上传。避免误报 422。
  • OCR 端到端 e2e 已跑通:上传加油小票 → MiniMax M3 多模态识别 → JSON 填表 → 写入数据库。

v2.32026-06-19

  • 登录失败锁定提示:输错密码现在会显示「已错 N 次 / 还剩 N 次 / 锁定 X 分」;5 次错后锁定 30 分钟,会显示「锁定至时间, 还剩 N 分 N 秒」。后端 BAD_CREDENTIALS / LOCKED 都返详细字段。
  • 车辆健康仪表盘/api/vehicles/:id/health):油耗、电耗、保养预测、洗车新鲜度、6 月趋势图(chart.js 堆叠柱)。
  • 洗车前后对比照/api/washes/:id/photos):multer 上传 + 4 路由 + 3 tab UIgallery / compare / upload)。
  • 月度报表/api/reports/monthly):ExcelJS 6 sheet + PDFKit 2.3KB,覆盖车辆 / 洗车 / 加油 / 充电 / 保养 / 保险。
  • Migrations 0015_wash_photos:新增 wash_photos 表 + before/after 字段。

v2.22026-06-19

  • MiniMax M3 多模态接入Settings → AI 截图识别加 provider 下拉(openai_compat / minimax_vl)。MiniMax M3 走 OpenAI 兼容协议 /chat/completions,域名 api.minimaxi.comOCR 任务关 thinking: {type:'disabled'} 防 JSON 污染。
  • 5 类 OCR schema:洗车 / 加油 / 充电 / 保养 / 保险,从截图提取字段直接填表。

v2.12026-06-17

  • 15 个 MySQL migrations + 完整幂等 carlog-init.sql(存储过程 + try-catch 包装 DDL / INDEX / ALTER)。
  • 化学品 / 加油 / 充电 / 保养 / 保险 5 大模块 CRUD。
  • 宝塔面板部署文档。

🧰 常用命令

# 数据库迁移
cd server && node src/bin/migrate.js

# 清空所有数据(不可恢复!要 confirm_token
node src/bin/reset-all.js --confirm

# 灌种子数据
node src/bin/reset-all.js --seed

# 导出全部为 JSON
node src/bin/export.js --format json --output ./backup.json

# 导出单表为 CSV
node src/bin/export.js --format csv --table wash_records --output ./washes.csv

# 备份(SQLite 拷贝 / MySQL mysqldump
node src/bin/backup.js

# Grocy 拉取(CLI
node src/bin/grocy-refresh-products.js

# 验证账号可登录
node src/bin/verify.js

🔐 安全注意事项

  • 生产环境必改 SESSION_SECRET
  • 如启用 HTTPS,在 app.locals.config.session.cookie_securetrue
  • Grocy 密码用 session cookie 缓存 24h;也可用 API Key(推荐,永久不过期)
  • 登录失败有 IP/账号双维度限流(默认 5 次/IP,10 次/账号)
  • 软删除的记录保留在 DB,可通过 /api/operation-logs 恢复

🌐 Grocy 配置

支持两种鉴权方式:

方式 设置 说明
Session Cookie GROCY_URL + GROCY_USERNAME + GROCY_PASSWORD POST /login 拿 cookie,缓存 24h
API Key GROCY_URL + GROCY_API_KEY GROCY-API-KEY header,永久

如果都不配:化学品列表能看但不能同步和扣减,其它模块全部正常工作。

📝 字段命名约定

  • 车辆:typecar/suv/mpv/truck/other)、powertrainice/hev/bev)、platecolor
  • 洗车:wash_typequick/full/detail/other)、wash_datecostlocationduration_min
  • 加油:refuel_datelitersprice_per_litertotal_costfuel_typeis_fullstation
  • 充电:charge_datekwhprice_per_kwhtotal_costcharge_typestart_socend_soc
  • 保养:maint_dateodometer_kmtotal_costshopitems[](动态项目)
  • 保险:insurance_typecompulsory/commercial)、companypolicy_nostart_dateend_datepremium

📱 移动端 & PWA

  • 响应式布局4 档断点(480 / 768 / 1024 / 1440),手机 / iPad / 桌面三端自适应
  • 导航:手机端汉堡按钮 + 右滑抽屉导航(核心 / 能耗 / 其他三分组)
  • 列表页:桌面端表格 → 手机端自动切换卡片堆叠(<MobileCardList> 通用组件)
  • 表单 / 弹窗:移动端单列布局 + 底部弹出 Sheet + sticky 操作栏
  • iOS safe-area:全面屏适配(env(safe-area-inset-*)
  • PWA
    • 安装 vite-plugin-pwaworkbox 自动生成 Service Worker
    • manifest.webmanifest + 192/512/maskable/apple-touch 全套图标
    • 离线访问已缓存页面,导航降级到 /offline
    • 启动屏(apple-touch-startup-image 由浏览器自动截屏处理)
    • 3 个快捷方式:新建洗车 / 加油 / 保养
    • 4 种运行时 toastPwaToasts.vue):新版本可用 / 离线就绪 / Android&桌面安装引导 / iOS Safari 添加到主屏幕

移动端使用方式

  1. iOS Safari:底部分享按钮 ⬆ → 添加到主屏幕(首次进入会有 toast 提示,session 内只弹一次)
  2. Android Chrome:底栏会出现"安装 CarLog"toast,点"安装"即可
  3. 桌面 Chrome:地址栏右侧 安装 CarLog
  4. 新版本发布后下拉刷新(或访问站点)会触发"新版本可用"toast,点"刷新"即可更新

Lighthouse 评分(最新一次运行,桌面端)

指标 分数
Performance 99
Accessibility 87
Best Practices 95
SEO 91

报告 HTML 在 .lighthouseci/lhr-*.html,重新跑分:npm run lighthouse

PWA 检查

npm run lighthouse:pwa 自动验证 11 项 PWA installabilitymanifest / icons 192+512+maskable+apple-touch / SW 注册 / theme-color / meta 标签 / 离线 fallback)。Lighthouse 12 已弃用 PWA 类别,本项目用 puppeteer 自检代替,结果稳定可重复。

🔌 API 速查

完整路径以 /api 为前缀,所有写操作需 requireAuth 中间件。返回 { ok, data, error } 三段式 JSON。

模块 方法 路径 说明
认证 POST /auth/login 登录,返回 csrfToken + Set-Cookie
认证 POST /auth/logout 登出
认证 GET /auth/me 当前用户
认证 POST /auth/change-password 改密码(CSRF
车辆 GET /vehicles 列表(支持 ?q=&page=&limit=
车辆 POST /vehicles 新建(CSRF
车辆 PUT /vehicles/:id 更新(CSRF
车辆 DELETE /vehicles/:id 软删(CSRF
车辆 GET /vehicles/:id/stats 单车统计(总里程/能耗/花费)
洗车 GET/POST/PUT/DELETE /washes[/:id] 同上
洗车 POST /washes/:id/photos 上传对比照(multipart
加油 GET/POST/PUT/DELETE /refuels[/:id] 油耗自动计算
充电 GET/POST/PUT/DELETE /chargings[/:id] 电耗自动计算
保养 GET/POST/PUT/DELETE /maintenances[/:id] 动态 items[]
保险 GET/POST/PUT/DELETE /insurances[/:id] 附件上传
保险 GET /insurances/expiring?days=30 即将到期
车品 GET /chemicals 库存列表
车品 GET/POST/PUT/DELETE /chemicals/:id 详情 / 编辑
车品 GET /chemicals/categories Grocy 分类
车品 POST /chemicals/sync 拉取 Grocy 库存
车品 GET /chemicals/low-stock 低库存预警
车品 POST /chemicals/batch-purchase 批量采购(事务)
日志 GET /operation-logs?page=&action=&user= 操作日志
日志 POST /operation-logs/:id/recover 一键恢复软删数据
AI POST /ai/recognize 图片识别(multipartfield=file
统计 GET /stats/summary 总览 KPI
统计 GET /stats/cost-by-type?from=&to= 按类型花费
统计 GET /stats/odometer 里程折线图
统计 GET /stats/efficiency?vehicle_id= 能耗
导出 GET /export/insurance.csv CSV 导出
导出 GET /export/insurance.pdf PDF 导出
同步 GET /sync/snapshot 全量 JSON 快照
同步 POST /sync/restore 还原(确认 token
系统 GET /health 健康检查

💡 建议增加的功能(按 ROI 排序)

🟢 高 ROIdetailer 日常痛点,必加)

  1. 🚗 车辆里程表(odometer)录入 + 自动同步 现在加油 / 充电 / 保养 / 洗车都只能手动填里程或留空。加一个 vehicles.current_km 字段,每次加油时自动建议上次里程 + 这次的差值(典型 400-600km/周)。直接解决油耗计算准确度问题(现在是基于「相邻 is_full=1」算,没数据就 fallback 0)。

  2. 加油提醒 / 保养提醒推送 现在保险有到期提醒(30 天),但加油 / 保养只能靠用户自己记得。加一个「提醒中心」页:

    • 距上次加油 > 30 天
    • 距上次保养 > 6 个月 / 5000km
    • 距上次洗车 > 14 天(你喜欢保持的车身干净程度) 可以「标记已处理」或 snooze。每天打开应用就能看到该干啥
  3. 💰 成本分类分析 按月 / 按年 / 按车显示:洗车 : 加油 : 充电 : 保养 : 保险 的成本占比饼图。能看出钱花在哪,决定要不要砍掉某些开销(比如发现保险比加油贵)。

  4. 🔍 全局搜索(v2.0+ 已经在做但没暴露 UI) /api/chemicals/grocy-search 只搜化学品。做一个顶栏 search box,能跨所有领域搜(车牌 / 商家名 / 保养项目 / 保单号),点击跳详情。找历史记录速度 × 10

🟡 中等 ROI(增强体验,做不做都行)

  1. 📸 拍照快捷录入(手机 PWA 现在 OCR 要先点「AI 识别」按钮 → 选图 → 识别。手机原生 PWA 加「桌面快捷方式」直接打开「拍照录入」页面,扫一眼小票自动填表。洗车店门口就能记

  2. 🏷️ 标签系统 给洗车 / 加油记录打标签:#自驾游 #通勤 #雨季 #精洗 #打蜡。一年后查「#打蜡 多少次」能判断打蜡频率。纯前端 + 简单 SQL 即可

  3. 📊 同比 / 环比对比 Stats 页加:「本月 vs 上月」「今年 vs 去年同月」自动算增减百分比。一眼看出趋势变化

  4. 🔔 系统通知中心 应用内通知(不是 push,是站内消息):OCR 完成 / 同步失败 / 备份成功 / 新版本可用。比 toast 更持久,用户能回看。

🟢 长期 / 玩法(看你个人兴趣)

  1. 🏆 成就系统(纯前端)

    • 「连续 30 天洗车」「1 年累计 100 次洗车」「单次最贵的精洗」
    • 解锁后给个 badge 分享到社交媒体(生成图片卡片)
    • detailer 圈子的「show off」属性
  2. 🛣️ 路线 / 行程记录trips 表:起点 / 终点 / 里程 / 油耗 / 备注。

    • 加油时关联 trip_id,自动算 trip 油耗
    • 月底看「本月去过哪些地方」
    • 自驾游爱好者会很喜欢
  3. 📦 备份到 OSS / 七牛 / 阿里云盘(自动化) 现在 bin/backup.js 只能本地。加 cron + 上传到对象存储:

    • bin/backup-upload.js 调 S3 兼容 API
    • 宝塔 cron 0 3 * * * 每天凌晨跑
    • 保留 30 天滚动 你的数据安全网,必须做
  4. 🔍 OCR 文本预览 + 高亮 现在 OCR 完直接填表,看不到识别到的原始文本。加一个「识别原始」tab:

    • 显示 AI 返的 raw 文本
    • 高亮置信度低的字段(让用户重点核对这些)
    • 提升用户对 OCR 结果的信任
  5. 🌐 i18n 多语言 现在 hardcode 中文。如果要跟车友分享 / 出国玩可能需要 EN。vue-i18n 一加就够。

🔴 别做(性价比低)

  • 多用户 / 多租户:你一个人玩,加这个复杂度 × 10,价值 = 0
  • TS 重构:除非你强迫症,JSDoc 已经覆盖了
  • 复杂 BI / 自定义仪表盘:你 8 辆车 + 5 年数据,PostgreSQL + Superset 都嫌重
  • 区块链溯源 / NFT 车辆档案:开玩笑的 😂

想问你几个开放问题

  1. 里程表录入:你加油时会主动记里程吗?(很多人靠加油站小票 + 全程导航算)
  2. 保养预测:你 4S 店 / 修理厂会提前打电话提醒,还是你 app 自己管?
  3. 跟车友分享:你想不想把数据导出发给车友 / 二手车买家看?
  4. 车机 / OBD 集成:你车有 OBD 接口吗?要不要读实时数据?

回答这几个我能给你更精准的优先级建议。


📋 License

MIT