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.Status != "" { db = db.Where("status = ?", req.Status) } 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("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 } 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, Iccid: req.Iccid, Location: req.Location, Longitude: req.Longitude, Latitude: req.Latitude, Altitude: req.Altitude, Status: "offline", 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 } 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) 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 } var taskIDs []string var droneSNs []string var simCardIDs []int64 if err := common.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&model.TaskPlan{}).Where("dock_id = ?", dock.DockID).Pluck("id", &taskIDs).Error; err != nil { return err } if err := tx.Model(&model.Drone{}).Where("dock_id = ?", dock.DockID).Pluck("drone_sn", &droneSNs).Error; err != nil { return err } if err := tx.Model(&model.SimCard{}).Where("dock_id = ?", dock.DockID).Pluck("id", &simCardIDs).Error; err != nil { return err } if len(droneSNs) > 0 { var videoIDs []int64 if err := tx.Model(&model.Video{}).Where("drone_sn IN ?", droneSNs).Pluck("id", &videoIDs).Error; err != nil { return err } if len(videoIDs) > 0 { if err := tx.Where("video_id IN ?", videoIDs).Delete(&model.DownloadLog{}).Error; err != nil { return err } if err := tx.Where("id IN ?", videoIDs).Delete(&model.Video{}).Error; err != nil { return err } } } if len(simCardIDs) > 0 { if err := tx.Where("sim_card_id IN ?", simCardIDs).Delete(&model.SimUsageRecord{}).Error; err != nil { return err } if err := tx.Where("sim_card_id IN ?", simCardIDs).Delete(&model.SimRechargeLog{}).Error; err != nil { return err } if err := tx.Where("id IN ?", simCardIDs).Delete(&model.SimCard{}).Error; err != nil { return err } } for _, item := range []any{ &model.Alarm{}, &model.DeviceCommandLog{}, &model.WorkflowState{}, &model.TaskExecution{}, &model.TaskPlan{}, &model.LiveSession{}, &model.Drone{}, } { if err := tx.Where("dock_id = ?", dock.DockID).Delete(item).Error; err != nil { return err } } return tx.Delete(&dock).Error }); err != nil { logger.ERROR("硬删除机巢失败", err) return common.ErrInternal } for _, taskID := range taskIDs { DefaultScheduler.RemoveTask(taskID) } _ = 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" status := "offline" if online { status = "online" } var dock model.Dock err := common.DB.Where("dock_id = ?", dockID).First(&dock).Error 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, Status: status, 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{"status": status, "updated_at": time.Now()} 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.Model(&dock).Updates(updates).Error; err != nil { logger.ERROR("更新机巢在线状态失败", err) } } // 在线集合 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, }) }