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 }