Files
story/internal/httpapi/device_handlers.go
T

120 lines
4.1 KiB
Go

package httpapi
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"data-terminal-cloud/internal/concurrency"
"data-terminal-cloud/internal/device"
"data-terminal-cloud/internal/deviceauth"
)
type DeviceService interface {
ReportBootstrap(context.Context, deviceauth.Identity, device.BootstrapReport) (device.Bootstrap, error)
GetConfiguration(context.Context, deviceauth.Identity) (device.Configuration, error)
}
type bootstrapResponse struct {
ServerTime time.Time `json:"server_time"`
ClockSkewMS int64 `json:"clock_skew_ms"`
Device device.Bootstrap `json:"device"`
}
func DeviceBootstrapHandler(service DeviceService) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
report, fieldErrors := parseBootstrapReport(request)
if len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "设备启动参数不正确", FieldErrors: fieldErrors,
})
return
}
result, err := service.ReportBootstrap(request.Context(), identity, report)
if err != nil {
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{
Code: "DEVICE_BOOTSTRAP_FAILED", Message: "暂时无法完成设备启动", Retryable: true,
})
return
}
now := time.Now().UTC()
WriteJSON(writer, request, http.StatusOK, bootstrapResponse{
ServerTime: now, ClockSkewMS: now.Sub(report.DeviceTime).Milliseconds(), Device: result,
})
})
}
func DeviceConfigHandler(service DeviceService) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
result, err := service.GetConfiguration(request.Context(), identity)
if err != nil {
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{
Code: "DEVICE_CONFIG_UNAVAILABLE", Message: "暂时无法读取设备配置", Retryable: true,
})
return
}
etag := concurrency.FormatETag(result.Revision)
writer.Header().Set("ETag", etag)
if request.Header.Get("If-None-Match") == etag {
writer.WriteHeader(http.StatusNotModified)
return
}
WriteJSON(writer, request, http.StatusOK, result)
})
}
func parseBootstrapReport(request *http.Request) (device.BootstrapReport, []FieldError) {
query := request.URL.Query()
report := device.BootstrapReport{
FirmwareVersion: strings.TrimSpace(query.Get("firmware_version")),
BoardRevision: strings.TrimSpace(query.Get("board_rev")),
RTCSDKVersion: strings.TrimSpace(query.Get("rtc_sdk_version")),
Codec: strings.TrimSpace(query.Get("codec")),
Network: strings.TrimSpace(query.Get("network")),
}
errors := make([]FieldError, 0)
if report.FirmwareVersion == "" {
errors = append(errors, FieldError{Field: "firmware_version", Message: "不能为空"})
}
for _, field := range []struct {
name string
value string
max int
}{
{"firmware_version", report.FirmwareVersion, 80},
{"board_rev", report.BoardRevision, 80},
{"rtc_sdk_version", report.RTCSDKVersion, 80},
{"codec", report.Codec, 40},
{"network", report.Network, 80},
} {
if len(field.value) > field.max {
errors = append(errors, FieldError{Field: field.name, Message: "长度超出限制"})
}
}
deviceTime, err := time.Parse(time.RFC3339, query.Get("device_time"))
if err != nil {
errors = append(errors, FieldError{Field: "device_time", Message: "必须是RFC3339时间"})
} else {
report.DeviceTime = deviceTime
}
if value := query.Get("sample_rate"); value != "" {
report.SampleRate, err = strconv.Atoi(value)
if err != nil || report.SampleRate <= 0 {
errors = append(errors, FieldError{Field: "sample_rate", Message: "必须是正整数"})
}
}
return report, errors
}