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

118 lines
3.7 KiB
Go

package area
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"time"
"yovision.local/sense/app/sense/admission"
"yovision.local/sense/app/sense/identity"
)
type Service struct {
store Store
profile func(string, string) (admission.Profile, error)
now func() time.Time
}
func NewService(store Store) *Service {
return &Service{store: store, profile: admission.VerifiedProfile, now: time.Now}
}
func (s *Service) Create(ctx context.Context, actor identity.Principal, deviceID, profileToken, name, kind, direction string, points []Point) (Area, error) {
profile, err := s.profile(deviceID, profileToken)
if err != nil {
return Area{}, err
}
if err := ValidateGeometry(kind, direction, points); err != nil {
return Area{}, err
}
name = strings.TrimSpace(name)
if name == "" {
return Area{}, fmt.Errorf("名称不能为空")
}
now := s.now().UTC()
item := Area{ID: newID(), DeviceID: deviceID, ProfileToken: profileToken, Width: profile.Width, Height: profile.Height, Name: name, Kind: kind, Direction: direction, Points: points, Enabled: true, Version: 1, CreatedAt: now, UpdatedAt: now}
if err := s.store.Save(ctx, item, 0); err != nil {
return Area{}, err
}
identity.RecordAudit(ctx, actor.UserID, "area.create", item.ID, "success", map[string]any{"version": 1, "kind": kind})
return item, nil
}
func (s *Service) Update(ctx context.Context, actor identity.Principal, id, name, direction string, points []Point, expected int64, recalibrate bool) (Area, error) {
current, err := s.store.Get(ctx, id)
if err != nil {
return Area{}, err
}
if current.Version != expected {
return Area{}, ErrConflict
}
profile, err := s.profile(current.DeviceID, current.ProfileToken)
if err != nil {
return Area{}, err
}
resolutionChanged := profile.Width != current.Width || profile.Height != current.Height
if resolutionChanged && !recalibrate {
current.RecalibrationRequired = true
current.Version++
current.UpdatedAt = s.now().UTC()
if err := s.store.Save(ctx, current, expected); err != nil {
return Area{}, err
}
return current, nil
}
if err := ValidateGeometry(current.Kind, direction, points); err != nil {
return Area{}, err
}
current.Name = strings.TrimSpace(name)
current.Direction = direction
current.Points = points
current.Width = profile.Width
current.Height = profile.Height
current.RecalibrationRequired = false
current.Version++
current.UpdatedAt = s.now().UTC()
if err := s.store.Save(ctx, current, expected); err != nil {
return Area{}, err
}
identity.RecordAudit(ctx, actor.UserID, "area.publish", id, "success", map[string]any{"version": current.Version, "recalibrated": recalibrate})
return current, nil
}
func (s *Service) Disable(ctx context.Context, actor identity.Principal, id string, expected int64) (Area, error) {
item, err := s.store.Get(ctx, id)
if err != nil {
return Area{}, err
}
if item.Version != expected {
return Area{}, ErrConflict
}
item.Enabled = false
item.Version++
item.UpdatedAt = s.now().UTC()
if err := s.store.Save(ctx, item, expected); err != nil {
return Area{}, err
}
identity.RecordAudit(ctx, actor.UserID, "area.disable", id, "success", map[string]any{"version": item.Version})
return item, nil
}
func (s *Service) List(ctx context.Context, deviceID string) ([]Area, error) {
items, err := s.store.List(ctx, deviceID)
if err != nil {
return nil, err
}
for i := range items {
if profile, profileErr := s.profile(items[i].DeviceID, items[i].ProfileToken); profileErr == nil && (profile.Width != items[i].Width || profile.Height != items[i].Height) {
items[i].RecalibrationRequired = true
}
}
return items, nil
}
func newID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return "area_" + hex.EncodeToString(b)
}