package service import ( "encoding/json" "errors" "fmt" "strconv" "time" "gorm.io/gorm" "laic-backend/cache" "laic-backend/common" "laic-backend/logger" "laic-backend/model" "laic-backend/mqtt" "laic-backend/tool" "laic-backend/vo" ) type FirmwareService struct{} var DefaultFirmwareService = &FirmwareService{} const defaultFirmwareComponent = "dock-edge-agent" // GetPage 固件分页列表(admin) func (s *FirmwareService) GetPage(req *vo.FirmwarePageReq) (*common.PageResponse[model.Firmware], *common.BusiError) { db := common.DB.Model(&model.Firmware{}) if req.Status != "" { db = db.Where("status = ?", req.Status) } if req.Component != "" { db = db.Where("component = ?", req.Component) } if req.Keyword != "" { kw := "%" + req.Keyword + "%" db = db.Where("version LIKE ? OR description LIKE ?", kw, kw) } var total int64 if err := db.Count(&total).Error; err != nil { logger.ERROR("统计固件失败", err) return nil, common.ErrInternal } var list []model.Firmware 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 } // GetReleasedList 已发布固件列表(供普通用户选择升级) func (s *FirmwareService) GetReleasedList() ([]model.Firmware, *common.BusiError) { var list []model.Firmware if err := common.DB.Where("status = ?", "released").Order("updated_at DESC, id DESC").Find(&list).Error; err != nil { logger.ERROR("查询已发布固件失败", err) return nil, common.ErrInternal } return list, nil } // GetDetail 固件详情(admin) func (s *FirmwareService) GetDetail(id int64) (*model.Firmware, *common.BusiError) { var fw model.Firmware if err := common.DB.First(&fw, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrFirmwareNotFound } return nil, common.ErrInternal } return &fw, nil } // Create 新增固件版本(admin) func (s *FirmwareService) Create(adminID int64, req *vo.FirmwareCreateReq) (*model.Firmware, *common.BusiError) { id, err := tool.NextID() if err != nil { return nil, common.ErrInternal } component := req.Component if component == "" { component = defaultFirmwareComponent } now := time.Now() fw := &model.Firmware{ ID: id, Component: component, Version: req.Version, Description: req.Description, FileURL: req.FileURL, Sha256: req.Sha256, Signature: req.Signature, FileSize: req.FileSize, Mandatory: req.Mandatory, Status: "draft", CreatedBy: adminID, CreatedAt: now, UpdatedAt: now, } if err := common.DB.Create(fw).Error; err != nil { if code, _ := common.ParseError(err); code == 1062 { return nil, common.ErrFirmwareVersionExist } logger.ERROR("新增固件失败", err) return nil, common.ErrInternal } return fw, nil } // Update 编辑固件(admin) func (s *FirmwareService) Update(id int64, req *vo.FirmwareUpdateReq) *common.BusiError { var fw model.Firmware if err := common.DB.First(&fw, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return common.ErrFirmwareNotFound } return common.ErrInternal } updates := map[string]any{"updated_at": time.Now()} if req.Description != "" { updates["description"] = req.Description } if req.FileURL != "" { updates["file_url"] = req.FileURL } if req.Sha256 != "" { updates["sha256"] = req.Sha256 } if req.Signature != "" { updates["signature"] = req.Signature } if req.FileSize != 0 { updates["file_size"] = req.FileSize } if req.Mandatory != nil { updates["mandatory"] = *req.Mandatory } if req.Status != "" { updates["status"] = req.Status } if err := common.DB.Model(&fw).Updates(updates).Error; err != nil { logger.ERROR("编辑固件失败", err) return common.ErrInternal } return nil } // Delete 删除固件(admin) func (s *FirmwareService) Delete(id int64) *common.BusiError { res := common.DB.Delete(&model.Firmware{}, id) if res.Error != nil { logger.ERROR("删除固件失败", res.Error) return common.ErrInternal } if res.RowsAffected == 0 { return common.ErrFirmwareNotFound } return nil } // Upgrade 下发 OTA 升级:校验机巢归属+在线 → 校验固件已发布 → MQTT ota/desired func (s *FirmwareService) Upgrade(userID int64, isAdmin bool, dockID int64, firmwareID int64) (*vo.FirmwareUpgradeVO, *common.BusiError) { var dock model.Dock if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&dock, dockID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrDockNotFound } return nil, common.ErrInternal } online, _ := common.SetMemberExists(cache.OnlineDockSetKey, dock.DockID) if !online { return nil, common.ErrDockOffline } var fw model.Firmware if err := common.DB.First(&fw, firmwareID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrFirmwareNotFound } return nil, common.ErrInternal } if fw.Status != "released" { return nil, common.ErrFirmwareNotReleased } updateID, err := tool.NextID() if err != nil { return nil, common.ErrInternal } payload := map[string]any{ "updateId": strconv.FormatInt(updateID, 10), "component": fw.Component, "version": fw.Version, "url": fw.FileURL, "sha256": fw.Sha256, "signature": fw.Signature, "mandatory": fw.Mandatory == 1, "issuedAt": time.Now().UnixMilli(), } b, err := json.Marshal(payload) if err != nil { return nil, common.ErrInternal } topic := fmt.Sprintf("dock-edge/v1/dock/%s/ota/desired", dock.DockID) if err := mqtt.Publish(topic, 1, true, b); err != nil { logger.ERROR("下发 OTA 升级失败", err) return nil, common.ErrInternal } return &vo.FirmwareUpgradeVO{ UpdateID: strconv.FormatInt(updateID, 10), DockID: dock.DockID, Firmware: &fw, }, nil }