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

871 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 洗车管理系统
个人 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`
```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_typewash/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 是 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.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.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 失败兜底 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` 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 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.62026-06-19
### v2.52026-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.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.com`OCR 任务关 `thinking: {type:'disabled'}` 防 JSON 污染。
- **5 类 OCR schema**:洗车 / 加油 / 充电 / 保养 / 保险,从截图提取字段直接填表。
### v2.12026-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 / 桌面三端自适应
- **导航**:手机端汉堡按钮 + 右滑抽屉导航(核心 / 能耗 / 其他三分组)
- **列表页**:桌面端表格 → 手机端自动切换卡片堆叠(`<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 添加到主屏幕
### 移动端使用方式
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(增强体验,做不做都行)
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