package httpapi import ( "net/http" "strings" "time" "data-terminal-cloud/internal/concurrency" "data-terminal-cloud/internal/identity" "data-terminal-cloud/internal/opsconfig" ) func CreateOpsResourceReferenceHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var body struct { ResourceType string `json:"resource_type"` ReferenceName string `json:"reference_name"` DisplayName string `json:"display_name"` VerifiedAt time.Time `json:"verified_at"` ExpiresAt *time.Time `json:"expires_at"` } if !decodeWebJSON(w, r, &body, "运营资源引用参数不正确") { return } item, created, err := service.CreateResourceReference(r.Context(), principal, tenantID, key, RequestID(r.Context()), body.ResourceType, body.ReferenceName, body.DisplayName, body.VerifiedAt, body.ExpiresAt) if err != nil { writeOpsConfigError(w, r, err) return } status := http.StatusCreated if !created { status = http.StatusOK w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, status, item) }) } func ListOpsResourceReferencesHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListResourceReferences(r.Context(), principal, tenantID) if err != nil { writeOpsConfigError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func RevokeOpsResourceReferenceHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } expected, err := concurrency.ParseIfMatch(r) if err != nil { writeETagError(w, r, err) return } var body struct { Reason string `json:"reason"` } if !decodeWebJSON(w, r, &body, "资源引用撤销参数不正确") { return } item, err := service.RevokeResourceReference(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), body.Reason, expected) if err != nil { writeOpsConfigError(w, r, err) return } w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func CreateOpsConfigEvaluationHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var body struct { EvidenceType string `json:"evidence_type"` SuiteVersion string `json:"suite_version"` Result string `json:"result"` Metrics map[string]any `json:"metrics"` ReportRef string `json:"report_ref"` } if !decodeWebJSON(w, r, &body, "离线评测参数不正确") { return } item, created, err := service.CreateEvaluation(r.Context(), principal, tenantID, r.PathValue("id"), key, RequestID(r.Context()), body.EvidenceType, body.SuiteVersion, body.Result, body.ReportRef, body.Metrics) if err != nil { writeOpsConfigError(w, r, err) return } status := http.StatusCreated if !created { status = http.StatusOK w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, status, item) }) } func ListOpsConfigEvaluationsHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListEvaluations(r.Context(), principal, tenantID, r.PathValue("id")) if err != nil { writeOpsConfigError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func CreateOpsDeviceGroupHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var body struct { Name string `json:"name"` Selector map[string]any `json:"selector"` DeviceIDs []string `json:"device_ids"` } if !decodeWebJSON(w, r, &body, "设备分组参数不正确") { return } item, created, err := service.CreateDeviceGroup(r.Context(), principal, tenantID, key, RequestID(r.Context()), body.Name, body.Selector, body.DeviceIDs) if err != nil { writeOpsConfigError(w, r, err) return } status := http.StatusCreated if !created { status = http.StatusOK w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, status, item) }) } func ListOpsDeviceGroupsHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListDeviceGroups(r.Context(), principal, tenantID) if err != nil { writeOpsConfigError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func ListOpsConfigRolloutsHandler(service *opsconfig.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListRollouts(r.Context(), principal, tenantID, r.URL.Query().Get("config_key")) if err != nil { writeOpsConfigError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func PauseOpsConfigRolloutHandler(service *opsconfig.Service) http.Handler { return opsConfigRolloutTransitionHandler(func(r *http.Request, principal identity.Principal, tenantID, reason string, expected int64) (opsconfig.Rollout, error) { return service.PauseRollout(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), reason, expected) }) } func ResumeOpsConfigRolloutHandler(service *opsconfig.Service) http.Handler { return opsConfigRolloutTransitionHandler(func(r *http.Request, principal identity.Principal, tenantID, reason string, expected int64) (opsconfig.Rollout, error) { return service.ResumeRollout(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), reason, expected) }) } func opsConfigRolloutTransitionHandler(transition func(*http.Request, identity.Principal, string, string, int64) (opsconfig.Rollout, error)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := resourcePrincipal(w, r, "id") if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } expected, err := concurrency.ParseIfMatch(r) if err != nil { writeETagError(w, r, err) return } var body struct { Reason string `json:"reason"` } if !decodeWebJSON(w, r, &body, "配置投放状态变更参数不正确") { return } item, err := transition(r, principal, tenantID, body.Reason, expected) if err != nil { writeOpsConfigError(w, r, err) return } w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion)) w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func opsTenantHeader(w http.ResponseWriter, r *http.Request) (string, bool) { tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id")) if !validUUID(tenantID) { writeValidation(w, r, "运营范围参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须是有效UUID"}) return "", false } return tenantID, true } func opsCreateHeaders(w http.ResponseWriter, r *http.Request) (string, string, bool) { tenantID, ok := opsTenantHeader(w, r) if !ok { return "", "", false } 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 "", "", false } return tenantID, key, true }