Files
yovision/Sense/server/app/sense/admission/service.go
T

120 lines
4.4 KiB
Go

package admission
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
"yovision.local/sense/app/sense/adapters/onvif"
"yovision.local/sense/app/sense/adapters/rtsp"
"yovision.local/sense/app/sense/device"
"yovision.local/sense/app/sense/identity"
)
type Profile struct {
Token string `json:"token"`
Name string `json:"name"`
Width int `json:"width"`
Height int `json:"height"`
Encoding string `json:"encoding"`
StreamURI string `json:"stream_uri"`
Kind string `json:"kind"`
Verification rtsp.Result `json:"verification"`
}
type Result struct {
DeviceID string `json:"device_id"`
Address string `json:"address"`
Status string `json:"status"`
Detail string `json:"detail"`
Profiles []Profile `json:"profiles"`
CheckedAt time.Time `json:"checked_at"`
}
type Service struct {
onvif onvif.Client
rtsp rtsp.Verifier
discoveryIP string
discoveryTimeout time.Duration
mu sync.RWMutex
results map[string]Result
now func() time.Time
}
func NewService(client onvif.Client, verifier rtsp.Verifier, discoveryIP string) *Service {
return &Service{onvif: client, rtsp: verifier, discoveryIP: discoveryIP, discoveryTimeout: 3 * time.Second, results: map[string]Result{}, now: time.Now}
}
func (s *Service) Discover(ctx context.Context) ([]string, error) {
if strings.TrimSpace(s.discoveryIP) == "" {
return nil, fmt.Errorf("discovery_not_configured")
}
return onvif.Discover(ctx, s.discoveryIP, s.discoveryTimeout)
}
func (s *Service) Probe(ctx context.Context, actor identity.Principal, deviceID, address string) (Result, error) {
credential, err := device.ReadCredential(ctx, deviceID)
if err != nil {
return Result{}, fmt.Errorf("credential_required")
}
profiles, err := s.onvif.Profiles(ctx, address, onvif.Credential{Username: credential.Username, Password: credential.Password})
if err != nil {
status, detail := classify(err)
result := Result{DeviceID: deviceID, Address: address, Status: status, Detail: detail, CheckedAt: s.now().UTC()}
s.save(result)
identity.RecordAudit(ctx, actor.UserID, "admission.probe", deviceID, "failure", map[string]any{"status": status})
return result, nil
}
items := make([]Profile, 0, len(profiles))
for _, profile := range profiles {
verification, verifyErr := s.rtsp.Verify(ctx, profile.StreamURI, rtsp.Credential{Username: credential.Username, Password: credential.Password})
if verifyErr != nil {
verification = rtsp.Result{Status: "failed", Detail: "视频地址格式不正确"}
}
kind := "other"
items = append(items, Profile{Token: profile.Token, Name: profile.Name, Width: profile.Width, Height: profile.Height, Encoding: profile.Encoding, StreamURI: profile.StreamURI, Kind: kind, Verification: verification})
}
sort.Slice(items, func(i, j int) bool { return items[i].Width*items[i].Height > items[j].Width*items[j].Height })
if len(items) > 0 {
items[0].Kind = "main"
}
if len(items) > 1 {
items[len(items)-1].Kind = "sub"
}
status := "ready"
detail := "设备与视频 Profile 已验证"
for _, item := range items {
if item.Verification.Status != "ready" {
status = "profile_failed"
detail = "部分视频 Profile 验证失败"
}
}
result := Result{DeviceID: deviceID, Address: address, Status: status, Detail: detail, Profiles: items, CheckedAt: s.now().UTC()}
s.save(result)
identity.RecordAudit(ctx, actor.UserID, "admission.probe", deviceID, "success", map[string]any{"profile_count": len(items), "status": status})
return result, nil
}
func (s *Service) Get(deviceID string) (Result, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
value, ok := s.results[deviceID]
return value, ok
}
func (s *Service) save(result Result) {
s.mu.Lock()
defer s.mu.Unlock()
s.results[result.DeviceID] = result
}
func classify(err error) (string, string) {
value := strings.ToLower(err.Error())
switch {
case strings.Contains(value, "authentication"):
return "authentication_failed", "设备拒绝了当前凭据,请更新后重试"
case strings.Contains(value, "deadline") || strings.Contains(value, "timeout"):
return "timeout", "设备响应超时"
case strings.Contains(value, "clock") || strings.Contains(value, "time"):
return "clock_skew", "设备时间可能不准确,请校时后重试"
default:
return "unreachable", "无法读取设备信息,请检查地址和网络"
}
}