package service import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "sync" "time" "3588AdminBackend/internal/models" "3588AdminBackend/internal/storage" ) type ResourceManagementService struct { repo *storage.ResourcesRepo } type InstalledResourceStatus struct { Name string `json:"name"` ResourceType string `json:"resource_type"` SHA256 string `json:"sha256"` SizeBytes int64 `json:"size_bytes"` UpdatedAt int64 `json:"updated_at"` } type ResourceStatusCell struct { ResourceName string `json:"resource_name"` ResourceType string `json:"resource_type"` Status string `json:"status"` Version string `json:"version"` } type ResourceStatusRow struct { DeviceID string `json:"device_id"` DeviceName string `json:"device_name"` Online bool `json:"online"` Cells []ResourceStatusCell `json:"cells"` ExtraCount int `json:"extra_resource_count"` ExtraResources []InstalledResourceStatus `json:"extra_resources"` } type ResourceStatusSummary struct { StandardResources int `json:"standard_resources"` Devices int `json:"devices"` CompleteDevices int `json:"complete_devices"` MissingDevices int `json:"missing_devices"` MismatchDevices int `json:"mismatch_devices"` } type ResourceStatusBoard struct { Summary ResourceStatusSummary `json:"summary"` Rows []ResourceStatusRow `json:"rows"` } func NewResourceManagementService(repo *storage.ResourcesRepo) *ResourceManagementService { return &ResourceManagementService{repo: repo} } func (s *ResourceManagementService) SyncStandardResourcesFromDirectory(dir string) error { if s == nil || s.repo == nil { return fmt.Errorf("resources repo is not configured") } dir = filepath.Clean(strings.TrimSpace(dir)) if dir == "" { return fmt.Errorf("standard resources dir is empty") } if err := os.MkdirAll(dir, 0o755); err != nil { return err } entries, err := os.ReadDir(dir) if err != nil { return err } for _, entry := range entries { if entry.IsDir() { // Subdirectory = resource_type; scan files inside resourceType := entry.Name() subDir := filepath.Join(dir, resourceType) subEntries, serr := os.ReadDir(subDir) if serr != nil { continue } for _, sub := range subEntries { if sub.IsDir() { continue } fullPath := filepath.Join(subDir, sub.Name()) sum, size, err := hashFile(fullPath) if err != nil { return err } record := storage.StandardResourceRecord{ Name: strings.TrimSuffix(sub.Name(), filepath.Ext(sub.Name())), ResourceType: resourceType, Version: "auto", SHA256: sum, SizeBytes: size, FilePath: fullPath, } if err := s.repo.Save(record); err != nil { return err } } continue } // Flat file: infer resource_type from filename or leave empty (legacy) fullPath := filepath.Join(dir, entry.Name()) sum, size, err := hashFile(fullPath) if err != nil { return err } record := storage.StandardResourceRecord{ Name: strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())), Version: "auto", SHA256: sum, SizeBytes: size, FilePath: fullPath, } if err := s.repo.Save(record); err != nil { return err } } return nil } func FetchInstalledResourceStatuses(agent *AgentClient, dev *models.Device) ([]InstalledResourceStatus, error) { if agent == nil || dev == nil || strings.TrimSpace(dev.IP) == "" || dev.AgentPort <= 0 { return nil, nil } body, status, err := agent.Do("GET", dev.IP, dev.AgentPort, "/v1/resources/status", nil) if err != nil { return nil, err } if status != 200 { return nil, nil // agent may not implement this endpoint yet — return empty } var resp struct { Resources []InstalledResourceStatus `json:"resources"` } if err := json.Unmarshal(body, &resp); err != nil { return nil, err } return resp.Resources, nil } func BuildResourceStatusBoard(standardResources []storage.StandardResourceRecord, devices []*models.Device, installed map[string][]InstalledResourceStatus) ResourceStatusBoard { board := ResourceStatusBoard{ Summary: ResourceStatusSummary{ StandardResources: len(standardResources), Devices: len(devices), }, Rows: make([]ResourceStatusRow, 0, len(devices)), } for _, device := range devices { if device == nil { continue } index := make(map[string]InstalledResourceStatus, len(installed[device.DeviceID])) for _, item := range installed[device.DeviceID] { index[item.Name] = item } standardIndex := make(map[string]struct{}, len(standardResources)) for _, res := range standardResources { standardIndex[res.Name] = struct{}{} } row := ResourceStatusRow{ DeviceID: device.DeviceID, DeviceName: device.DisplayName(), Online: device.Online, Cells: make([]ResourceStatusCell, 0, len(standardResources)), ExtraResources: make([]InstalledResourceStatus, 0), } hasMissing := false hasMismatch := false for _, res := range standardResources { cell := ResourceStatusCell{ ResourceName: res.Name, ResourceType: res.ResourceType, Version: res.Version, Status: "missing", } if item, ok := index[res.Name]; ok { if strings.EqualFold(strings.TrimSpace(item.SHA256), strings.TrimSpace(res.SHA256)) { cell.Status = "ok" } else { cell.Status = "mismatch" hasMismatch = true } } else { hasMissing = true } if cell.Status == "missing" { hasMissing = true } row.Cells = append(row.Cells, cell) } for _, item := range installed[device.DeviceID] { if _, ok := standardIndex[item.Name]; ok { continue } row.ExtraResources = append(row.ExtraResources, item) } sort.Slice(row.ExtraResources, func(i, j int) bool { return row.ExtraResources[i].Name < row.ExtraResources[j].Name }) row.ExtraCount = len(row.ExtraResources) switch { case hasMismatch: board.Summary.MismatchDevices++ case hasMissing: board.Summary.MissingDevices++ default: board.Summary.CompleteDevices++ } board.Rows = append(board.Rows, row) } sort.SliceStable(board.Rows, func(i, j int) bool { return board.Rows[i].DeviceName < board.Rows[j].DeviceName }) return board } // FetchAndBuildResourceBoard 并发查询所有在线设备的资源状态并构建面板,总超时 5 秒。 func FetchAndBuildResourceBoard(agent *AgentClient, devices []*models.Device, standardResources []storage.StandardResourceRecord) ResourceStatusBoard { board := ResourceStatusBoard{ Summary: ResourceStatusSummary{ StandardResources: len(standardResources), Devices: len(devices), }, Rows: make([]ResourceStatusRow, 0, len(devices)), } if agent == nil || len(standardResources) == 0 { return board } installed := make(map[string][]InstalledResourceStatus) var mu sync.Mutex var wg sync.WaitGroup // 总超时:最多等 5 秒 done := make(chan struct{}) go func() { time.Sleep(5 * time.Second) close(done) }() for _, device := range devices { if device == nil || !device.Online { continue } wg.Add(1) go func(dev *models.Device) { defer wg.Done() items, err := FetchInstalledResourceStatuses(agent, dev) if err == nil { mu.Lock() installed[dev.DeviceID] = items mu.Unlock() } }(device) } // Wait with timeout waitCh := make(chan struct{}) go func() { wg.Wait() close(waitCh) }() select { case <-waitCh: case <-done: } return BuildResourceStatusBoard(standardResources, devices, installed) }