# 洗车管理系统 个人 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 洗车管理系统 cd 洗车管理系统 # 后端依赖 cd server && npm install # 前端依赖 cd ../client && npm install ``` ### 3. 配置 `.env` 在项目根目录 `洗车管理系统/.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. 初始化数据库 ```bash cd server node src/bin/migrate.js # 输出:✓ 0001_init.sql ... ✓ 0014_grocy_auth.sql ``` ### 5. 构建前端 ```bash cd ../client npm run build # 产物在 client/dist/ ``` ### 6. 启动服务 ```bash cd ../server node src/bin/serve.js # [server] http://0.0.0.0:8787 # [db] MySQL connected (carlog) ``` 首次访问 `http://localhost:8787/` 会跳到 `/setup` 引导你创建管理员账号。 ### 7. (可选)灌种子数据 ```bash cd server node src/bin/reset-all.js --seed # 清空 + 写 2 辆车 + ~50 条记录 node src/bin/seed-demo.js # 单独跑种子(不清空) ``` ### 8. 生产部署(PM2) ```bash npm install -g pm2 cd server pm2 start src/bin/serve.js --name carwash pm2 save pm2 startup ``` Nginx 反向代理示例: ```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` → 提交 - 或命令行: ```bash mysql -ucarlog -p carlog < carlog-init.sql ``` > 这个 SQL 已经做了**完全幂等**(存储过程 + try-catch),已存在的表/索引/列会自动跳过,**反复重跑不会破坏数据**。也无需再跑 `migrate.js`(migration 表会标记为已应用)。 **方式 B:用源码迁移** 如果偏好用源码版本(库已有数据想增量迁移): ```bash cd /www/wwwroot/carwash/server node src/bin/migrate.js ``` 在项目根目录(`/www/wwwroot/carwash/.env`)新建文件: ```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. 装依赖 宝塔 → 终端(确保当前在项目根目录): ```bash 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 /` 段替换成: ```nginx 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: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,前端会按本地显示 | ### 升级步骤 ```bash 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/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_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.js` axios 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 次(用 `_csrfRetried` flag 防双发)。 - **健康检查拆分**:`/api/health`(兼容旧)+ `/api/health/live`(进程活着)+ `/api/health/ready`(DB `SELECT 1` 通过),k8s/宝塔监控能区分"我在启动"和"我坏了"。 - **3 个真正有用的图表数据**:`GET /api/stats/extra` 返 `fuelTrend` / `costPerVehicle` / `washSeason`;Stats.vue 加油价趋势 + 每辆车年均养护 + 洗车季节频率 + 各车成本明细表。 - **OpenAPI 文档**:`/api/docs` Swagger UI + `/api/openapi.json` schema,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:624` `new 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。 - 宝塔面板部署文档。 ## 🧰 常用命令 ```bash # 数据库迁移 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 / 桌面三端自适应 - **导航**:手机端汉堡按钮 + 右滑抽屉导航(核心 / 能耗 / 其他三分组) - **列表页**:桌面端表格 → 手机端自动切换卡片堆叠(`` 通用组件) - **表单 / 弹窗**:移动端单列布局 + 底部弹出 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 添加到主屏幕 ### 移动端使用方式 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 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 日常痛点,必加) 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(增强体验,做不做都行) 5. **📸 拍照快捷录入(手机 PWA)** 现在 OCR 要先点「AI 识别」按钮 → 选图 → 识别。手机原生 PWA 加「桌面快捷方式」直接打开「拍照录入」页面,扫一眼小票自动填表。**洗车店门口就能记**。 6. **🏷️ 标签系统** 给洗车 / 加油记录打标签:#自驾游 #通勤 #雨季 #精洗 #打蜡。一年后查「#打蜡 多少次」能判断打蜡频率。**纯前端 + 简单 SQL 即可**。 7. **📊 同比 / 环比对比** Stats 页加:「本月 vs 上月」「今年 vs 去年同月」自动算增减百分比。**一眼看出趋势变化**。 8. **🔔 系统通知中心** 应用内通知(不是 push,是站内消息):OCR 完成 / 同步失败 / 备份成功 / 新版本可用。比 toast 更持久,用户能回看。 ### 🟢 长期 / 玩法(看你个人兴趣) 9. **🏆 成就系统**(纯前端) - 「连续 30 天洗车」「1 年累计 100 次洗车」「单次最贵的精洗」 - 解锁后给个 badge 分享到社交媒体(生成图片卡片) - **detailer 圈子的「show off」属性** 10. **🛣️ 路线 / 行程记录** 加 `trips` 表:起点 / 终点 / 里程 / 油耗 / 备注。 - 加油时关联 trip_id,自动算 trip 油耗 - 月底看「本月去过哪些地方」 - **自驾游爱好者会很喜欢** 11. **📦 备份到 OSS / 七牛 / 阿里云盘(自动化)** 现在 `bin/backup.js` 只能本地。加 cron + 上传到对象存储: - `bin/backup-upload.js` 调 S3 兼容 API - 宝塔 cron `0 3 * * *` 每天凌晨跑 - 保留 30 天滚动 **你的数据安全网,必须做** 12. **🔍 OCR 文本预览 + 高亮** 现在 OCR 完直接填表,看不到识别到的原始文本。加一个「识别原始」tab: - 显示 AI 返的 raw 文本 - 高亮置信度低的字段(让用户重点核对这些) - **提升用户对 OCR 结果的信任** 13. **🌐 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