Files
hr_data_analyzer/controllers/gateway.go
T
2026-05-02 08:43:01 +08:00

421 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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=&regionId=
// 超级管理员可查询全部;操作员/区域管理员仅能查询所属 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 = &regionID
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: &regionID,
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
}