<p align="center">
  <img src="./assets/readme/hero.svg" width="100%" alt="充电桩实时监控：公司内部 EV 充电桩状态监控系统，看一眼楼下还有没有空位">
</p>

# 充电桩实时监控

> 公司内部 EV 充电桩状态实时监控 Web 应用。解决员工下楼找充电桩经常扑空的痛点——先看一眼，再决定要不要下楼。

## ✨ 功能特性

| 能力                     | 说明                                                           |
| ------------------------ | -------------------------------------------------------------- |
| ⚡**实时监控**     | 每 60 秒静默轮询，状态无感刷新                                 |
| 📊**三态状态机**   | 空闲（带时长）/ 充电中（带进度）/ 占用（充完未拔），语义色区分 |
| 🔌**AC / DC 分区** | 交流 7kW 慢充 ×15 + 直流 20kW 快充 ×2，分区域展示            |
| 🚗**DC 充电动效**  | SVG 汽车剪影 + 3D 电池 + 能量光束入场动画                      |
| 🔔**邮件订阅**     | 有空位第一时间邮件通知，触发一次后自动失效                     |
| 📱**响应式设计**   | 移动端 2 列 / PC 端 4 列，深色 Apple 风格 UI                   |
| 💬**职场语录**     | 抢桩焦虑时来一句职场自黑调侃                                   |
| 🧪**Mock 模式**    | 无需后端即可本地体验完整界面                                   |

## 🖥 界面预览
![1785740999191](image/README/1785740999191.png)

## 🚀 快速开始

### 1. 环境要求

- Python 3.9+
- pip
- （可选）MySQL 8.x —— 不配置则回退到 JSON 文件模拟存储

### 2. 安装依赖

```bash
pip install -r backend/requirements.txt
```

### 3. 获取 NIO Power Token（必需）

用统一管理器一键登录，token 自动持久化到数据库，无需手动填表：

```bash
cd backend
python token_manager.py send-code 13800138000      # 1. 发送验证码到手机
python token_manager.py login 13800138000 123456   # 2. 填入 6 位验证码，登录并持久化
python token_manager.py status                     # 3. 确认 token 来源为 database
```

### 4. 配置环境变量

```bash
cp backend/.env.example backend/.env
# 编辑 backend/.env，填写数据库 / 邮件 / 服务配置
# token 相关变量（NIO_AUTHORIZATION 等）可留空：CLI 登录后 token 已存数据库
```

### 5. 启动服务

### 5. 启动服务

**方式一：一键启动（推荐）**

```bash
./start.sh
# 前端 http://localhost:8080  后端 http://localhost:5001
```

**方式二：手动启动**

```bash
cd backend && python server.py            # 后端
cd frontend && python -m http.server 8080 # 另开终端启动前端
```

**方式三：Mock 模式（无需后端与 token）**

```bash
open "http://localhost:8080?mock=true"
```

---

## 🔑 获取 NIO Power Token

数据来自蔚来 NIO Power 官方接口（`app.niopower.com`），请求需要携带 `Authorization: Bearer <access_token>`。本项目用 `backend/token_manager.py` **统一管理 token 全生命周期**：登录、持久化、自动刷新，全部命令行完成，无需改代码。

### Token 来源与优先级

`TokenManager` 按以下优先级取 token：

| 优先级 | 来源                   | 说明                                                                    |
| ------ | ---------------------- | ----------------------------------------------------------------------- |
| 1      | **显式参数**     | 构造时传入`authorization=`（主要用于测试）                            |
| 2      | **数据库持久化** | `system_config` 表中 CLI 登录 / 自动刷新写入的 token，永远最新        |
| 3      | **环境变量**     | `NIO_AUTHORIZATION` / `NIO_REFRESH_TOKEN`，兼容只配环境变量的旧部署 |

> **推荐做法**：用 CLI 登录一次，token 存进数据库，环境变量全都不用填。scheduler 启动时自动读取数据库里最新的 token。

### 一、手机号登录（推荐）

两条命令，token 自动持久化到数据库，跨进程共享，**无需手动抄写**：

```bash
cd backend
python token_manager.py send-code 13800138000       # 发送验证码到手机
python token_manager.py login 13800138000 123456    # 填入 6 位验证码，登录并持久化
```

登录成功后终端会打印 `NIO_AUTHORIZATION` / `NIO_REFRESH_TOKEN`，仅用于**在新环境部署**时复用；当前机器无需配置任何 token 变量。

### 二、只配环境变量（兼容旧部署）

已有现成 token，不想要数据库时，直接配置 `.env` 即可：

```bash
# backend/.env
NIO_AUTHORIZATION="Bearer <你的 token>"
NIO_REFRESH_TOKEN="<refresh_token>"     # 可选，配置后失效可自动刷新
NIO_DEVICE_ID="<与 token 配套的设备 ID>"  # 必须与获取 token 时一致
NIO_API_SECRET_PRODUCTION="<签名密钥>"    # 必需，用于接口请求签名
```

> **注意**：`device_id` 与 token 绑定，不配套接口会报 `invalid deviceId`。若不确定，登录一次让系统持久化一套一致的 token + device_id。

### Token 过期怎么办——自动刷新

access_token 有时效。**不用手动管**：业务请求返回 `auth_failed` / `invalid_token` 等错误码时，`TokenManager` 会自动用 `refresh_token` 刷新并重试一次。

手动刷新（或排查问题）：

