// Package rtcclient 封装对 RTC 后端 Django REST API 的 HTTP 调用。 package rtcclient import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" ) // StoryInfo 是 GET /api/v1/devices/stories/ 返回的故事信息。 type StoryInfo struct { Title string `json:"title"` AudioURL string `json:"audio_url"` } // Client 是 RTC 后端的 HTTP 客户端,复用连接池。 type Client struct { baseURL string httpClient *http.Client } // New 创建 Client,baseURL 形如 "http://rtc-backend-svc:8000"。 func New(baseURL string) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), httpClient: &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ MaxIdleConns: 50, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, }, // 限制重定向次数,防止无限跳转 CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 3 { return errors.New("rtcclient: too many redirects") } return nil }, }, } } // rtcResponse 是 RTC 后端的统一响应结构。 type rtcResponse struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } // FetchStoryByMAC 通过设备 MAC 地址获取随机故事。 // 返回 nil, nil 表示设备/用户/故事不存在(非错误,调用方直接跳过)。 func (c *Client) FetchStoryByMAC(ctx context.Context, mac string) (*StoryInfo, error) { url := c.baseURL + "/api/v1/devices/stories/" req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("rtcclient: build request: %w", err) } q := req.URL.Query() q.Set("mac_address", strings.ToUpper(mac)) req.URL.RawQuery = q.Encode() resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("rtcclient: request failed: %w", err) } defer resp.Body.Close() // 404 表示设备/用户/故事不存在,不是服务器错误 if resp.StatusCode == http.StatusNotFound { return nil, nil } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("rtcclient: unexpected status %d", resp.StatusCode) } var result rtcResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("rtcclient: decode response: %w", err) } if result.Code != 0 { return nil, nil // 业务错误(如暂无故事),返回 nil 让调用方处理 } var story StoryInfo if err := json.Unmarshal(result.Data, &story); err != nil { return nil, fmt.Errorf("rtcclient: decode story: %w", err) } if story.Title == "" || story.AudioURL == "" { return nil, nil } return &story, nil }