继续更新 update 门户站点界面和功能
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -22,6 +23,25 @@ type Service struct {
|
||||
client *http.Client
|
||||
stop chan struct{}
|
||||
once sync.Once
|
||||
mu sync.RWMutex
|
||||
jobs map[string]CheckJob
|
||||
events chan Event
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
type CheckJob struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
FinishedAt string `json:"finishedAt"`
|
||||
Total int `json:"total"`
|
||||
Checked int `json:"checked"`
|
||||
Stats map[string]int `json:"stats"`
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
type legacyMedia struct {
|
||||
@@ -52,6 +72,8 @@ func NewService(cfg *config.Config, store *db.Store) *Service {
|
||||
store: store,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
stop: make(chan struct{}),
|
||||
jobs: map[string]CheckJob{},
|
||||
events: make(chan Event, 32),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,21 +259,115 @@ func (s *Service) CheckDue(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) QueueCheckAll() CheckJob {
|
||||
items, err := s.store.ListSources(true)
|
||||
if err != nil {
|
||||
job := CheckJob{ID: newJobID(), Status: "failed", StartedAt: time.Now().UTC().Format(time.RFC3339), FinishedAt: time.Now().UTC().Format(time.RFC3339), Stats: map[string]int{}, LastError: err.Error()}
|
||||
s.saveJob(job)
|
||||
return job
|
||||
}
|
||||
filtered := make([]db.Source, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Enabled {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
job := CheckJob{ID: newJobID(), Status: "running", StartedAt: time.Now().UTC().Format(time.RFC3339), Total: len(filtered), Stats: map[string]int{"ok": 0, "redirected": 0, "degraded": 0, "error": 0}}
|
||||
s.saveJob(job)
|
||||
go s.runCheckJob(context.Background(), job.ID, filtered)
|
||||
return job
|
||||
}
|
||||
|
||||
func (s *Service) CheckJobs() []CheckJob {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
items := make([]CheckJob, 0, len(s.jobs))
|
||||
for _, item := range s.jobs {
|
||||
items = append(items, item)
|
||||
}
|
||||
sortJobs(items)
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *Service) CheckJob(id string) (CheckJob, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
item, ok := s.jobs[id]
|
||||
return item, ok
|
||||
}
|
||||
|
||||
func (s *Service) Events() <-chan Event {
|
||||
return s.events
|
||||
}
|
||||
|
||||
func (s *Service) runCheckJob(ctx context.Context, id string, items []db.Source) {
|
||||
if len(items) == 0 {
|
||||
s.updateJob(id, func(job *CheckJob) {
|
||||
job.Status = "completed"
|
||||
job.FinishedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
})
|
||||
s.emit("source_check.completed", map[string]any{"jobId": id})
|
||||
return
|
||||
}
|
||||
const concurrency = 4
|
||||
work := make(chan db.Source)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range work {
|
||||
status, err := s.CheckOneStatus(ctx, item)
|
||||
if err != nil && status == "" {
|
||||
status = "error"
|
||||
}
|
||||
s.updateJob(id, func(job *CheckJob) {
|
||||
job.Checked++
|
||||
if job.Stats == nil {
|
||||
job.Stats = map[string]int{}
|
||||
}
|
||||
job.Stats[status]++
|
||||
if err != nil {
|
||||
job.LastError = err.Error()
|
||||
}
|
||||
})
|
||||
s.emit("source_check.progress", map[string]any{"jobId": id, "sourceId": item.SourceID, "status": status})
|
||||
}
|
||||
}()
|
||||
}
|
||||
for _, item := range items {
|
||||
work <- item
|
||||
}
|
||||
close(work)
|
||||
wg.Wait()
|
||||
s.updateJob(id, func(job *CheckJob) {
|
||||
job.Status = "completed"
|
||||
job.FinishedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
})
|
||||
s.emit("source_check.completed", map[string]any{"jobId": id})
|
||||
}
|
||||
|
||||
func (s *Service) CheckSourceID(ctx context.Context, sourceID string) (db.Source, error) {
|
||||
item, err := s.store.GetSourceBySourceID(sourceID)
|
||||
if err != nil {
|
||||
return db.Source{}, err
|
||||
}
|
||||
return item, s.CheckOne(ctx, item)
|
||||
_, err = s.CheckOneStatus(ctx, item)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Service) CheckOne(ctx context.Context, item db.Source) error {
|
||||
_, err := s.CheckOneStatus(ctx, item)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) CheckOneStatus(ctx context.Context, item db.Source) (string, error) {
|
||||
if strings.TrimSpace(item.APIURL) == "" {
|
||||
return errors.New("source api_url is empty")
|
||||
return "error", errors.New("source api_url is empty")
|
||||
}
|
||||
timeout := time.Duration(item.TimeoutMS) * time.Millisecond
|
||||
if timeout <= 0 {
|
||||
timeout = 8 * time.Second
|
||||
if timeout <= 0 || timeout < 15*time.Second {
|
||||
timeout = 15 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
@@ -262,7 +378,7 @@ func (s *Service) CheckOne(ctx context.Context, item db.Source) error {
|
||||
req, err := http.NewRequestWithContext(ctx, method, item.APIURL, nil)
|
||||
if err != nil {
|
||||
_ = s.store.RecordSourceCheck(item.ID, "error", 0, err.Error())
|
||||
return err
|
||||
return "error", err
|
||||
}
|
||||
redirects := []string{}
|
||||
client := *s.client
|
||||
@@ -282,7 +398,7 @@ func (s *Service) CheckOne(ctx context.Context, item db.Source) error {
|
||||
latency := int(time.Since(start).Milliseconds())
|
||||
if err != nil {
|
||||
_ = s.store.RecordSourceCheck(item.ID, "error", latency, err.Error())
|
||||
return err
|
||||
return "error", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
status := "ok"
|
||||
@@ -306,7 +422,47 @@ func (s *Service) CheckOne(ctx context.Context, item db.Source) error {
|
||||
"error": resp.Status,
|
||||
})
|
||||
}
|
||||
return s.store.RecordSourceCheck(item.ID, status, latency, message)
|
||||
if err := s.store.RecordSourceCheck(item.ID, status, latency, message); err != nil {
|
||||
return status, err
|
||||
}
|
||||
s.emit("source_check.item", map[string]any{"sourceId": item.SourceID, "status": status, "latencyMs": latency})
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (s *Service) saveJob(job CheckJob) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.jobs[job.ID] = job
|
||||
}
|
||||
|
||||
func (s *Service) updateJob(id string, mutate func(*CheckJob)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
job := s.jobs[id]
|
||||
mutate(&job)
|
||||
s.jobs[id] = job
|
||||
}
|
||||
|
||||
func (s *Service) emit(kind string, data map[string]any) {
|
||||
event := Event{Type: kind, Data: data}
|
||||
select {
|
||||
case s.events <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func newJobID() string {
|
||||
return fmt.Sprintf("check-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func sortJobs(items []CheckJob) {
|
||||
for i := 0; i < len(items); i++ {
|
||||
for j := i + 1; j < len(items); j++ {
|
||||
if items[j].StartedAt > items[i].StartedAt {
|
||||
items[i], items[j] = items[j], items[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isHTTPURL(value *url.URL) bool {
|
||||
|
||||
Reference in New Issue
Block a user