Compare commits

..

No commits in common. "79d8beb942885ae8e1f35f33e5749f9dd6b31668" and "c219ec2fcf8bc7e6a7f161b152e3a4986cded171" have entirely different histories.

5 changed files with 7 additions and 214 deletions

View File

@ -306,73 +306,6 @@ class DeviceViewSet(viewsets.ViewSet):
return success(message='WiFi 配置成功') return success(message='WiFi 配置成功')
@action(
detail=False, methods=['get'],
url_path='stories',
authentication_classes=[], permission_classes=[AllowAny]
)
def stories_by_mac(self, request):
"""
获取设备关联用户的随机故事公开接口无需认证
GET /api/v1/devices/stories/?mac_address=AA:BB:CC:DD:EE:FF
hw-ws-service 调用
优先返回用户自己的故事无则兜底返回系统默认故事is_default=True
"""
mac = request.query_params.get('mac_address', '').strip()
if not mac:
return error(message='mac_address 参数不能为空')
mac = mac.upper().replace('-', ':')
try:
device = Device.objects.get(mac_address=mac)
except Device.DoesNotExist:
return error(
code=ErrorCode.DEVICE_NOT_FOUND,
message='未找到对应设备',
status_code=status.HTTP_404_NOT_FOUND
)
user_device = (
UserDevice.objects
.filter(device=device, is_active=True, bind_type='owner')
.select_related('user')
.first()
)
if not user_device:
return error(
code=ErrorCode.NOT_FOUND,
message='该设备尚未绑定用户',
status_code=status.HTTP_404_NOT_FOUND
)
from apps.stories.models import Story
# 优先随机取用户自己有 audio_url 的故事
story = (
Story.objects
.filter(user=user_device.user)
.exclude(audio_url='')
.order_by('?')
.first()
)
# 兜底:用户暂无故事时使用系统默认故事
if not story:
story = (
Story.objects
.filter(is_default=True)
.exclude(audio_url='')
.order_by('?')
.first()
)
if not story:
return error(
code=ErrorCode.STORY_NOT_FOUND,
message='暂无可播放的故事',
status_code=status.HTTP_404_NOT_FOUND
)
return success(data={'title': story.title, 'audio_url': story.audio_url})
@action(detail=False, methods=['post'], url_path='report-status', @action(detail=False, methods=['post'], url_path='report-status',
authentication_classes=[], permission_classes=[AllowAny]) authentication_classes=[], permission_classes=[AllowAny])
def report_status(self, request): def report_status(self, request):

View File

