126 lines
4.5 KiB
Go
126 lines
4.5 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/device"
|
||
|
|
"data-terminal-cloud/internal/deviceauth"
|
||
|
|
"data-terminal-cloud/internal/saas"
|
||
|
|
)
|
||
|
|
|
||
|
|
type DeviceCommandService interface {
|
||
|
|
PullCommands(context.Context, deviceauth.Identity, int) ([]device.Command, error)
|
||
|
|
AcknowledgeCommand(context.Context, deviceauth.Identity, string, device.CommandAcknowledgement) (device.Command, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func PullDeviceCommandsHandler(service DeviceCommandService) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
id, ok := deviceauth.FromContext(r.Context())
|
||
|
|
if !ok {
|
||
|
|
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||
|
|
items, err := service.PullCommands(r.Context(), id, limit)
|
||
|
|
if err != nil {
|
||
|
|
WriteAPIError(w, r, http.StatusServiceUnavailable, Error{Code: "DEVICE_COMMANDS_UNAVAILABLE", Message: "暂时无法读取设备命令", Retryable: true})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func AcknowledgeDeviceCommandHandler(service DeviceCommandService) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
id, ok := deviceauth.FromContext(r.Context())
|
||
|
|
if !ok {
|
||
|
|
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
Status string `json:"status"`
|
||
|
|
ResultSummary map[string]any `json:"result_summary"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "设备命令回执不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.Status = strings.ToUpper(strings.TrimSpace(input.Status))
|
||
|
|
if input.Status != "ACKNOWLEDGED" && input.Status != "SUCCEEDED" && input.Status != "FAILED" {
|
||
|
|
writeValidation(w, r, "设备命令回执不正确", FieldError{Field: "status", Message: "必须为ACKNOWLEDGED、SUCCEEDED或FAILED"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if input.ResultSummary == nil {
|
||
|
|
input.ResultSummary = map[string]any{}
|
||
|
|
}
|
||
|
|
item, err := service.AcknowledgeCommand(r.Context(), id, r.PathValue("id"), device.CommandAcknowledgement{Status: input.Status, ResultSummary: input.ResultSummary})
|
||
|
|
if err != nil {
|
||
|
|
WriteError(w, r, http.StatusConflict, "DEVICE_COMMAND_STATE_CONFLICT", "命令不存在、已过期或状态不允许回执")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListDeviceCommandsHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListDeviceCommands(r.Context(), p, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func CreateDeviceCommandHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
key := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||
|
|
if len(key) < 16 || len(key) > 128 {
|
||
|
|
writeValidation(w, r, "缺少有效的重复请求识别码", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128字符"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
Type string `json:"type"`
|
||
|
|
Payload map[string]any `json:"payload"`
|
||
|
|
ExpiresInSec int `json:"expires_in_sec"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "设备命令参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.Type = strings.TrimSpace(input.Type)
|
||
|
|
if input.Type != "apply_config" && input.Type != "reboot" && input.Type != "diagnostics" && input.Type != "firmware_update" {
|
||
|
|
writeValidation(w, r, "设备命令参数不正确", FieldError{Field: "type", Message: "命令类型不受支持"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if input.ExpiresInSec < 30 || input.ExpiresInSec > 86400 {
|
||
|
|
writeValidation(w, r, "设备命令参数不正确", FieldError{Field: "expires_in_sec", Message: "必须为30至86400秒"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if input.Payload == nil {
|
||
|
|
input.Payload = map[string]any{}
|
||
|
|
}
|
||
|
|
item, created, err := service.CreateDeviceCommand(r.Context(), p, r.PathValue("id"), key, RequestID(r.Context()), input.Type, input.Payload, input.ExpiresInSec)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
status := http.StatusAccepted
|
||
|
|
if !created {
|
||
|
|
status = http.StatusOK
|
||
|
|
w.Header().Set("Idempotency-Replayed", "true")
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, status, item)
|
||
|
|
})
|
||
|
|
}
|