- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 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
洗车管理系统
个人 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 schema(wash/refuel/charge/maint/insurance)
│ ├── 多 provider:OpenAI 兼容 / 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
│ ├── manifest(id / icons 192/512/maskable/apple-touch / shortcuts)
│ ├── Service Worker(vite-plugin-pwa autoUpdate)
│ ├── 离线缓存:API static(30 天 CacheFirst)+ uploads + images(SWR)+ fonts
│ ├── navigateFallback → /index.html(白名单 /api/、/uploads/)
│ ├── iOS / Android / Desktop 安装提示(beforeinstallprompt 拦截 + 引导)
│ ├── 新版本可用提示(needRefresh toast)
│ └── 离线就绪提示
│
├── 🔧 工具与脚本
│ ├── 备份:bin/backup.js(SQLite 拷贝 / MySQL mysqldump)
│ ├── 导出:bin/export.js(JSON / CSV,单表 / 全量)
│ ├── 灌种子:bin/seed-demo.js
│ ├── 重置:bin/reset-all.js(强 confirm_token)
│ ├── Grocy 拉取:bin/grocy-refresh-products.js
│ ├── 天气刷新:bin/weather.js
│ ├── 账号管理:bin/users.js(disable / enable / reset pwd)
│ ├── 验证:bin/verify.js
│ └── 迁移:bin/migrate.js(15 个 SQL 幂等执行)
│
├── 🩺 运维
│ ├── 健康检查:`/api/health`(兼容)+ `/api/health/live`(进程活)+ `/api/health/ready`(DB 通)
│ ├── 调试面板:DebugPanel(API 调用日志 + Vue error + unhandledrejection)
│ └── OpenAPI 文档:`/api/docs`(Swagger UI)+ `/api/openapi.json`
│
└── 🌐 部署
├── Express HTTP 服务(port 8787)
├── 静态资源托管(uploads/)
├── SPA fallback(client/dist/)
├── 宝塔面板部署文档(PM2 + Nginx + SSL)
└── Docker-ready(carlog-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-源码.zip(492 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.sql(28 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,跟着引导:
- 创建管理员账号(用户名 + 密码)
- 选是否启用 Grocy(也可以先跳过,回设置里填)
- 完成 → 登录
8. 申请 SSL(可选但强烈推荐)
宝塔站点 → SSL → Let's Encrypt → 选域名 → 申请 → 强制 HTTPS 打开。
申请完 SESSION_SECRET 不动,但 .env 里的 session.cookie_secure 改成 true(在 Web 设置页里改也行),重启 PM2。
9. 备份策略
宝塔 计划任务 加两条:
- 每日 03:00:
node /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_type 非 service_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 |
✅(字段 company 非 insurer) |
| 保险附件 | 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 function:ensureCookie 未从 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 clause:sync 路由 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/prefsGET/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_type(wash/refuel/charge/maintenance/insurance),可按 tag_id 找所有打了该标签的记录 - 🔔 站内通知中心:
notifications表 + 推送工具函数(pushNotification),OCR 完成 / 同步成功 / 备份完成等可持久化通知,GET/POST/标已读/全部标已读 - 🏆 成就系统:14 个预置成就(洗车新手→狂魔 / 一周一洗 / 万里征程 / 十万俱乐部 / 保险达人等),自动算 progress、解锁时持久化到
user_achievements
Bug 修复(Trae 引入的):
- 🐛 MySQL pool 连接超时(生产 P0):
server/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 同款 bug(getMonth()等本地方法),但 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.lastInsertRowid,mysql2 是 BigInt,JSON 序列化会变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.js(7 个用例:reminders 包装 / 加油提醒 / cost-breakdown 5 分类 + 百分比 / compare 月环比同比) - 新增
routes.tags.test.js(8 个用例:CRUD / toggle 双向 / 非法 record_type 400 / 重名 409 / 级联删除) - 新增
routes.notifications.test.js(6 个用例:列表 + unread 计数 / 创建 / 标已读 / 全部已读) - 总测试数 76 → 97 全过
v2.7(2026-06-20)
新功能:
- 401 自动跳登录 + 表单草稿:
client/src/api/client.jsaxios interceptor catch 401 → 触发form-draft:flush-all事件把所有 useFormDraft 暂存到 sessionStorage → 跳/login?reason=expired&redirect=原页;登录成功后回原页,草稿自动恢复。 - AI OCR 失败兜底 modal:
client/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 次(用_csrfRetriedflag 防双发)。 - 健康检查拆分:
/api/health(兼容旧)+/api/health/live(进程活着)+/api/health/ready(DBSELECT 1通过),k8s/宝塔监控能区分"我在启动"和"我坏了"。 - 3 个真正有用的图表数据:
GET /api/stats/extra返fuelTrend/costPerVehicle/washSeason;Stats.vue 加油价趋势 + 每辆车年均养护 + 洗车季节频率 + 各车成本明细表。 - OpenAPI 文档:
/api/docsSwagger UI +/api/openapi.jsonschema,29 条核心路由有 JSDoc 注释。
Bug 修复(Trae 引入的):
- 🐛 WashNew.vue modal 永远显示:line 14
:show="ai.showFallback.value"模板里用 ref 的.value→ 传的是 Ref 对象本身(永远 truthy),改成:show="ai.showFallback"让 Vue 模板自动解包。其他 view(ChargingList/InsuranceList/RefuelList/WashShow/MaintenanceList)都正确用const aiBusy = ai.busy别名,没踩这个坑。 - 🐛 Swagger 0 paths:swagger.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 limit:Trae 只在/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.6(2026-06-19)
v2.5(2026-06-19)
Bug 修复:
- 🐛 登录锁定时区错 8 小时:
server/src/services/rateLimit.js的nowIso()和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:624new Date(year, month, 1)是本地午夜,.toISOString()转 UTC 时可能跨月(尤其在月底)。改成Date.UTC()构造。
v2.4(2026-06-19)
新增 + 修复:
- AI 测试连接智能选图:
POST /api/ai/test不再用源码里内嵌的 1×1 PNG(MiniMax 内容审查会判敏感),改成动态从uploads/ai/里挑最新的真实图片(>500B);没有就提示用户先上传。避免误报 422。 - OCR 端到端 e2e 已跑通:上传加油小票 → MiniMax M3 多模态识别 → JSON 填表 → 写入数据库。
v2.3(2026-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 UI(gallery / compare / upload)。 - 月度报表(
/api/reports/monthly):ExcelJS 6 sheet + PDFKit 2.3KB,覆盖车辆 / 洗车 / 加油 / 充电 / 保养 / 保险。 - Migrations 0015_wash_photos:新增
wash_photos表 + before/after 字段。
v2.2(2026-06-19)
- MiniMax M3 多模态接入:Settings → AI 截图识别加 provider 下拉(
openai_compat/minimax_vl)。MiniMax M3 走 OpenAI 兼容协议/chat/completions,域名api.minimaxi.com;OCR 任务关thinking: {type:'disabled'}防 JSON 污染。 - 5 类 OCR schema:洗车 / 加油 / 充电 / 保养 / 保险,从截图提取字段直接填表。
v2.1(2026-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_secure设true - 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,永久 |
如果都不配:化学品列表能看但不能同步和扣减,其它模块全部正常工作。
📝 字段命名约定
- 车辆:
type(car/suv/mpv/truck/other)、powertrain(ice/hev/bev)、plate、color - 洗车:
wash_type(quick/full/detail/other)、wash_date、cost、location、duration_min - 加油:
refuel_date、liters、price_per_liter、total_cost、fuel_type、is_full、station - 充电:
charge_date、kwh、price_per_kwh、total_cost、charge_type、start_soc、end_soc - 保养:
maint_date、odometer_km、total_cost、shop、items[](动态项目) - 保险:
insurance_type(compulsory/commercial)、company、policy_no、start_date、end_date、premium
📱 移动端 & PWA
- 响应式布局:4 档断点(
480 / 768 / 1024 / 1440),手机 / iPad / 桌面三端自适应 - 导航:手机端汉堡按钮 + 右滑抽屉导航(核心 / 能耗 / 其他三分组)
- 列表页:桌面端表格 → 手机端自动切换卡片堆叠(
<MobileCardList>通用组件) - 表单 / 弹窗:移动端单列布局 + 底部弹出 Sheet + sticky 操作栏
- iOS safe-area:全面屏适配(
env(safe-area-inset-*)) - PWA:
- 安装
vite-plugin-pwa,workbox 自动生成 Service Worker manifest.webmanifest+ 192/512/maskable/apple-touch 全套图标- 离线访问已缓存页面,导航降级到
/offline - 启动屏(
apple-touch-startup-image由浏览器自动截屏处理) - 3 个快捷方式:新建洗车 / 加油 / 保养
- 4 种运行时 toast(
PwaToasts.vue):新版本可用 / 离线就绪 / Android&桌面安装引导 / iOS Safari 添加到主屏幕
- 安装
移动端使用方式
- iOS Safari:底部分享按钮 ⬆ → 添加到主屏幕(首次进入会有 toast 提示,session 内只弹一次)
- Android Chrome:底栏会出现"安装 CarLog"toast,点"安装"即可
- 桌面 Chrome:地址栏右侧 ➕ 安装 CarLog
- 新版本发布后下拉刷新(或访问站点)会触发"新版本可用"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 installability(manifest / 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 |
图片识别(multipart:field=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 排序)
🟢 高 ROI(detailer 日常痛点,必加)
-
🚗 车辆里程表(odometer)录入 + 自动同步 现在加油 / 充电 / 保养 / 洗车都只能手动填里程或留空。加一个
vehicles.current_km字段,每次加油时自动建议上次里程 + 这次的差值(典型 400-600km/周)。直接解决油耗计算准确度问题(现在是基于「相邻 is_full=1」算,没数据就 fallback 0)。 -
⛽ 加油提醒 / 保养提醒推送 现在保险有到期提醒(30 天),但加油 / 保养只能靠用户自己记得。加一个「提醒中心」页:
- 距上次加油 > 30 天
- 距上次保养 > 6 个月 / 5000km
- 距上次洗车 > 14 天(你喜欢保持的车身干净程度) 可以「标记已处理」或 snooze。每天打开应用就能看到该干啥。
-
💰 成本分类分析 按月 / 按年 / 按车显示:洗车 : 加油 : 充电 : 保养 : 保险 的成本占比饼图。能看出钱花在哪,决定要不要砍掉某些开销(比如发现保险比加油贵)。
-
🔍 全局搜索(v2.0+ 已经在做但没暴露 UI)
/api/chemicals/grocy-search只搜化学品。做一个顶栏 search box,能跨所有领域搜(车牌 / 商家名 / 保养项目 / 保单号),点击跳详情。找历史记录速度 × 10。
🟡 中等 ROI(增强体验,做不做都行)
-
📸 拍照快捷录入(手机 PWA) 现在 OCR 要先点「AI 识别」按钮 → 选图 → 识别。手机原生 PWA 加「桌面快捷方式」直接打开「拍照录入」页面,扫一眼小票自动填表。洗车店门口就能记。
-
🏷️ 标签系统 给洗车 / 加油记录打标签:#自驾游 #通勤 #雨季 #精洗 #打蜡。一年后查「#打蜡 多少次」能判断打蜡频率。纯前端 + 简单 SQL 即可。
-
📊 同比 / 环比对比 Stats 页加:「本月 vs 上月」「今年 vs 去年同月」自动算增减百分比。一眼看出趋势变化。
-
🔔 系统通知中心 应用内通知(不是 push,是站内消息):OCR 完成 / 同步失败 / 备份成功 / 新版本可用。比 toast 更持久,用户能回看。
🟢 长期 / 玩法(看你个人兴趣)
-
🏆 成就系统(纯前端)
- 「连续 30 天洗车」「1 年累计 100 次洗车」「单次最贵的精洗」
- 解锁后给个 badge 分享到社交媒体(生成图片卡片)
- detailer 圈子的「show off」属性
-
🛣️ 路线 / 行程记录 加
trips表:起点 / 终点 / 里程 / 油耗 / 备注。- 加油时关联 trip_id,自动算 trip 油耗
- 月底看「本月去过哪些地方」
- 自驾游爱好者会很喜欢
-
📦 备份到 OSS / 七牛 / 阿里云盘(自动化) 现在
bin/backup.js只能本地。加 cron + 上传到对象存储:bin/backup-upload.js调 S3 兼容 API- 宝塔 cron
0 3 * * *每天凌晨跑 - 保留 30 天滚动 你的数据安全网,必须做
-
🔍 OCR 文本预览 + 高亮 现在 OCR 完直接填表,看不到识别到的原始文本。加一个「识别原始」tab:
- 显示 AI 返的 raw 文本
- 高亮置信度低的字段(让用户重点核对这些)
- 提升用户对 OCR 结果的信任
-
🌐 i18n 多语言 现在 hardcode 中文。如果要跟车友分享 / 出国玩可能需要 EN。vue-i18n 一加就够。
🔴 别做(性价比低)
- 多用户 / 多租户:你一个人玩,加这个复杂度 × 10,价值 = 0
- TS 重构:除非你强迫症,JSDoc 已经覆盖了
- 复杂 BI / 自定义仪表盘:你 8 辆车 + 5 年数据,PostgreSQL + Superset 都嫌重
- 区块链溯源 / NFT 车辆档案:开玩笑的 😂
❓ 想问你几个开放问题
- 里程表录入:你加油时会主动记里程吗?(很多人靠加油站小票 + 全程导航算)
- 保养预测:你 4S 店 / 修理厂会提前打电话提醒,还是你 app 自己管?
- 跟车友分享:你想不想把数据导出发给车友 / 二手车买家看?
- 车机 / OBD 集成:你车有 OBD 接口吗?要不要读实时数据?
回答这几个我能给你更精准的优先级建议。
📋 License
MIT