ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
276 lines
10 KiB
Go
276 lines
10 KiB
Go
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", "设备管理操作失败")
|
|
}
|
|
}
|