This commit is contained in:
2025-12-09 16:33:44 +08:00
parent 345af5e2a3
commit 1d42f5ea50
49 changed files with 12015 additions and 1 deletions

740
device.go Executable file
View File

@@ -0,0 +1,740 @@
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"os"
"strings"
"sync"
"time"
)
// Device 设备信息结构
type Device struct {
MAC string `json:"mac"`
Username string `json:"username"`
DeviceName string `json:"device_name"`
Token string `json:"token"`
BindTime time.Time `json:"bind_time"`
LastSeen time.Time `json:"last_seen"`
IsActive bool `json:"is_active"`
}
// BindingCode 绑定码结构
type BindingCode struct {
Code string `json:"code"`
Username string `json:"username"`
ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used"`
}
// DeviceManager 设备管理器
type DeviceManager struct {
devices map[string]*Device // MAC -> Device
bindingCodes map[string]*BindingCode // Code -> BindingCode
tokens map[string]string // Token -> MAC
mu sync.RWMutex
filePath string
}
var deviceManager *DeviceManager
var deviceManagerOnce sync.Once
// GetDeviceManager 获取设备管理器单例
func GetDeviceManager() *DeviceManager {
deviceManagerOnce.Do(func() {
deviceManager = &DeviceManager{
devices: make(map[string]*Device),
bindingCodes: make(map[string]*BindingCode),
tokens: make(map[string]string),
filePath: "./devices.json",
}
deviceManager.LoadFromFile()
})
return deviceManager
}
// GenerateBindingCode 生成6位数字绑定码
func (dm *DeviceManager) GenerateBindingCode(username string) (string, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
// 生成6位随机数字
var code string
for i := 0; i < 6; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
code += fmt.Sprintf("%d", n.Int64())
}
// 检查是否已存在(小概率)
if _, exists := dm.bindingCodes[code]; exists {
// 递归重新生成
return dm.GenerateBindingCode(username)
}
// 创建绑定码5分钟有效
bindingCode := &BindingCode{
Code: code,
Username: username,
ExpiresAt: time.Now().Add(5 * time.Minute),
Used: false,
}
dm.bindingCodes[code] = bindingCode
fmt.Printf("[Device] Generated binding code %s for user %s\n", code, username)
return code, nil
}
// generateDeviceToken 生成设备Token
func generateDeviceToken() string {
b := make([]byte, 32)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
// BindDevice 绑定设备
func (dm *DeviceManager) BindDevice(mac, bindingCode, deviceName string) (*Device, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
// 验证绑定码
code, exists := dm.bindingCodes[bindingCode]
if !exists {
return nil, fmt.Errorf("绑定码不存在")
}
if code.Used {
return nil, fmt.Errorf("绑定码已使用")
}
if time.Now().After(code.ExpiresAt) {
return nil, fmt.Errorf("绑定码已过期")
}
// 检查设备是否已绑定
if existingDevice, exists := dm.devices[mac]; exists {
return nil, fmt.Errorf("设备已绑定到用户 %s", existingDevice.Username)
}
// 生成Token
token := generateDeviceToken()
// 创建设备
device := &Device{
MAC: mac,
Username: code.Username,
DeviceName: deviceName,
Token: token,
BindTime: time.Now(),
LastSeen: time.Now(),
IsActive: true,
}
// 保存设备信息
dm.devices[mac] = device
dm.tokens[token] = mac
// 标记绑定码已使用
code.Used = true
// 保存到文件
dm.SaveToFile()
fmt.Printf("[Device] Device %s bound to user %s\n", mac, code.Username)
return device, nil
}
// VerifyToken 验证设备Token
func (dm *DeviceManager) VerifyToken(token string) (*Device, error) {
dm.mu.RLock()
defer dm.mu.RUnlock()
mac, exists := dm.tokens[token]
if !exists {
return nil, fmt.Errorf("无效的Token")
}
device, exists := dm.devices[mac]
if !exists {
return nil, fmt.Errorf("设备不存在")
}
if !device.IsActive {
return nil, fmt.Errorf("设备已停用")
}
return device, nil
}
// GetDeviceByMAC 根据MAC地址获取设备
func (dm *DeviceManager) GetDeviceByMAC(mac string) (*Device, error) {
dm.mu.RLock()
defer dm.mu.RUnlock()
device, exists := dm.devices[mac]
if !exists {
return nil, fmt.Errorf("设备未绑定")
}
return device, nil
}
// UpdateLastSeen 更新设备最后在线时间
func (dm *DeviceManager) UpdateLastSeen(mac string) {
dm.mu.Lock()
defer dm.mu.Unlock()
if device, exists := dm.devices[mac]; exists {
device.LastSeen = time.Now()
dm.SaveToFile()
}
}
// DirectBindDevice 直接绑定设备(不需要绑定码)
func (dm *DeviceManager) DirectBindDevice(mac, username, deviceName string) (*Device, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
// 检查设备是否已绑定
if existingDevice, exists := dm.devices[mac]; exists {
return nil, fmt.Errorf("设备已绑定到用户 %s", existingDevice.Username)
}
// 生成Token
token := generateDeviceToken()
// 创建设备
device := &Device{
MAC: mac,
Username: username,
DeviceName: deviceName,
Token: token,
BindTime: time.Now(),
LastSeen: time.Now(),
IsActive: true,
}
dm.devices[mac] = device
dm.tokens[token] = mac
dm.SaveToFile()
fmt.Printf("[Device] Device %s directly bound to user %s\n", mac, username)
return device, nil
}
// UnbindDevice 解绑设备
func (dm *DeviceManager) UnbindDevice(mac string) error {
dm.mu.Lock()
defer dm.mu.Unlock()
device, exists := dm.devices[mac]
if !exists {
return fmt.Errorf("设备不存在")
}
// 删除Token映射
delete(dm.tokens, device.Token)
// 删除设备
delete(dm.devices, mac)
dm.SaveToFile()
fmt.Printf("[Device] Device %s unbound\n", mac)
return nil
}
// GetDevice 获取单个设备信息
func (dm *DeviceManager) GetDevice(mac string) (*Device, error) {
dm.mu.RLock()
defer dm.mu.RUnlock()
device, exists := dm.devices[mac]
if !exists {
return nil, fmt.Errorf("设备不存在")
}
return device, nil
}
// GetUserDevices 获取用户的所有设备
func (dm *DeviceManager) GetUserDevices(username string) []*Device {
dm.mu.RLock()
defer dm.mu.RUnlock()
var devices []*Device
for _, device := range dm.devices {
if device.Username == username {
devices = append(devices, device)
}
}
return devices
}
// SaveToFile 保存设备信息到文件
func (dm *DeviceManager) SaveToFile() error {
data := struct {
Devices map[string]*Device `json:"devices"`
}{
Devices: dm.devices,
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Println("[Error] Failed to marshal devices:", err)
return err
}
err = os.WriteFile(dm.filePath, jsonData, 0644)
if err != nil {
fmt.Println("[Error] Failed to write devices.json:", err)
return err
}
return nil
}
// LoadFromFile 从文件加载设备信息
func (dm *DeviceManager) LoadFromFile() error {
file, err := os.Open(dm.filePath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("[Info] devices.json not found, creating new file")
return nil
}
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
var fileData struct {
Devices map[string]*Device `json:"devices"`
}
err = json.Unmarshal(data, &fileData)
if err != nil {
return err
}
dm.devices = fileData.Devices
if dm.devices == nil {
dm.devices = make(map[string]*Device)
}
// 重建Token索引
for mac, device := range dm.devices {
dm.tokens[device.Token] = mac
}
fmt.Printf("[Info] Loaded %d devices from file\n", len(dm.devices))
return nil
}
// HTTP处理器
// GenerateBindingCodeHandler 生成绑定码
func GenerateBindingCodeHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前登录用户
username := GetCurrentUser(r) // 需要从session获取
if username == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
dm := GetDeviceManager()
code, err := dm.GenerateBindingCode(username)
if err != nil {
http.Error(w, "Failed to generate binding code", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"code": code,
"expires_in": 300, // 5分钟
})
}
// BindDeviceHandler ESP32设备绑定接口
func BindDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
fmt.Println("[API] ESP32 device bind request received from", r.RemoteAddr)
// 解析请求
var req struct {
MAC string `json:"mac"`
BindingCode string `json:"binding_code"`
DeviceName string `json:"device_name"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.MAC == "" || req.BindingCode == "" {
http.Error(w, "Missing required fields", http.StatusBadRequest)
return
}
// 如果没有提供设备名称,使用默认名称
if req.DeviceName == "" {
req.DeviceName = "ESP32音乐播放器"
}
dm := GetDeviceManager()
device, err := dm.BindDevice(req.MAC, req.BindingCode, req.DeviceName)
if err != nil {
fmt.Printf("[Error] Device bind failed: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "设备绑定成功",
"token": device.Token,
"username": device.Username,
})
}
// VerifyDeviceHandler 验证设备Token
func VerifyDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
token := r.Header.Get("X-Device-Token")
if token == "" {
token = r.URL.Query().Get("token")
}
if token == "" {
http.Error(w, "Missing token", http.StatusBadRequest)
return
}
dm := GetDeviceManager()
device, err := dm.VerifyToken(token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// 更新最后在线时间
dm.UpdateLastSeen(device.MAC)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"device": map[string]interface{}{
"mac": device.MAC,
"username": device.Username,
"device_name": device.DeviceName,
"bind_time": device.BindTime,
"last_seen": device.LastSeen,
},
})
}
// GetCurrentUser 从请求中获取当前登录用户
func GetCurrentUser(r *http.Request) string {
userStore := GetUserStore()
// 1. 尝试从 Authorization Header 获取 (Bearer Token)
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
token := strings.TrimPrefix(authHeader, "Bearer ")
// 直接从UserStore验证Token
user, err := userStore.GetUserByToken(token)
if err == nil && user != nil {
return user.Username
}
}
// 2. 尝试从 Cookie 获取 session_token
cookie, err := r.Cookie("session_token")
if err == nil {
username := userStore.GetUsernameByToken(cookie.Value)
if username != "" {
return username
}
}
// 3. 尝试从 X-Device-Token Header 获取 (用于ESP32设备)
deviceToken := r.Header.Get("X-Device-Token")
if deviceToken != "" {
dm := GetDeviceManager()
device, err := dm.VerifyToken(deviceToken)
if err == nil && device != nil {
// 更新最后在线时间
dm.UpdateLastSeen(device.MAC)
return device.Username
}
}
return ""
}
// DirectBindDeviceHandler Web端直接绑定设备不需要绑定码
func DirectBindDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前用户
username := GetCurrentUser(r)
if username == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "未登录",
})
return
}
// 解析请求
var req struct {
MAC string `json:"mac"`
DeviceName string `json:"device_name"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "请求格式错误",
})
return
}
if req.MAC == "" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "MAC地址不能为空",
})
return
}
// 如果没有提供设备名称,使用默认名称
if req.DeviceName == "" {
req.DeviceName = "ESP32音乐播放器"
}
dm := GetDeviceManager()
device, err := dm.DirectBindDevice(req.MAC, username, req.DeviceName)
if err != nil {
fmt.Printf("[Error] Direct bind failed: %v\n", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "设备绑定成功",
"device": map[string]interface{}{
"mac": device.MAC,
"device_name": device.DeviceName,
"bind_time": device.BindTime,
},
})
}
// ListUserDevicesHandler 列出用户的所有设备
func ListUserDevicesHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前用户
username := GetCurrentUser(r)
if username == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "未登录",
})
return
}
dm := GetDeviceManager()
devices := dm.GetUserDevices(username)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"devices": devices,
})
}
// UnbindDeviceHandler 解绑设备
func UnbindDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取当前用户
username := GetCurrentUser(r)
if username == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "未登录",
})
return
}
// 解析请求
var req struct {
MAC string `json:"mac"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "请求格式错误",
})
return
}
if req.MAC == "" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "MAC地址不能为空",
})
return
}
dm := GetDeviceManager()
// 检查设备是否属于当前用户
device, err := dm.GetDevice(req.MAC)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "设备不存在",
})
return
}
if device.Username != username {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "无权解绑此设备",
})
return
}
// 解绑设备
err = dm.UnbindDevice(req.MAC)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "设备已解绑",
})
}
// SyncDeviceHandler ESP32用MAC地址同步Token用于网页端绑定后自动获取token
func SyncDeviceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
MAC string `json:"mac"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.MAC == "" {
http.Error(w, "Missing MAC address", http.StatusBadRequest)
return
}
dm := GetDeviceManager()
device, err := dm.GetDevice(req.MAC)
if err != nil || device == nil {
// 设备未绑定
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": "设备未绑定",
})
return
}
// 更新最后在线时间
dm.UpdateLastSeen(device.MAC)
// 返回 token 和用户名
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"token": device.Token,
"username": device.Username,
"message": "同步成功",
})
fmt.Printf("[Info] Device %s synced token for user: %s\n", device.MAC, device.Username)
}