// Package device 定義 Device domain model 與 Repository 介面。 // // 對齊 database.md §2.2(雙狀態模型 — Minor-3)與 §3(Repository interface)。 // 雛形以 InMemoryRepository 實作;Phase 1 新增 PostgresRepository 取代。 package device import ( "context" "errors" "sync" "time" ) // ========================================================================== // Errors // ========================================================================== var ( // ErrNotFound 表示指定 ID 的 Device 不存在。 ErrNotFound = errors.New("device: not found") ) // ========================================================================== // Remote / USB 狀態常數(對齊 database.md §2.2) // ========================================================================== // RemoteStatus 是雲端對 tunnel 連線的觀察值。 type RemoteStatus = string const ( // RemoteStatusOnline 表示 tunnel 有效、雲端可達。 RemoteStatusOnline RemoteStatus = "online" // RemoteStatusOffline 表示 tunnel 斷線或從未連上。 RemoteStatusOffline RemoteStatus = "offline" // RemoteStatusReconnecting 表示 tunnel 短暫斷線、local agent 重連中。 RemoteStatusReconnecting RemoteStatus = "reconnecting" // RemoteStatusError 表示 tunnel 發生未預期錯誤(yamux 異常等)。 RemoteStatusError RemoteStatus = "error" ) // USBStatus 是 local agent 從 Kneron SDK 讀到的 USB 狀態。 type USBStatus = string const ( // USBStatusOnline USB 插著且可用。 USBStatusOnline USBStatus = "online" // USBStatusOffline USB 拔掉了。 USBStatusOffline USBStatus = "offline" // USBStatusUnknown 尚未回報 / 初始狀態。 USBStatusUnknown USBStatus = "unknown" ) // ========================================================================== // Device struct // ========================================================================== // Device 對應 database.md §2.2 的 Device 實體。 // // 雙狀態說明(Minor-3): // - Status(USB-level):local agent 觀察到的 USB 連接狀態 // - RemoteStatus(tunnel-level):雲端觀察到的 tunnel 連線狀態 // // 前端優先顯示 RemoteStatus,次要顯示 Status(見 TDD §10.5.1)。 type Device struct { ID string `json:"id"` OwnerUserID string `json:"ownerUserId"` Name string `json:"name"` DeviceType string `json:"deviceType"` SerialNumber string `json:"serialNumber,omitempty"` // tunnel-level 狀態 RemoteStatus RemoteStatus `json:"remoteStatus"` LastSeenAt *time.Time `json:"lastSeenAt,omitempty"` LastConnectedAt *time.Time `json:"lastConnectedAt,omitempty"` // USB-level 狀態 Status USBStatus `json:"status"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` PairedAt *time.Time `json:"pairedAt,omitempty"` DeletedAt *time.Time `json:"deletedAt,omitempty"` } // ========================================================================== // Repository interface // ========================================================================== // Repository 是 Device 持久層介面。 // // 所有查詢方法**必須略過 DeletedAt != nil 的紀錄**(soft delete)。 // Phase 1 的 PostgresRepository 會加上 `WHERE deleted_at IS NULL`。 type Repository interface { // Get 取得單一 device;不存在或已軟刪除回 ErrNotFound。 Get(ctx context.Context, id string) (*Device, error) // GetBySerial 以 (ownerUserID, serialNumber) 查詢(避免同 user 重複註冊同 serial)。 GetBySerial(ctx context.Context, ownerUserID, serial string) (*Device, error) // List 列出某 user 的所有(未刪除)device。 List(ctx context.Context, ownerUserID string) ([]*Device, error) // Save 新增或更新一筆 device(upsert 語意,by ID)。 // 實作應更新 UpdatedAt;若為新建則同時設定 CreatedAt。 Save(ctx context.Context, d *Device) error // Delete 標記為軟刪除(設定 DeletedAt)。 Delete(ctx context.Context, id string) error } // ========================================================================== // InMemoryRepository // ========================================================================== // InMemoryRepository 是 Phase 0 雛形的記憶體實作。 type InMemoryRepository struct { mu sync.RWMutex devices map[string]*Device } // NewInMemoryRepository 建立一個空的記憶體 Repository。 func NewInMemoryRepository() *InMemoryRepository { return &InMemoryRepository{ devices: make(map[string]*Device), } } // Get 取得單一 device。 func (r *InMemoryRepository) Get(ctx context.Context, id string) (*Device, error) { r.mu.RLock() defer r.mu.RUnlock() d, ok := r.devices[id] if !ok || d.DeletedAt != nil { return nil, ErrNotFound } cp := *d return &cp, nil } // GetBySerial 以 (owner, serial) 查詢。 func (r *InMemoryRepository) GetBySerial(ctx context.Context, ownerUserID, serial string) (*Device, error) { r.mu.RLock() defer r.mu.RUnlock() for _, d := range r.devices { if d.DeletedAt != nil { continue } if d.OwnerUserID == ownerUserID && d.SerialNumber == serial { cp := *d return &cp, nil } } return nil, ErrNotFound } // List 列出某 user 的所有未刪除 device。 func (r *InMemoryRepository) List(ctx context.Context, ownerUserID string) ([]*Device, error) { r.mu.RLock() defer r.mu.RUnlock() out := make([]*Device, 0) for _, d := range r.devices { if d.DeletedAt != nil { continue } if d.OwnerUserID == ownerUserID { cp := *d out = append(out, &cp) } } return out, nil } // Save 新增或更新 device(upsert by ID)。 func (r *InMemoryRepository) Save(ctx context.Context, d *Device) error { if d == nil || d.ID == "" { return errors.New("device: Save requires non-nil device with ID") } r.mu.Lock() defer r.mu.Unlock() now := time.Now().UTC() // Copy 避免外部後續修改影響 store cp := *d if existing, ok := r.devices[d.ID]; ok && existing.DeletedAt == nil { cp.CreatedAt = existing.CreatedAt // 保留原始 CreatedAt } else if cp.CreatedAt.IsZero() { cp.CreatedAt = now } cp.UpdatedAt = now r.devices[d.ID] = &cp return nil } // Delete 標記 device 為軟刪除。 func (r *InMemoryRepository) Delete(ctx context.Context, id string) error { r.mu.Lock() defer r.mu.Unlock() d, ok := r.devices[id] if !ok || d.DeletedAt != nil { return ErrNotFound } now := time.Now().UTC() d.DeletedAt = &now d.UpdatedAt = now return nil } // 編譯時檢查:確保 InMemoryRepository 實作 Repository。 var _ Repository = (*InMemoryRepository)(nil)