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

127 lines
3.9 KiB
Go

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)
}