users
└── ninebot_accounts (一对多:一个用户可绑定多个九号账号)
└── devices (一对多:一个账号对应多台设备)
└── device_snapshots (一对多:每次采集追加一行快照)
| 字段 | 类型 | 说明 |
|---|---|---|
user_id |
FK | 关联本系统用户 |
username |
string | 九号账号用户名 |
password |
text (encrypted) | 九号账号密码(AES 加密存储) |
ninebot_uuid |
string | 九号平台用户 UUID |
phone |
string | 手机号 |
email |
string | 邮箱 |
avatar |
string | 头像 URL |
access_token |
text | 九号 access token |
refresh_token |
text | 九号 refresh token |
access_token_validity |
bigint | access token 过期时间(毫秒时间戳) |
region |
string | 区域(如 bj) |
enabled |
boolean | 账号是否启用 |
| 字段 | 类型 | 说明 |
|---|---|---|
ninebot_account_id |
FK | 关联九号账号 |
sn |
string | 设备序列号(unique per account) |
device_name |
string | 设备名称 |
img |
string | 设备图片 URL |
| 字段 | 类型 | 说明 |
|---|---|---|
device_id |
FK | 关联设备 |
gsm |
smallint | 信号强度(0–31) |
gsm_time |
int | 信号采集时间(Unix 时间戳) |
pwr |
tinyint | 电源开关状态(0/1) |
dump_energy |
tinyint | 剩余电量(%) |
estimate_mileage |
decimal(8,2) | 预计续航里程(km) |
location_desc |
string | 位置文字描述 |
charging_state |
tinyint | 充电状态(0=未充电) |
power_status |
tinyint | 车辆状态(0=锁车) |
remain_charge_time |
string | 剩余充电时间 |
所有接口(除注册/登录外)均需在请求头携带:
Authorization: Bearer <access_token>
POST /api/register
Content-Type: application/json
{
"name": "张三",
"email": "zhang@example.com",
"password": "password123",
"password_confirmation": "password123"
}响应 201:
{
"session": {
"token_type": "bearer",
"access_token": "eyJ...",
"expires_in": 3600,
"user": {
"id": 1,
"name": "张三",
"email": "zhang@example.com"
}
}
}POST /api/login
Content-Type: application/json
{
"email": "zhang@example.com",
"password": "password123"
}GET /api/me
Authorization: Bearer <token>POST /api/refresh
Authorization: Bearer <token>POST /api/logout
Authorization: Bearer <token>调用九号登录接口验证后,将凭证与账号信息存入数据库(密码加密存储)。
POST /api/ninebot-accounts
Authorization: Bearer <token>
Content-Type: application/json
{
"username": "shine09",
"password": "your_ninebot_password"
}响应 201:
{
"ninebot_account": {
"id": 1,
"username": "shine09",
"ninebot_uuid": "1148401935171325952",
"phone": "139****7804",
"avatar": "https://avatar-cn.ninebot.com/...",
"region": "bj",
"enabled": true,
"created_at": "2026-03-13T06:28:53.000000Z",
"updated_at": "2026-03-13T06:28:53.000000Z"
}
}同一账号名重复绑定时,会更新已有记录(upsert)。
GET /api/ninebot-accounts
Authorization: Bearer <token>DELETE /api/ninebot-accounts/{id}
Authorization: Bearer <token>响应 204(No Content)。
调用九号设备列表接口,将返回的设备写入/更新 devices 表,然后返回。
GET /api/ninebot-accounts/{ninebotAccountId}/devices
Authorization: Bearer <token>响应 200:
{
"devices": [
{
"id": 1,
"ninebot_account_id": 1,
"sn": "N9DFDxxxxxxxxx",
"device_name": "新国飙",
"img": "https://oms-oss-public.ninebot.com/..."
}
]
}错误体风格:
{
"errors": [
{
"type": "invalid_request_error",
"code": "ninebot_login_failed",
"message": "Invalid username or password."
}
]
}| HTTP 状态码 | 场景 |
|---|---|
401 |
未携带 Token 或 Token 无效 |
403 |
操作不属于自己的资源 |
404 |
设备不存在或不属于当前用户 |
422 |
参数校验失败,或九号接口返回非成功状态 |
前端设备页中的电量趋势图(frontend/src/components/devices/battery-trend-chart.tsx)在显示折线数值标签时,遵循以下规则:
- 仅在开启“显示数值标签”时渲染标签。
- 按折线数据顺序逐点判断。
- 当前点数值如果与“上一个已显示标签的数值”相同,则不重复显示。
- 当前点数值如果与“上一个已显示标签的数值”不同,则显示一次。
- 非法值或小于等于
0的值不显示标签。
也就是说,标签逻辑不是“只显示最高点和最低点”,而是“相邻重复值去重后,每个变化过的数值显示一次”。
- Tooltip 跟随横轴高亮线显示当前时间点的数据。
- Tooltip 会展示当前时间点、状态说明,以及当前视图对应的百分比值。
- 为避免频繁触发埋点,Tooltip 对外追踪回调做了节流处理。
- Tooltip 使用 portal 渲染,避免在图表最左侧或最右侧因为容器裁切而不可见。
- 当存在实时电量且当前时间不在整点时,图表尾部会补一个“虚拟实时点”。
- 该点用于表达“当前时刻”的实时电量,不代表接口返回的一条整点历史记录。
- 虚拟点会在图上显示
实时标记,并在 Tooltip 中标识为“实时(虚拟)”。
- 横轴表示时间。
- 纵轴表示百分比(
%)。 - 在
soc视图中,纵轴表示当前剩余电量百分比。 - 在
discharge视图中,纵轴表示每段时间内的耗电百分比。 - 在
charge视图中,纵轴表示每段时间内的充入百分比。 - 纵轴刻度会根据当前区间动态收敛,避免在小范围波动时显示过密的刻度标签。