2026-08-29 21:56:51 +08:00
|
|
|
package httpapi
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"data-terminal-cloud/internal/concurrency"
|
|
|
|
|
"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 {
|
2026-09-06 17:04:11 +08:00
|
|
|
Model string `json:"model"`
|
|
|
|
|
HardwareRevision string `json:"hardware_revision"`
|
|
|
|
|
MinimumSecurityVersion string `json:"minimum_security_version"`
|
|
|
|
|
MinimumFirmwareVersion string `json:"minimum_firmware_version"`
|
|
|
|
|
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"`
|
|
|
|
|
InitialActivationState string `json:"activation_state"`
|
2026-08-29 21:56:51 +08:00
|
|
|
}
|
|
|
|
|
if !decodeWebJSON(w, r, &body, "设备登记参数不正确") {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (body.HouseholdID != "" && !validUUID(body.HouseholdID)) || (body.SubjectID != "" && !validUUID(body.SubjectID)) {
|
|
|
|
|
writeValidation(w, r, "设备登记参数不正确", FieldError{Field: "assignment", Message: "家庭和长者必须是有效UUID"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
result, err := service.Create(r.Context(), principal, tenantID, RequestID(r.Context()), opsdevice.CreateInput{
|
|
|
|
|
Model: body.Model, HardwareRevision: body.HardwareRevision, MinimumSecurityVersion: body.MinimumSecurityVersion,
|
|
|
|
|
MinimumFirmwareVersion: body.MinimumFirmwareVersion, HouseholdID: body.HouseholdID, SubjectID: body.SubjectID,
|
2026-09-06 17:04:11 +08:00
|
|
|
VoiceRuntime: body.VoiceRuntime, HardwareAgentBotID: body.HardwareAgentBotID, HardwareAgentTransport: body.HardwareAgentTransport,
|
|
|
|
|
HardwareAgentMinimumSDKVersion: body.HardwareAgentMinimumSDKVersion,
|
|
|
|
|
InitialActivationState: body.InitialActivationState,
|
2026-08-29 21:56:51 +08:00
|
|
|
})
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 17:04:11 +08:00
|
|
|
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})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 21:56:51 +08:00
|
|
|
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 {
|
2026-09-06 17:04:11 +08:00
|
|
|
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"`
|
2026-08-29 21:56:51 +08:00
|
|
|
}
|
|
|
|
|
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,
|
2026-09-06 17:04:11 +08:00
|
|
|
VoiceRuntime: body.VoiceRuntime, HardwareAgentBotID: body.HardwareAgentBotID, HardwareAgentTransport: body.HardwareAgentTransport,
|
|
|
|
|
HardwareAgentMinimumSDKVersion: body.HardwareAgentMinimumSDKVersion,
|
2026-08-29 21:56:51 +08:00
|
|
|
})
|
|
|
|
|
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, 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", "只有已登记且尚未绑定的设备可以生成绑定码")
|
2026-09-06 17:04:11 +08:00
|
|
|
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")
|
2026-08-29 21:56:51 +08:00
|
|
|
default:
|
|
|
|
|
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "设备管理操作失败")
|
|
|
|
|
}
|
|
|
|
|
}
|