# 许可证验证接口文档

## 📋 接口概述

**接口路径：** `POST /api/v1/auth/verify`

**功能：** 验证用户的登录 token 是否有效（用于心跳检测）

**调用频率：** 每 60 秒调用一次

---

## 📥 请求参数

### Headers
```json
{
  "Content-Type": "application/json",
  "Authorization": "Bearer {token}"
}
```

### Body (JSON)
```json
{
  "username": "string",       // 用户名
  "device_id": "string",      // 设备 ID
  "timestamp": 1234567890     // 客户端时间戳（毫秒）
}
```

### 示例请求
```http
POST /api/v1/auth/verify HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "username": "zuowei",
  "device_id": "065dfbc8-9539-405e-8af0-7cfd56000e6",
  "timestamp": 1702828800000
}
```

---

## 📤 响应格式

### 成功响应（200 OK）

```json
{
  "valid": true,
  "username": "zuowei",
  "expire_date": "2025-12-31T23:59:59Z"  // 可选：账户过期时间
}
```

### 失败响应 1：Token 无效（401 Unauthorized）

```json
{
  "detail": "Token 无效或已过期"
}
```

### 失败响应 2：账户已过期（403 Forbidden）

```json
{
  "valid": false,
  "message": "账户已过期"
}
```

### 失败响应 3：会话不存在（404 Not Found）

```json
{
  "detail": "会话不存在或已登出"
}
```

---

## 🔧 后端实现示例（FastAPI）

### 1. 在 `Server/app/api/v1/auth.py` 添加路由

```python
from fastapi import Depends, HTTPException, status
from pydantic import BaseModel
from app.core.security import get_current_user
from app.db import get_db
from sqlalchemy.orm import Session
from app.models.user import User, UserSession
from datetime import datetime, timezone

class VerifyRequest(BaseModel):
    """验证请求"""
    username: str
    device_id: str
    timestamp: int

class VerifyResponse(BaseModel):
    """验证响应"""
    valid: bool
    username: str = None
    expire_date: str = None

@router.post("/verify", response_model=VerifyResponse)
async def verify_license(
    request: VerifyRequest,
    current_user: dict = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    """
    验证许可证（心跳检测）
    检查：
    1. Token 是否有效（通过 Depends(get_current_user) 自动验证）
    2. 用户是否存在
    3. 会话是否活跃
    4. 账户是否过期
    """
    
    # 1. 查询用户
    user = db.query(User).filter(User.username == request.username).first()
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 2. 检查账户是否过期
    if user.expire_date:
        expire_dt = user.expire_date
        if expire_dt.tzinfo is None:
            expire_dt = expire_dt.replace(tzinfo=timezone.utc)
        
        if datetime.now(timezone.utc) > expire_dt:
            return VerifyResponse(
                valid=False,
                username=request.username,
                expire_date=user.expire_date.isoformat() if user.expire_date else None
            )
    
    # 3. 检查会话是否活跃
    session = db.query(UserSession).filter(
        UserSession.user_id == user.id,
        UserSession.device_id == request.device_id,
        UserSession.active == True
    ).first()
    
    if not session:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="会话不存在或已登出"
        )
    
    # 4. 更新最后活跃时间
    session.last_seen_at = datetime.now(timezone.utc)
    db.commit()
    
    # 5. 返回验证成功
    return VerifyResponse(
        valid=True,
        username=request.username,
        expire_date=user.expire_date.isoformat() if user.expire_date else None
    )
```

---

### 2. 更新 `Server/app/schemas/auth.py`

添加验证相关的 Schema：

```python
class VerifyRequest(BaseModel):
    """验证请求模型"""
    username: str
    device_id: str
    timestamp: int

class VerifyResponse(BaseModel):
    """验证响应模型"""
    valid: bool
    username: Optional[str] = None
    expire_date: Optional[str] = None
```

---

## 🧪 测试验证接口

### 使用 curl 测试

```bash
curl -X POST http://127.0.0.1:8000/api/v1/auth/verify \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -d '{
    "username": "zuowei",
    "device_id": "test-device-123",
    "timestamp": 1702828800000
  }'
```

### 预期响应

**成功：**
```json
{
  "valid": true,
  "username": "zuowei",
  "expire_date": null
}
```

**Token 无效：**
```json
{
  "detail": "Could not validate credentials"
}
```

**会话不存在：**
```json
{
  "detail": "会话不存在或已登出"
}
```

---

## 📊 数据库表设计（参考）

确保数据库有以下表和字段：

### users 表
```sql
CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username VARCHAR UNIQUE NOT NULL,
    expire_date DATETIME,  -- 账户过期时间（NULL = 永久）
    ...
);
```

### user_sessions 表
```sql
CREATE TABLE user_sessions (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    device_id VARCHAR NOT NULL,
    active BOOLEAN DEFAULT TRUE,
    last_seen_at DATETIME,  -- 最后活跃时间
    ...
    FOREIGN KEY (user_id) REFERENCES users(id)
);
```

---

## 🔐 安全注意事项

1. **Token 验证**
   - 使用 `Depends(get_current_user)` 确保 Token 有效
   - Token 过期自动返回 401

2. **会话管理**
   - 验证成功时更新 `last_seen_at`
   - 可用于统计在线时长

3. **过期检查**
   - 支持账户过期时间
   - 过期返回 `valid: false`

4. **频率控制**
   - 前端已做 30 秒缓存，减少请求频率
   - 后端可添加 Rate Limiting

---

## 🚀 部署检查

添加接口后，确认：

1. ✅ 路由已注册
   ```python
   # Server/app/main.py
   app.include_router(auth.router, prefix=f"{settings.API_V1_STR}/auth")
   ```

2. ✅ 数据库表存在
   ```bash
   # 检查 users 和 user_sessions 表
   ```

3. ✅ Token 验证正常
   ```bash
   # 测试登录获取 Token
   # 测试 verify 接口
   ```

4. ✅ 前端心跳正常
   ```
   # 前端每 60 秒调用一次
   # 30 秒内不重复验证（有缓存）
   ```

---

## 📝 完整实现清单

- [ ] 添加 `VerifyRequest` 和 `VerifyResponse` Schema
- [ ] 在 `auth.py` 添加 `/verify` 路由
- [ ] 实现 Token 验证逻辑
- [ ] 实现账户过期检查
- [ ] 实现会话活跃检查
- [ ] 更新最后活跃时间
- [ ] 测试接口（成功、失败、过期等场景）
- [ ] 部署到服务器

---

## 💡 可选增强

### 1. 添加限流（Rate Limiting）

```python
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@router.post("/verify")
@limiter.limit("120/minute")  # 每分钟最多 120 次
async def verify_license(...):
    ...
```

### 2. 添加详细日志

```python
import logging

logger = logging.getLogger(__name__)

@router.post("/verify")
async def verify_license(...):
    logger.info(f"[Verify] {request.username} from {request.device_id}")
    ...
```

### 3. 返回更多信息

```python
class VerifyResponse(BaseModel):
    valid: bool
    username: str = None
    expire_date: str = None
    permissions: list = []  # 用户权限列表
    online_time: int = 0    # 在线时长（秒）
```

---

**实现这个接口后，前端的心跳检测就能正常工作了！** ✅

