package service import ( "errors" "time" "gorm.io/gorm" "laic-backend/cache" "laic-backend/common" "laic-backend/logger" "laic-backend/model" "laic-backend/tool" "laic-backend/vo" ) type DockService struct{} var DefaultDockService = &DockService{} // withUserFilter admin 不过滤,普通用户按 user_id 过滤(GORM scope) func withUserFilter(userID int64, isAdmin bool) func(*gorm.DB) *gorm.DB { if isAdmin { return func(db *gorm.DB) *gorm.DB { return db } } return func(db *gorm.DB) *gorm.DB { return db.Where("user_id = ?", userID) } } // withDockFilter admin 不过滤,普通用户按所属机巢过滤(用于无 user_id 的 dock 关联表,如 alarm / command_log) func withDockFilter(userID int64, isAdmin bool) func(*gorm.DB) *gorm.DB { if isAdmin { return func(db *gorm.DB) *gorm.DB { return db } } return func(db *gorm.DB) *gorm.DB { return db.Where("dock_id IN (SELECT dock_id FROM dock WHERE user_id = ?)", userID) } } // GetPage 机巢分页列表 func (s *DockService) GetPage(userID int64, isAdmin bool, req *vo.DockPageReq) (*common.PageResponse[model.Dock], *common.BusiError) { db := common.DB.Model(&model.Dock{}).Scopes(withUserFilter(userID, isAdmin)) if req.RegisterStatus != "" { db = db.Where("register_status = ?", req.RegisterStatus) } if req.Keyword != "" { kw := "%" + req.Keyword + "%" db = db.Where("name LIKE ? OR dock_id LIKE ? OR code LIKE ?", kw, kw, kw) } var total int64 if err := db.Count(&total).Error; err != nil { logger.ERROR("统计机巢失败", err) return nil, common.ErrInternal } var list []model.Dock if err := db.Scopes(req.Paginate).Order("created_at DESC, id DESC").Find(&list).Error; err != nil { logger.ERROR("查询机巢列表失败", err) return nil, common.ErrInternal } return common.Page(req.Pagination, total, list), nil } // GetDetail 机巢详情(含实时状态) func (s *DockService) GetDetail(userID int64, isAdmin bool, id int64) (*vo.DockVO, *common.BusiError) { var dock model.Dock if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&dock, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrDockNotFound } return nil, common.ErrInternal } online, realtime := s.realtime(dock.DockID) return &vo.DockVO{Dock: &dock, Online: online, Realtime: realtime}, nil } // GetStatus 机巢纯实时状态 func (s *DockService) GetStatus(userID int64, isAdmin bool, id int64) (*vo.DockStatusVO, *common.BusiError) { var dock model.Dock if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).Select("dock_id").First(&dock, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrDockNotFound } return nil, common.ErrInternal } online, realtime := s.realtime(dock.DockID) return &vo.DockStatusVO{DockID: dock.DockID, Online: online, Realtime: realtime}, nil } // Create 手动登记机巢(归属当前用户;admin 可指定 userID) func (s *DockService) Create(userID int64, isAdmin bool, req *vo.DockCreateReq) (*model.Dock, *common.BusiError) { owner := userID if isAdmin && req.UserID != 0 { owner = req.UserID } var existing model.Dock if err := common.DB.Unscoped().Where("dock_id = ?", req.DockID).First(&existing).Error; err == nil { if existing.DeletedAt.Valid { updates := map[string]any{ "user_id": owner, "deleted_at": nil, "register_status": "registered", "updated_at": time.Now(), "name": req.Name, "code": req.Code, "sn": req.SN, "location": req.Location, "longitude": req.Longitude, "latitude": req.Latitude, "altitude": req.Altitude, } if err := common.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Unscoped().Model(&existing).Updates(updates).Error; err != nil { return err } return tx.Model(&model.Drone{}).Where("dock_id = ?", existing.DockID).Update("user_id", owner).Error }); err != nil { logger.ERROR("恢复已删除机巢失败", err) return nil, common.ErrInternal } common.DB.First(&existing, existing.ID) if req.Iccid != "" { DefaultSimRechargeService.EnsureCardFromICCID(owner, existing.DockID, req.Iccid) } return &existing, nil } if existing.UserID != 0 { return nil, common.ErrDockExists } updates := map[string]any{ "user_id": owner, "register_status": "registered", "updated_at": time.Now(), } if req.Name != "" { updates["name"] = req.Name } if req.Code != "" { updates["code"] = req.Code } if req.SN != "" { updates["sn"] = req.SN } if req.Location != "" { updates["location"] = req.Location } updates["longitude"] = req.Longitude updates["latitude"] = req.Latitude updates["altitude"] = req.Altitude if err := common.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&existing).Updates(updates).Error; err != nil { return err } return tx.Model(&model.Drone{}).Where("dock_id = ?", existing.DockID).Update("user_id", owner).Error }); err != nil { logger.ERROR("认领机巢失败", err) return nil, common.ErrInternal } common.DB.First(&existing, existing.ID) if req.Iccid != "" { DefaultSimRechargeService.EnsureCardFromICCID(owner, existing.DockID, req.Iccid) } return &existing, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { logger.ERROR("查询机巢失败", err) return nil, common.ErrInternal } id, err := tool.NextID() if err != nil { return nil, common.ErrInternal } now := time.Now() dock := &model.Dock{ ID: id, UserID: owner, DockID: req.DockID, Name: req.Name, Code: req.Code, SN: req.SN, Location: req.Location, Longitude: req.Longitude, Latitude: req.Latitude, Altitude: req.Altitude, RegisterStatus: "registered", DockIDSource: "manual", ProtocolVer: "1.0", CreatedAt: now, UpdatedAt: now, } if err := common.DB.Create(dock).Error; err != nil { if code, _ := common.ParseError(err); code == 1062 { return nil, common.ErrDockExists } logger.ERROR("登记机巢失败", err) return nil, common.ErrInternal } if req.Iccid != "" { DefaultSimRechargeService.EnsureCardFromICCID(owner, dock.DockID, req.Iccid) } return dock, nil } // Update 更新/认领机巢 func (s *DockService) Update(userID int64, isAdmin bool, id int64, req *vo.DockUpdateReq) (*model.Dock, *common.BusiError) { var dock model.Dock if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&dock, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrDockNotFound } return nil, common.ErrInternal } updates := map[string]any{"updated_at": time.Now()} if req.Name != "" { updates["name"] = req.Name } if req.Code != "" { updates["code"] = req.Code } if req.Location != nil { updates["location"] = *req.Location } if req.Longitude != nil { updates["longitude"] = *req.Longitude } if req.Latitude != nil { updates["latitude"] = *req.Latitude } if req.Altitude != nil { updates["altitude"] = *req.Altitude } if isAdmin && req.UserID != 0 { updates["user_id"] = req.UserID } if req.RegisterAs == "registered" { updates["register_status"] = "registered" if isAdmin && req.UserID != 0 { updates["user_id"] = req.UserID } else { updates["user_id"] = userID // 普通用户认领 → 归属自己 } } if err := common.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&dock).Updates(updates).Error; err != nil { return err } if owner, ok := updates["user_id"]; ok { if err := tx.Model(&model.Drone{}).Where("dock_id = ?", dock.DockID).Update("user_id", owner).Error; err != nil { return err } } return nil }); err != nil { logger.ERROR("更新机巢失败", err) return nil, common.ErrInternal } common.DB.First(&dock, id) if req.RegisterAs == "registered" && req.Iccid != "" { DefaultSimRechargeService.EnsureCardFromICCID(dock.UserID, dock.DockID, req.Iccid) } return &dock, nil } // Delete 软删除机巢:保留任务、直播、告警、指令、视频等历史数据用于审计。 func (s *DockService) Delete(userID int64, isAdmin bool, id int64) *common.BusiError { var dock model.Dock if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&dock, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return common.ErrDockNotFound } logger.ERROR("查询待删除机巢失败", err) return common.ErrInternal } if err := common.DB.Transaction(func(tx *gorm.DB) error { // 防止删除后 cron 任务仍继续向已移除的设备下发指令;任务历史保留。 if err := tx.Model(&model.TaskPlan{}). Where("dock_id = ? AND schedule_type = ? AND status = ?", dock.DockID, "cron", "enabled"). Updates(map[string]any{"status": "disabled", "updated_at": time.Now()}).Error; err != nil { return err } return tx.Delete(&dock).Error }); err != nil { logger.ERROR("软删除机巢失败", err) return common.ErrInternal } DefaultScheduler.RemoveTaskForDock(dock.DockID) _ = common.SetRemove(cache.OnlineDockSetKey, dock.DockID) _ = common.Delete(cache.DockHeartbeatKeyOf(dock.DockID)) _ = common.Delete(cache.DockStatusKeyOf(dock.DockID)) return nil } // realtime 读取机巢实时状态(Redis Hash + 在线集合) func (s *DockService) realtime(dockID string) (bool, map[string]string) { realtime, err := common.HashGetAll(cache.DockStatusKeyOf(dockID)) if err != nil { realtime = map[string]string{} } online, _ := common.SetMemberExists(cache.OnlineDockSetKey, dockID) return online, realtime } // EnsureFromOnline 设备自发现:不存在则登记为 pending,存在则更新在线状态与软件版本 func (s *DockService) EnsureFromOnline(dockID string, st statusOnline) { online := st.Status == "online" var dock model.Dock err := common.DB.Unscoped().Where("dock_id = ?", dockID).First(&dock).Error if err == nil && dock.DeletedAt.Valid { // 已删除设备不会因 MQTT 自动发现而复活,必须通过“添加/认领机巢”显式恢复。 _ = common.SetRemove(cache.OnlineDockSetKey, dockID) _ = common.Delete(cache.DockHeartbeatKeyOf(dockID)) return } if errors.Is(err, gorm.ErrRecordNotFound) { id, e := tool.NextID() if e != nil { logger.ERROR("生成机巢 ID 失败", e) return } now := time.Now() dock = model.Dock{ID: id, UserID: 0, DockID: dockID, Name: st.Name, Code: dockID, SN: dockID, Location: st.Location, Latitude: st.Latitude, Longitude: st.Longitude, RegisterStatus: "pending", DockIDSource: "auto", SoftwareVer: st.SoftwareVersion, ProtocolVer: st.ProtocolVersion, CreatedAt: now, UpdatedAt: now} if err := common.DB.Create(&dock).Error; err != nil { logger.ERROR("登记机巢失败", err) return } } else if err == nil { updates := map[string]any{"updated_at": time.Now()} if dock.DeletedAt.Valid { updates["deleted_at"] = nil } if st.SoftwareVersion != "" { updates["software_ver"] = st.SoftwareVersion } if st.ProtocolVersion != "" { updates["protocol_ver"] = st.ProtocolVersion } if dock.Name == "" && st.Name != "" { updates["name"] = st.Name } if dock.Location == "" && st.Location != "" { updates["location"] = st.Location } if dock.Latitude == 0 && st.Latitude != 0 { updates["latitude"] = st.Latitude } if dock.Longitude == 0 && st.Longitude != 0 { updates["longitude"] = st.Longitude } if err := common.DB.Unscoped().Model(&dock).Updates(updates).Error; err != nil { logger.ERROR("更新机巢在线状态失败", err) } } else { logger.ERROR("查询机巢在线状态失败", err) return } if online { _ = common.SetAdd(cache.OnlineDockSetKey, dockID) refreshDeviceHeartbeat(cache.DockHeartbeatKeyOf(dockID)) } else { _ = common.SetRemove(cache.OnlineDockSetKey, dockID) _ = common.Delete(cache.DockHeartbeatKeyOf(dockID)) } broadcast("dock.status", map[string]any{"dockId": dockID, "online": online, "softwareVer": st.SoftwareVersion, "protocolVer": st.ProtocolVersion}) }