package controllers import ( "encoding/json" "errors" "hr_receiver/config" "hr_receiver/models" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // GatewayAdminController 处理网关相关的HTTP请求 type GatewayAdminController struct { DB *gorm.DB } // gatewayPayload 用于接收前端JSON数据的结构体 type gatewayPayload struct { MAC string `json:"mac"` Name string `json:"name"` RegionID uint32 `json:"regionId"` Location string `json:"location"` ProjectType string `json:"projectType"` IsSold bool `json:"isSold"` // SoldAt 是可选字段。如果 IsSold 为 true 且未传此字段,后端自动设为当前时间 SoldAt *time.Time `json:"soldAt"` } // NewGatewayAdminController 初始化控制器 func NewGatewayAdminController() *GatewayAdminController { return &GatewayAdminController{DB: config.DB} } // List 获取网关列表 // GET /api/v1/gateways?keyword=&isSold=&projectType=®ionId= // 超级管理员可查询全部;操作员/区域管理员仅能查询所属 regionIDs 的网关 func (gc *GatewayAdminController) List(c *gin.Context) { var items []models.Gateway query := gc.DB.Model(&models.Gateway{}).Order("region_id ASC, created_at DESC") // 非超级管理员按用户区域过滤 roleValue, _ := c.Get("role") role, _ := roleValue.(models.UserRole) if role != models.UserRoleSuperAdmin { regionIDs, err := getUserRegionIDsFromContext(c) if err != nil { writeError(c, http.StatusForbidden, err.Error()) return } if len(regionIDs) == 0 { writeError(c, http.StatusForbidden, "当前用户未配置可访问区域") return } query = query.Where("region_id IN ?", regionIDs) } // 模糊搜索 if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" { likeValue := "%" + keyword + "%" query = query.Where("name LIKE ? OR mac LIKE ?", likeValue, likeValue) } // 按售出状态筛选 (可选) if soldStr := c.Query("isSold"); soldStr != "" { isSold, _ := strconv.ParseBool(soldStr) query = query.Where("is_sold = ?", isSold) } // 按项目类型筛选 (可选) if projectType := c.Query("projectType"); projectType != "" { query = query.Where("project_type = ?", projectType) } if regionIDStr := strings.TrimSpace(c.Query("regionId")); regionIDStr != "" { regionID, err := strconv.ParseUint(regionIDStr, 10, 32) if err != nil || regionID == 0 { writeError(c, http.StatusBadRequest, "regionId参数无效") return } query = query.Where("region_id = ?", uint32(regionID)) } if err := query.Find(&items).Error; err != nil { writeError(c, http.StatusInternalServerError, "查询网关列表失败") return } writeSuccess(c, http.StatusOK, "查询成功", items) } // GetByMACForUser 按 MAC 查询网关信息 // GET /api/v1/gateways/by-mac?mac= func (gc *GatewayAdminController) GetByMACForUser(c *gin.Context) { macInput := strings.ToUpper(strings.TrimSpace(c.Query("mac"))) if macInput == "" { writeError(c, http.StatusBadRequest, "mac参数不能为空") return } // 规范化:去掉冒号和横线,兼容不同存储格式 macClean := strings.ReplaceAll(strings.ReplaceAll(macInput, ":", ""), "-", "") query := gc.DB.Where("REPLACE(REPLACE(UPPER(mac), ':', ''), '-', '') = ?", macClean) roleValue, _ := c.Get("role") role, _ := roleValue.(models.UserRole) if role != models.UserRoleSuperAdmin { regionIDs, err := getUserRegionIDsFromContext(c) if err != nil { writeError(c, http.StatusForbidden, err.Error()) return } if len(regionIDs) == 0 { writeError(c, http.StatusForbidden, "当前用户未配置可访问区域") return } query = query.Where("region_id IN ?", regionIDs) } var item models.Gateway if err := query.First(&item).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { writeError(c, http.StatusNotFound, "未找到该网关") return } writeError(c, http.StatusInternalServerError, "查询网关失败") return } writeSuccess(c, http.StatusOK, "查询成功", item) } // Create 创建新网关 // POST /api/gateways func (gc *GatewayAdminController) Create(c *gin.Context) { var payload gatewayPayload if err := c.ShouldBindJSON(&payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } if err := validateGatewayPayload(payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } projectType, err := gc.findGatewayProjectType(payload.ProjectType) if err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } // 处理售出逻辑:如果已售出但未提供时间,默认为当前时间 (2026-04-29) var soldAt *time.Time if payload.IsSold { if payload.SoldAt == nil { // 设置为指定时间:2020-04-29 specificTime := time.Date(2026, 4, 29, 0, 0, 0, 0, time.Local) soldAt = &specificTime } else { soldAt = payload.SoldAt } } record := models.Gateway{ MAC: strings.ToUpper(strings.TrimSpace(payload.MAC)), Name: strings.TrimSpace(payload.Name), RegionID: payload.RegionID, Location: strings.TrimSpace(payload.Location), ProjectType: projectType.Code, IsSold: payload.IsSold, SoldAt: soldAt, } if err := gc.DB.Create(&record).Error; err != nil { if strings.Contains(strings.ToLower(err.Error()), "duplicate") { writeError(c, http.StatusConflict, "该MAC地址的网关已存在") return } writeError(c, http.StatusInternalServerError, "保存网关失败") return } if err := gc.syncGatewayInventory(record); err != nil { writeError(c, http.StatusInternalServerError, err.Error()) return } writeSuccess(c, http.StatusCreated, "创建成功", record) } // Update 更新网关信息 // PUT /api/gateways/:id func (gc *GatewayAdminController) Update(c *gin.Context) { record, err := gc.findByID(c.Param("id")) if err != nil { respondGatewayLookupError(c, err) return } var payload gatewayPayload if err := c.ShouldBindJSON(&payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } if err := validateGatewayPayload(payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } projectType, err := gc.findGatewayProjectType(payload.ProjectType) if err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } // 更新字段 record.Name = strings.TrimSpace(payload.Name) record.RegionID = payload.RegionID record.Location = strings.TrimSpace(payload.Location) record.ProjectType = projectType.Code record.IsSold = payload.IsSold // 核心逻辑:处理售出时间 // 情况1: 标记为已售出,但 SoldAt 为空 -> 自动填充指定时间 (2026-04-29) // 情况2: 标记为已售出,且提供了 SoldAt -> 使用提供的值 // 情况3: 标记为未售出 -> 强制 SoldAt 为 nil (防止数据残留) if payload.IsSold { if payload.SoldAt == nil { specificTime := time.Date(2026, 4, 29, 0, 0, 0, 0, time.Local) record.SoldAt = &specificTime } else { record.SoldAt = payload.SoldAt } } else { record.SoldAt = nil } if err := gc.DB.Save(&record).Error; err != nil { writeError(c, http.StatusInternalServerError, "更新网关失败") return } if err := gc.syncGatewayInventory(record); err != nil { writeError(c, http.StatusInternalServerError, err.Error()) return } writeSuccess(c, http.StatusOK, "更新成功", record) } // Delete 删除网关 // DELETE /api/gateways/:id func (gc *GatewayAdminController) Delete(c *gin.Context) { record, err := gc.findByID(c.Param("id")) if err != nil { respondGatewayLookupError(c, err) return } // 建议:如果网关已售出,禁止删除,防止数据丢失 if record.IsSold { writeError(c, http.StatusForbidden, "已售出的网关禁止删除") return } if err := gc.DB.Delete(&record).Error; err != nil { writeError(c, http.StatusInternalServerError, "删除网关失败") return } if err := gc.deleteGatewayInventory(record); err != nil { writeError(c, http.StatusInternalServerError, err.Error()) return } writeSuccess(c, http.StatusOK, "删除成功", nil) } // 辅助方法:根据ID查找记录 func (gc *GatewayAdminController) findByID(id string) (models.Gateway, error) { var record models.Gateway numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64) if err != nil { return record, gorm.ErrRecordNotFound } if err := gc.DB.First(&record, numericID).Error; err != nil { return record, err } return record, nil } func (gc *GatewayAdminController) findGatewayProjectType(projectTypeCode string) (models.ProjectType, error) { var projectType models.ProjectType code := strings.TrimSpace(strings.ToLower(projectTypeCode)) if code == "" { return projectType, errors.New("projectType is required") } if err := gc.DB.Where("code = ? AND is_active = ?", code, true).First(&projectType).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return projectType, errors.New("projectType is invalid") } return projectType, errors.New("failed to query project type") } if !projectType.SupportsGateway { return projectType, errors.New("current project type does not support gateways") } return projectType, nil } func (gc *GatewayAdminController) syncGatewayInventory(record models.Gateway) error { mac := strings.ToUpper(strings.TrimSpace(record.MAC)) if mac == "" { return nil } parameterValuesBytes, err := json.Marshal(map[string]interface{}{ "mac": mac, "location": strings.TrimSpace(record.Location), }) if err != nil { return err } status := models.ProductInventoryStatusInStock if record.IsSold { status = models.ProductInventoryStatusSold } serialNumber := mac regionID := record.RegionID var inventory models.ProductInventory err = gc.DB.Where("source_type = ? AND source_ref = ?", "gateway", mac).First(&inventory).Error switch { case err == nil: inventory.ProductCode = "collection_gateway" inventory.ProjectTypeCode = strings.TrimSpace(strings.ToLower(record.ProjectType)) inventory.SerialNumber = &serialNumber inventory.AssetName = strings.TrimSpace(record.Name) inventory.Status = status inventory.RegionID = ®ionID inventory.StorageLocation = strings.TrimSpace(record.Location) inventory.ParameterValues = string(parameterValuesBytes) inventory.SourceType = "gateway" inventory.SourceRef = mac if record.IsSold { inventory.SoldAt = record.SoldAt } else { inventory.SoldAt = nil } return gc.DB.Save(&inventory).Error case errors.Is(err, gorm.ErrRecordNotFound): inventory = models.ProductInventory{ ProductCode: "collection_gateway", ProjectTypeCode: strings.TrimSpace(strings.ToLower(record.ProjectType)), SerialNumber: &serialNumber, AssetName: strings.TrimSpace(record.Name), Status: status, RegionID: ®ionID, StorageLocation: strings.TrimSpace(record.Location), SoldAt: record.SoldAt, ParameterValues: string(parameterValuesBytes), SourceType: "gateway", SourceRef: mac, } return gc.DB.Create(&inventory).Error default: return err } } func (gc *GatewayAdminController) deleteGatewayInventory(record models.Gateway) error { mac := strings.ToUpper(strings.TrimSpace(record.MAC)) if mac == "" { return nil } return gc.DB.Where("source_type = ? AND source_ref = ?", "gateway", mac).Delete(&models.ProductInventory{}).Error } // 辅助方法:验证输入数据 func validateGatewayPayload(payload gatewayPayload) error { mac := strings.TrimSpace(payload.MAC) if mac == "" { return errors.New("MAC地址是必填项") } if len(mac) < 12 { return errors.New("MAC地址格式不正确") } if strings.TrimSpace(payload.Name) == "" { return errors.New("网关名称是必填项") } if strings.TrimSpace(payload.ProjectType) == "" { return errors.New("项目类型是必填项") } return nil } // 错误处理函数 func respondGatewayLookupError(c *gin.Context, err error) { if errors.Is(err, gorm.ErrRecordNotFound) { writeError(c, http.StatusNotFound, "未找到该网关") return } writeError(c, http.StatusInternalServerError, "查询网关时出错") } func getUserRegionIDsFromContext(c *gin.Context) ([]uint32, error) { regionValue, exists := c.Get("regionIDs") if !exists { return nil, errors.New("missing user regions") } regionIDs, ok := regionValue.([]uint32) if !ok { return nil, errors.New("invalid user regions") } filtered := make([]uint32, 0, len(regionIDs)) for _, regionID := range regionIDs { if regionID == 0 { continue } filtered = append(filtered, regionID) } return filtered, nil }