package httpapi import ( "errors" "net/http" "strings" "data-terminal-cloud/internal/concurrency" "data-terminal-cloud/internal/devicebinding" "data-terminal-cloud/internal/identity" "data-terminal-cloud/internal/opsdevice" ) func ListOpsDevicesHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } items, err := service.List(r.Context(), principal, tenantID) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func ListOpsDeviceOptionsHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } items, err := service.Options(r.Context(), principal, tenantID) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func CreateOpsDeviceHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } var body struct { Model string `json:"model"` } if !decodeWebJSON(w, r, &body, "设备登记参数不正确") { return } result, err := service.Create(r.Context(), principal, tenantID, RequestID(r.Context()), opsdevice.CreateInput{ Model: body.Model, }) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("ETag", concurrency.FormatETag(result.Device.Version)) WriteJSON(w, r, http.StatusCreated, result) }) } func TransitionOpsDeviceActivationHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } if !validUUID(r.PathValue("id")) { writeValidation(w, r, "设备编号不正确", FieldError{Field: "id", Message: "必须是UUID"}) return } var body struct { TargetState string `json:"target_state"` Reason string `json:"reason"` ExpectedRevision int64 `json:"expected_revision"` } if !decodeWebJSON(w, r, &body, "激活状态迁移参数不正确") { return } item, err := service.TransitionActivation(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), opsdevice.ActivationTransitionInput{ TargetState: body.TargetState, Reason: body.Reason, ExpectedRevision: body.ExpectedRevision, }) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("ETag", concurrency.FormatETag(item.Version)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func ListOpsDeviceActivationTransitionsHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } if !validUUID(r.PathValue("id")) { writeValidation(w, r, "设备编号不正确", FieldError{Field: "id", Message: "必须是UUID"}) return } items, err := service.ListActivationTransitions(r.Context(), principal, tenantID, r.PathValue("id")) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func UpdateOpsDeviceHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } if !validUUID(r.PathValue("id")) { writeValidation(w, r, "设备编号不正确", FieldError{Field: "id", Message: "必须是UUID"}) return } expected, err := concurrency.ParseIfMatch(r) if err != nil { writeETagError(w, r, err) return } var body struct { Model *string `json:"model"` HardwareRevision *string `json:"hardware_revision"` MinimumSecurityVersion *string `json:"minimum_security_version"` MinimumFirmwareVersion *string `json:"minimum_firmware_version"` Status *string `json:"status"` HouseholdID *string `json:"household_id"` SubjectID *string `json:"subject_id"` VoiceRuntime *string `json:"voice_runtime"` HardwareAgentBotID *string `json:"hardware_agent_bot_id"` HardwareAgentTransport *string `json:"hardware_agent_transport"` HardwareAgentMinimumSDKVersion *string `json:"hardware_agent_minimum_sdk_version"` } if !decodeWebJSON(w, r, &body, "设备修改参数不正确") { return } if (body.HouseholdID != nil && *body.HouseholdID != "" && !validUUID(*body.HouseholdID)) || (body.SubjectID != nil && *body.SubjectID != "" && !validUUID(*body.SubjectID)) { writeValidation(w, r, "设备修改参数不正确", FieldError{Field: "assignment", Message: "家庭和长者必须是有效UUID"}) return } item, err := service.Update(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), expected, opsdevice.UpdateInput{ Model: body.Model, HardwareRevision: body.HardwareRevision, MinimumSecurityVersion: body.MinimumSecurityVersion, MinimumFirmwareVersion: body.MinimumFirmwareVersion, Status: body.Status, HouseholdID: body.HouseholdID, SubjectID: body.SubjectID, VoiceRuntime: body.VoiceRuntime, HardwareAgentBotID: body.HardwareAgentBotID, HardwareAgentTransport: body.HardwareAgentTransport, HardwareAgentMinimumSDKVersion: body.HardwareAgentMinimumSDKVersion, }) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("ETag", concurrency.FormatETag(item.Version)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func DeleteOpsDeviceHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } if !validUUID(r.PathValue("id")) { writeValidation(w, r, "设备编号不正确", FieldError{Field: "id", Message: "必须是UUID"}) return } expected, err := concurrency.ParseIfMatch(r) if err != nil { writeETagError(w, r, err) return } item, err := service.Delete(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), expected) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("ETag", concurrency.FormatETag(item.Version)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func RotateOpsDeviceCredentialHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } if !validUUID(r.PathValue("id")) { writeValidation(w, r, "设备编号不正确", FieldError{Field: "id", Message: "必须是UUID"}) return } item, err := service.RotateCredential(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context())) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusCreated, item) }) } func IssueOpsDeviceClaimHandler(service *opsdevice.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, tenantID, ok := opsDevicePrincipal(w, r) if !ok { return } if !validUUID(r.PathValue("id")) { writeValidation(w, r, "设备编号不正确", FieldError{Field: "id", Message: "必须是UUID"}) return } item, err := service.IssueDeviceClaim(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context())) if err != nil { writeOpsDeviceError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusCreated, item) }) } func opsDevicePrincipal(w http.ResponseWriter, r *http.Request) (principal identity.Principal, tenantID string, ok bool) { principal, ok = webPrincipal(w, r) if !ok { return principal, "", false } tenantID = strings.TrimSpace(r.Header.Get("X-Tenant-Id")) if !validUUID(tenantID) { writeValidation(w, r, "运维设备请求参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须提供有效的客户组织UUID"}) return principal, "", false } return principal, tenantID, true } func writeOpsDeviceError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, devicebinding.ErrConfirmationRequired): WriteError(w, r, http.StatusConflict, "DEVICE_CONFIRMATION_REQUIRED", "该设备使用生产凭据和按键确认流程,请使用首次接入管理入口") case errors.Is(err, opsdevice.ErrInvalid): writeValidation(w, r, "设备管理参数不正确", FieldError{Field: "device", Message: "设备字段或家庭/长者归属不正确"}) case errors.Is(err, opsdevice.ErrAccessDenied): WriteError(w, r, http.StatusForbidden, "OPS_ACCESS_DENIED", "仅独立运维账号可以管理设备") case errors.Is(err, opsdevice.ErrNotFound): WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "设备不存在") case errors.Is(err, opsdevice.ErrVersionConflict): WriteError(w, r, http.StatusPreconditionFailed, "VERSION_CONFLICT", "设备信息已变化,请刷新后重试") case errors.Is(err, opsdevice.ErrClaimUnavailable): WriteError(w, r, http.StatusConflict, "DEVICE_CLAIM_UNAVAILABLE", "只有已登记且尚未绑定的设备可以生成绑定码") case errors.Is(err, opsdevice.ErrActivationConflict): WriteError(w, r, http.StatusConflict, "DEVICE_ACTIVATION_CONFLICT", "设备激活状态已变化或不允许执行该迁移") case errors.Is(err, opsdevice.ErrActivationNotReady): WriteError(w, r, http.StatusConflict, "DEVICE_ACTIVATION_NOT_READY", "设备绑定或运行凭据尚未就绪,不能进入 ACTIVE") case errors.Is(err, opsdevice.ErrProvisioningUnavailable): WriteError(w, r, http.StatusServiceUnavailable, "DEVICE_PROVISIONING_UNAVAILABLE", "设备烧录配置未就绪,请联系平台管理员") default: WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "设备管理操作失败") } }