```bash
python token_manager.py status       # 查看 token 来源 / 登录手机号 / 是否已配 refresh
python token_manager.py refresh      # 手动用 refresh_token 换新 token
```

> 若 `refresh_token` 也失效（长期未用），刷新会失败，重新执行 `login` 即可。

### 安全提醒

- **不要把真实 token / 签名密钥提交到 git**。`backend/.env` 已在 `.gitignore` 排除，只提交 `.env.example` 占位模板。
- Token 是个人账号凭证，泄露后可被调用 NIO Power 接口，发现泄露请及时轮换。
- 请遵守 NIO Power 服务条款，仅在授权范围内使用官方接口。生产环境务必设置 `CORS_ORIGINS`（未设置时后端会拒绝启动）。

---

## 🏗 系统架构

<p align="center">
  <img src="./assets/readme/architecture.svg" width="100%" alt="系统架构图：NIO Power API 经 scheduler 定时采集写入 MySQL，Flask 提供 REST 接口，前端 60 秒轮询，订阅提醒走 SMTP 邮件">
</p>

**数据流**：`NIO Power API → scheduler.py（每 60s 采集）→ MySQL / JSON → server.py（Flask REST）→ 前端轮询`

后端在 `scheduler.py` 中后台定时采集充电桩状态，同时检查订阅条件并发送空闲提醒邮件（工作日 10:00–19:00）；`server.py` 对前端暴露只读接口。

## 📁 项目结构

```
.
├── frontend/                    # 前端（纯 HTML + JS + Tailwind，无需构建）
│   ├── index.html              # 主页面 SPA
│   ├── serve.py                # 本地开发服务器
│   └── src/
│       ├── js/
│       │   ├── main.js         # 入口：初始化、模式判断
│       │   ├── config.js       # 配置（API 地址、轮询间隔）
│       │   ├── api.js          # API 请求、订阅、Toast/Modal
│       │   ├── render.js       # DOM 渲染（卡片、筛选器）
│       │   ├── polling.js      # 智能轮询（工作日/非工作日）
│       │   ├── cache.js        # localStorage 缓存
│       │   ├── mock.js         # 前端 Mock 数据
│       │   ├── quotes.js       # 职场语录库
│       │   └── dc-anim.js      # DC 充电动效
│       └── css/                # 主样式 + DC 动画样式
├── backend/                     # 后端（Python Flask）
│   ├── server.py               # Flask API 服务主入口
│   ├── nio_power_api.py        # NIO Power API 封装（签名算法）
│   ├── nio_power_api_raw.py    # 原始 API 调用（返回未处理 JSON）
│   ├── token_manager.py        # NIO token 统一管理器（登录/刷新/持久化）
│   ├── scheduler.py            # 后台定时采集 + 邮件通知
│   ├── db.py                   # MySQL 数据库层（连接池）
│   ├── mock_db.py              # JSON 文件模拟数据库
│   ├── init.sql                # 数据库建表脚本
│   └── .env.example            # 环境变量模板
└── start.sh                     # 本地一键启动脚本
```

## 🔌 API 接口

| 方法   | 路径                        | 说明           |
| ------ | --------------------------- | -------------- |
| GET    | `/api/station/<group_id>` | 获取充电站数据 |
| POST   | `/api/subscribe`          | 订阅空闲提醒   |
| DELETE | `/api/subscribe`          | 取消订阅       |
| GET    | `/health`                 | 健康检查       |

## 🔔 邮件订阅规则

| 规则项   | 说明                                                 |
| -------- | ---------------------------------------------------- |
| 开放时段 | 工作日周一至周五 10:00 – 19:00                      |
| 触发条件 | 有充电桩空闲超过 1 分钟即通知，触发一次后 5 分钟失效 |
| 合并通知 | 多桩同时空闲时合并为一封邮件                         |
| 取消通知 | 空闲桩被占用后自动发送取消邮件                       |
| 周末     | 订阅入口隐藏/置灰                                    |

## 🧑‍💻 开发说明

### 前端模块化

前端采用 ES Modules 架构，无需构建工具：

- `main.js` — 入口，初始化和全局函数注册
- `api.js` — API 请求、Toast/Modal 控制
- `render.js` — DOM 渲染、卡片、筛选器
- `polling.js` — 智能轮询（工作日/非工作日自适应）
- `dc-anim.js` — DC 充电动效（SVG 汽车 + 3D 电池 + 能量光束）

### Mock 模式

localhost 下默认启用 Mock 模式：

- 前端 Mock：`src/js/mock.js` 生成随机充电桩数据
- 后端 Mock：`mock_db.py` 使用 JSON 文件存储
- 切换真实 API：使用非 localhost 域名，或配置 `window.RUNTIME_CONFIG`

### Token 运维

Token 由 `backend/token_manager.py` 统一管理，日常运维不需要改代码：

```bash
python token_manager.py status       # 查看 token 来源 / 登录手机号 / refresh 是否已配
python token_manager.py refresh      # 手动刷新 access_token
python token_manager.py login <手机号> <验证码>  # 重新登录并持久化
```

scheduler 采集时统一走 `TokenManager.get_chargers()`：遇 `auth_failed` / `invalid_token` 等错误码自动刷新重试一次；token 与 device_id 持久化在数据库 `system_config` 表，跨进程共享。

### 部署

GitHub Actions 自动部署：

1. 推送到 `main` 分支触发部署
2. 通过 SSH 连接服务器执行 `git pull`
3. 自动重启后端服务

## 📄 许可证

[GPL-3.0](LICENSE)