@ -12,14 +12,12 @@ import (
// Connection 保存单个硬件连接的状态,所有方法并发安全。 // Connection 保存单个硬件连接的状态,所有方法并发安全。
type Connection struct { type Connection struct {
WS *websocket.Conn WS *websocket.Conn
DeviceID string // MAC 地址,来自 URL 参数 device-id DeviceID string // MAC 地址,来自 URL 参数 device-id
ClientID string // 来自 URL 参数 client-id ClientID string // 来自 URL 参数 client-id
SessionID string // 握手后分配的会话 ID
mu sync.Mutex mu sync.Mutex
handshaked bool // 是否已完成 hello 握手 isPlaying bool
isPlaying bool abortCh chan struct{} // close(abortCh) 通知流控 goroutine 中止播放
abortCh chan struct{} // close(abortCh) 通知流控 goroutine 中止播放
writeMu sync.Mutex // gorilla/websocket 写操作不并发安全,需独立锁 writeMu sync.Mutex // gorilla/websocket 写操作不并发安全,需独立锁
} }
@ -33,30 +31,6 @@ func New(ws *websocket.Conn, deviceID, clientID string) *Connection {
} }
} }
// Handshake 完成 hello 握手,存储 session_id。
func (c *Connection) Handshake(sessionID string) {
c.mu.Lock()
defer c.mu.Unlock()
c.SessionID = sessionID
c.handshaked = true
}
// IsHandshaked 返回连接是否已完成 hello 握手。
func (c *Connection) IsHandshaked() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.handshaked
}
// SendCmd 向硬件发送控制指令,并发安全。
func (c *Connection) SendCmd(action string, params any) error {
return c.SendJSON(map[string]any{
"type": "cmd",
"action": action,
"params": params,
})
}
// StartPlayback 开始新一轮播放,返回 abortCh 供流控 goroutine 监听。 // StartPlayback 开始新一轮播放,返回 abortCh 供流控 goroutine 监听。
// 若已在播放,先中止上一轮再开始新的。 // 若已在播放,先中止上一轮再开始新的。
func (c *Connection) StartPlayback() <-chan struct{} { func (c *Connection) StartPlayback() <-chan struct{} {

View File

@ -1,13 +0,0 @@
package handler
import (
"log"
"github.com/qy/hw-ws-service/internal/connection"
)
// HandleAbort 处理硬件发来的 {"type":"abort"} 指令,中止当前播放。
func HandleAbort(conn *connection.Connection) {
log.Printf("[abort][%s] stopping playback", conn.DeviceID)
conn.StopPlayback()
}

View File

@ -1,45 +0,0 @@
package handler
import (
"crypto/rand"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/qy/hw-ws-service/internal/connection"
)
// helloMessage 是硬件发来的 hello 握手消息。
type helloMessage struct {
MAC string `json:"mac"`
}
// HandleHello 处理硬件的 hello 握手消息。
// 校验 MAC 地址,分配 session_id返回握手响应。
func HandleHello(conn *connection.Connection, raw []byte) error {
var msg helloMessage
if err := json.Unmarshal(raw, &msg); err != nil {
return fmt.Errorf("hello: invalid json: %w", err)
}
// MAC 地址与 URL 参数不一致时记录警告,但不拒绝连接
if msg.MAC != "" && !strings.EqualFold(msg.MAC, conn.DeviceID) {
log.Printf("[hello][%s] MAC mismatch: url=%s body=%s", conn.DeviceID, conn.DeviceID, msg.MAC)
}
sessionID := newSessionID()
conn.Handshake(sessionID)
return conn.SendJSON(map[string]string{
"type": "hello",
"status": "ok",
"session_id": sessionID,
})
}
func newSessionID() string {
b := make([]byte, 4)
rand.Read(b) //nolint:errcheck // crypto/rand.Read 在标准库中不会返回错误
return fmt.Sprintf("%x", b)
}

View File

@ -10,7 +10,6 @@ import (
"net" "net"
"net/http" "net/http"
"sync" "sync"
"time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/qy/hw-ws-service/internal/connection" "github.com/qy/hw-ws-service/internal/connection"
@ -23,8 +22,6 @@ const (
maxConnections = 500 maxConnections = 500
// maxMessageBytes WebSocket 单条消息上限4KB防止内存耗尽攻击。 // maxMessageBytes WebSocket 单条消息上限4KB防止内存耗尽攻击。
maxMessageBytes = 4 * 1024 maxMessageBytes = 4 * 1024
// helloTimeout 握手超时:连接建立后必须在此时间内发送 hello否则断开。
helloTimeout = 10 * time.Second
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
@ -134,17 +131,7 @@ func (s *Server) handleConn(w http.ResponseWriter, r *http.Request) {
log.Printf("server: device %s connected, active=%d", deviceID, s.activeCount()) log.Printf("server: device %s connected, active=%d", deviceID, s.activeCount())
// 阶段1等待 hello 握手(超时 helloTimeout // 消息读取循环
ws.SetReadDeadline(time.Now().Add(helloTimeout)) //nolint:errcheck
if !s.waitForHello(conn) {
log.Printf("server: device %s hello timeout or failed", deviceID)
return
}
ws.SetReadDeadline(time.Time{}) //nolint:errcheck // 握手成功,取消读超时
log.Printf("server: device %s handshaked, session=%s", deviceID, conn.SessionID)
// 阶段2正常消息循环
for { for {
msgType, raw, err := ws.ReadMessage() msgType, raw, err := ws.ReadMessage()
if err != nil { if err != nil {
@ -156,7 +143,7 @@ func (s *Server) handleConn(w http.ResponseWriter, r *http.Request) {
return return
} }
// 只处理文本消息 // 只处理文本消息(二进制为上行音频,本服务暂不处理)
if msgType != websocket.TextMessage { if msgType != websocket.TextMessage {
continue continue
} }
@ -172,40 +159,12 @@ func (s *Server) handleConn(w http.ResponseWriter, r *http.Request) {
switch envelope.Type { switch envelope.Type {
case "story": case "story":
go handler.HandleStory(conn, s.client) go handler.HandleStory(conn, s.client)
case "abort":
handler.HandleAbort(conn)
default: default:
log.Printf("server: unhandled message type %q from %s", envelope.Type, deviceID) log.Printf("server: unhandled message type %q from %s", envelope.Type, deviceID)
} }
} }
} }
// waitForHello 等待并处理第一条 hello 消息,成功返回 true。
func (s *Server) waitForHello(conn *connection.Connection) bool {
msgType, raw, err := conn.WS.ReadMessage()
if err != nil {
return false
}
if msgType != websocket.TextMessage {
log.Printf("server: device %s sent non-text as first message", conn.DeviceID)
return false
}
var envelope struct {
Type string `json:"type"`
}
if err := json.Unmarshal(raw, &envelope); err != nil || envelope.Type != "hello" {
log.Printf("server: device %s first message is not hello (got %q)", conn.DeviceID, envelope.Type)
return false
}
if err := handler.HandleHello(conn, raw); err != nil {
log.Printf("server: device %s hello failed: %v", conn.DeviceID, err)
return false
}
return true
}
// register 注册连接,若同一设备已有连接则踢掉旧连接。 // register 注册连接,若同一设备已有连接则踢掉旧连接。
func (s *Server) register(conn *connection.Connection) error { func (s *Server) register(conn *connection.Connection) error {
s.mu.Lock() s.mu.Lock()
@ -225,21 +184,6 @@ func (s *Server) register(conn *connection.Connection) error {
return nil return nil
} }
// SendCmd 向指定设备发送控制指令。
// 若设备不在线或未握手,返回 error。
func (s *Server) SendCmd(deviceID, action string, params any) error {
s.mu.Lock()
conn, ok := s.conns[deviceID]
s.mu.Unlock()
if !ok {
return fmt.Errorf("server: device %s not connected", deviceID)
}
if !conn.IsHandshaked() {
return fmt.Errorf("server: device %s not handshaked", deviceID)
}
return conn.SendCmd(action, params)
}
func (s *Server) unregister(deviceID string) { func (s *Server) unregister(deviceID string) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()