# 设备与在线时长接口

## 概述
- 基础地址：`http://localhost:8000`
- 版本前缀：`/api/v1`
- 说明：同一账号仅允许一个设备同时在线（依据 `device_id`）；后端会记录登录登出并可统计在线时长。

## 登录（含单设备限制）
- 方法与路径：`POST /api/v1/auth/login`
- 请求体：
```json
{
  "username": "alice",
  "password": "secret123",
  "device_id": "devA"
}
```
- 成功响应：
```json
{
  "access_token": "<JWT>",
  "token_type": "bearer",
  "username": "alice"
}
```
- 失败响应：
  - 401：`用户名或密码错误`
  - 403：`该账号已在其他设备在线`
- 说明：
  - 必须传入稳定的 `device_id`（前端生成并持久化），用于限制并发登录与统计时长
  - 登录成功会记录会话的 `login_at`，并将设备会话标记为活跃

## 登出（记录在线时长）
- 方法与路径：`POST /api/v1/auth/logout`
- 请求体：
```json
{
  "username": "alice",
  "device_id": "devA"
}
```
- 成功响应：
```json
{ "detail": "已退出登录" }
```
- 行为：
  - 将对应设备的会话置为非活跃，并记录 `logout_at`
  - 若存在 `login_at`，会计算本次会话的在线时长 `duration_seconds`（单位：秒）

## 在线时长统计
- 方法与路径：`GET /api/v1/auth/online-time/{username}`
- 成功响应：
```json
{
  "username": "alice",
  "total_seconds": 1234,
  "active_seconds": 56
}
```
- 字段说明：
  - `total_seconds`：历史所有已登出会话的累计在线时长（秒）
  - `active_seconds`：当前活跃会话的实时在线时长（秒），若无活跃会话则为 0；实时时长基于最近心跳时间 `last_seen_at` 与 `login_at` 的差值
- 统计逻辑：
  - 登录时写入 `login_at`（UTC）
  - 登出时写入 `logout_at` 并计算 `duration_seconds`
  - 查询时将已登出会话的 `duration_seconds` 累加为 `total_seconds`，并以当前时间与活跃会话的 `login_at` 计算 `active_seconds`

## 心跳接口（保持在线时长统计）
- 方法与路径：`POST /api/v1/auth/heartbeat`
- 请求体：
```json
{
  "username": "alice",
  "device_id": "devA"
}
```
- 成功响应：
```json
{ "detail": "心跳已更新" }
```
- 说明：
  - 前端应在用户在线期间定期调用心跳（例如每 30～60 秒），以更新会话的 `last_seen_at`
  - 若未调用登出而直接关闭应用，在线时长将以最近一次心跳为准，不会无限累计

## 错误信息（统一中文）
- 401：`用户名或密码错误`
- 403：`该账号已在其他设备在线`
- 404：`用户不存在`

## 前端调用示例（Axios）
```ts
import axios from 'axios';
const API = 'http://localhost:8000/api/v1/auth';

export async function login(username: string, password: string, deviceId: string) {
  return axios.post(`${API}/login`, { username, password, device_id: deviceId });
}

export async function logout(username: string, deviceId: string) {
  return axios.post(`${API}/logout`, { username, device_id: deviceId });
}

export async function getOnlineTime(username: string) {
  return axios.get(`${API}/online-time/${encodeURIComponent(username)}`);
}
```
