package web import ( "encoding/json" "errors" "net/http" "strings" "ymhut-box/server/unified-management/internal/db" ) func (r *router) handleAdminSources(w http.ResponseWriter, req *http.Request) { path := cleanPath(req.URL.Path) switch { case req.Method == http.MethodGet && path == "/api/admin/sources": catalog, err := r.sources.Catalog(true) if err != nil { writeError(w, http.StatusInternalServerError, "SOURCES_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "catalog": catalog}) case req.Method == http.MethodPost && path == "/api/admin/sources/import-media-types": if err := r.sources.ImportLegacyMediaTypes(req.Context()); err != nil { writeError(w, http.StatusInternalServerError, "IMPORT_FAILED", err) return } _ = r.sources.PublishLegacyMediaTypes(req.Context(), "admin") writeJSON(w, http.StatusOK, map[string]any{"ok": true}) case req.Method == http.MethodPost && path == "/api/admin/sources/check": job := r.sources.QueueCheckAll() writeJSON(w, http.StatusOK, map[string]any{"ok": true, "queued": true, "jobId": job.ID, "job": job}) case req.Method == http.MethodGet && path == "/api/admin/sources/check/status": writeJSON(w, http.StatusOK, map[string]any{"ok": true, "items": r.sources.CheckJobs()}) case req.Method == http.MethodGet && strings.HasPrefix(path, "/api/admin/sources/check/status/"): jobID := strings.TrimPrefix(path, "/api/admin/sources/check/status/") if job, ok := r.sources.CheckJob(jobID); ok { writeJSON(w, http.StatusOK, map[string]any{"ok": true, "job": job}) return } writeError(w, http.StatusNotFound, "CHECK_JOB_NOT_FOUND", errors.New("check job not found")) case req.Method == http.MethodPost && strings.HasPrefix(path, "/api/admin/sources/") && strings.HasSuffix(path, "/check"): sourceID := strings.TrimSuffix(strings.TrimPrefix(path, "/api/admin/sources/"), "/check") item, err := r.sources.CheckSourceID(req.Context(), sourceID) if err != nil { writeError(w, http.StatusBadRequest, "CHECK_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "source": item}) case (req.Method == http.MethodPost || req.Method == http.MethodPut) && path == "/api/admin/sources": item, err := r.decodeAdminSource(req) if err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } saved, err := r.store.UpsertSource(item) if err != nil { writeError(w, http.StatusInternalServerError, "SOURCE_SAVE_FAILED", err) return } _ = r.sources.PublishLegacyMediaTypes(req.Context(), "admin") _ = r.releases.PublishLegacyUpdateInfo(req, "admin") _ = r.store.InsertAudit(db.AuditLog{Actor: "admin", Type: "source.saved", Target: saved.SourceID, Message: "客户端接口已保存并同步兼容 media-types.json", IP: req.RemoteAddr, UserAgent: req.UserAgent()}) writeJSON(w, http.StatusOK, map[string]any{"ok": true, "source": saved}) case req.Method == http.MethodDelete && strings.HasPrefix(path, "/api/admin/sources/"): sourceID := strings.TrimPrefix(path, "/api/admin/sources/") if err := r.sources.DeleteSourceAndPublishCompatibility(req.Context(), sourceID, "admin"); err != nil { writeError(w, http.StatusInternalServerError, "SOURCE_DELETE_FAILED", err) return } _ = r.releases.PublishLegacyUpdateInfo(req, "admin") writeJSON(w, http.StatusOK, map[string]any{"ok": true}) default: http.NotFound(w, req) } } type adminSourceRequest struct { ID int64 `json:"id"` CategoryID string `json:"categoryId"` CategoryIDAlt string `json:"category_id"` CategoryName string `json:"categoryName"` CategoryNameAlt string `json:"category_name"` SourceID string `json:"sourceId"` SourceIDAlt string `json:"source_id"` Name string `json:"name"` Description string `json:"description"` Method string `json:"method"` APIURL string `json:"apiUrl"` APIURLAlt string `json:"api_url"` URLTemplate string `json:"urlTemplate"` URLTemplateAlt string `json:"url_template"` ThumbnailURL string `json:"thumbnailUrl"` ThumbnailURLAlt string `json:"thumbnail_url"` ProxyMode string `json:"proxyMode"` ProxyModeAlt string `json:"proxy_mode"` TimeoutMS int `json:"timeoutMs"` TimeoutMSAlt int `json:"timeout_ms"` RetryCount int `json:"retryCount"` RetryCountAlt int `json:"retry_count"` CacheSeconds int `json:"cacheSeconds"` CacheSecondsAlt int `json:"cache_seconds"` CheckIntervalSec int `json:"checkIntervalSec"` CheckIntervalSecAlt int `json:"check_interval_sec"` Enabled *bool `json:"enabled"` ClientVisible *bool `json:"clientVisible"` ClientVisibleAlt *bool `json:"client_visible"` SupportedFormats []string `json:"supportedFormats"` SupportedFormatsAlt []string `json:"supported_formats"` LastStatus string `json:"lastStatus"` LastStatusAlt string `json:"last_status"` LastLatencyMS int `json:"lastLatencyMs"` LastLatencyMSAlt int `json:"last_latency_ms"` LastCheckedAt string `json:"lastCheckedAt"` LastCheckedAtAlt string `json:"last_checked_at"` LastError string `json:"lastError"` LastErrorAlt string `json:"last_error"` ConsecutiveFailure int `json:"consecutiveFailure"` ConsecutiveFailAlt int `json:"consecutive_failure"` } func (r *router) decodeAdminSource(req *http.Request) (db.Source, error) { var body adminSourceRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return db.Source{}, err } sourceID := firstNonEmpty(body.SourceID, body.SourceIDAlt) var existing db.Source hasExisting := false if sourceID != "" { if item, err := r.store.GetSourceBySourceID(sourceID); err == nil { existing = item hasExisting = true } } item := existing item.ID = body.ID item.CategoryID = firstNonEmpty(body.CategoryID, body.CategoryIDAlt, item.CategoryID) item.CategoryName = firstNonEmpty(body.CategoryName, body.CategoryNameAlt, item.CategoryName) item.SourceID = firstNonEmpty(sourceID, item.SourceID) item.Name = firstNonEmpty(body.Name, item.Name) item.Description = body.Description item.Method = firstNonEmpty(body.Method, item.Method) item.APIURL = firstNonEmpty(body.APIURL, body.APIURLAlt, item.APIURL) item.URLTemplate = firstNonEmpty(body.URLTemplate, body.URLTemplateAlt, item.URLTemplate, item.APIURL) item.ThumbnailURL = firstNonEmpty(body.ThumbnailURL, body.ThumbnailURLAlt) item.ProxyMode = firstNonEmpty(body.ProxyMode, body.ProxyModeAlt, item.ProxyMode) item.TimeoutMS = firstPositive(body.TimeoutMS, body.TimeoutMSAlt, item.TimeoutMS) item.RetryCount = firstPositive(body.RetryCount, body.RetryCountAlt, item.RetryCount) item.CacheSeconds = firstPositive(body.CacheSeconds, body.CacheSecondsAlt, item.CacheSeconds) item.CheckIntervalSec = firstPositive(body.CheckIntervalSec, body.CheckIntervalSecAlt, item.CheckIntervalSec) item.LastStatus = firstNonEmpty(body.LastStatus, body.LastStatusAlt, item.LastStatus) item.LastLatencyMS = firstPositive(body.LastLatencyMS, body.LastLatencyMSAlt, item.LastLatencyMS) item.LastCheckedAt = firstNonEmpty(body.LastCheckedAt, body.LastCheckedAtAlt, item.LastCheckedAt) item.LastError = firstNonEmpty(body.LastError, body.LastErrorAlt, item.LastError) item.ConsecutiveFailure = firstPositive(body.ConsecutiveFailure, body.ConsecutiveFailAlt, item.ConsecutiveFailure) if formats := firstNonEmptyStringSlice(body.SupportedFormats, body.SupportedFormatsAlt); len(formats) > 0 { data, _ := json.Marshal(formats) item.SupportedFormats = string(data) } if body.Enabled != nil { item.Enabled = *body.Enabled item.EnabledSet = true } else if hasExisting { item.Enabled = existing.Enabled } visible := body.ClientVisible if visible == nil { visible = body.ClientVisibleAlt } if visible != nil { item.ClientVisible = *visible item.ClientVisibleSet = true } else if hasExisting { item.ClientVisible = existing.ClientVisible } return item, nil } func firstNonEmptyStringSlice(values ...[]string) []string { for _, value := range values { if len(value) > 0 { return value } } return nil }