package device import ( "context" "crypto/rand" "encoding/hex" "fmt" "strings" "time" "yovision.local/sense/app/sense/identity" ) type Service struct { store Store vault *CredentialVault now func() time.Time } func NewService(store Store, vault *CredentialVault) *Service { return &Service{store: store, vault: vault, now: time.Now} } func (s *Service) Create(ctx context.Context, actor identity.Principal, name, location, modality string, capabilities []string) (Device, error) { name = strings.TrimSpace(name) if name == "" { return Device{}, fmt.Errorf("设备名称不能为空") } if modality == "" { modality = ModalityVideo } adapter := AdapterNotReady if modality == ModalityVideo { adapter = AdapterReady } now := s.now().UTC() item := Device{ID: newID(), Name: name, Location: strings.TrimSpace(location), Modality: modality, Capabilities: capabilities, Status: StatusPending, AdapterStatus: adapter, Version: 1, CreatedAt: now, UpdatedAt: now} if err := s.store.Create(ctx, item); err != nil { return Device{}, err } identity.RecordAudit(ctx, actor.UserID, "device.create", item.ID, "success", map[string]any{"modality": modality}) return item, nil } func (s *Service) Get(ctx context.Context, id string) (Device, error) { item, err := s.store.Get(ctx, id) if err == nil { item.CredentialConfigured = len(item.CredentialCiphertext) > 0 item.CredentialCiphertext = nil } return item, err } func (s *Service) List(ctx context.Context, filter ListFilter) (Page, error) { page, err := s.store.List(ctx, filter) for i := range page.Items { page.Items[i].CredentialCiphertext = nil } return page, err } func (s *Service) Update(ctx context.Context, actor identity.Principal, id, name, location string, capabilities []string, expected int64) (Device, error) { item, err := s.store.Get(ctx, id) if err != nil { return Device{}, err } item.Name = strings.TrimSpace(name) item.Location = strings.TrimSpace(location) item.Capabilities = capabilities item.Version++ item.UpdatedAt = s.now().UTC() if item.Name == "" { return Device{}, fmt.Errorf("设备名称不能为空") } if err := s.store.Update(ctx, item, expected); err != nil { return Device{}, err } identity.RecordAudit(ctx, actor.UserID, "device.update", id, "success", map[string]any{"version": item.Version}) item.CredentialConfigured = len(item.CredentialCiphertext) > 0 item.CredentialCiphertext = nil return item, nil } func (s *Service) SetCredential(ctx context.Context, actor identity.Principal, id, username, password string) (Device, error) { if strings.TrimSpace(username) == "" || password == "" { return Device{}, fmt.Errorf("用户名和密码不能为空") } item, err := s.store.Get(ctx, id) if err != nil { return Device{}, err } ciphertext, err := s.vault.Encrypt(username, password) if err != nil { return Device{}, err } expected := item.Version item.CredentialCiphertext = ciphertext item.CredentialConfigured = true item.Version++ item.UpdatedAt = s.now().UTC() if err := s.store.Update(ctx, item, expected); err != nil { return Device{}, err } identity.RecordAudit(ctx, actor.UserID, "device.credential.update", id, "success", map[string]any{"configured": true}) item.CredentialCiphertext = nil return item, nil } func (s *Service) Disable(ctx context.Context, actor identity.Principal, id string, expected int64) (Device, error) { item, err := s.store.Get(ctx, id) if err != nil { return Device{}, err } item.Status = StatusDisabled item.Version++ item.UpdatedAt = s.now().UTC() if err := s.store.Update(ctx, item, expected); err != nil { return Device{}, err } identity.RecordAudit(ctx, actor.UserID, "device.disable", id, "success", nil) item.CredentialConfigured = len(item.CredentialCiphertext) > 0 item.CredentialCiphertext = nil return item, nil } func newID() string { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { panic(err) } return "dev_" + hex.EncodeToString(b) }