Files
hr_data_analyzer/controllers/gateway.go
T
2026-05-04 16:20:46 +08:00

464 lines
14 KiB
Go

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}
}
// @Summary 获取网关列表
// @Description 查询网关列表,支持关键词/售出状态/项目类型/区域筛选。超级管理员可查全部,操作员只能查所属区域
// @Tags 网关管理
// @Produce json
// @Param keyword query string false "关键词(名称/MAC模糊搜索)"
// @Param isSold query bool false "售出状态"
// @Param projectType query string false "项目类型"
// @Param regionId query int false "区域ID"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Router /admin/gateways [get]
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)
}
// @Summary 按MAC查询网关
// @Description 根据MAC地址查询网关信息(支持格式兼容:带冒号/横线/纯数字)
// @Tags 网关管理
// @Produce json
// @Param mac query string true "MAC地址"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Failure 400 {object} SwagAPIResponse "MAC参数为空"
// @Failure 404 {object} SwagAPIResponse "未找到该网关"
// @Router /gateways/by-mac [get]
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)
}
// @Summary 创建网关
// @Description 创建新的网关记录,自动同步到产品库存
// @Tags 网关管理
// @Accept json
// @Produce json
// @Param gateway body gatewayPayload true "网关信息"
// @Security BearerAuth
// @Success 201 {object} SwagAPIResponse "创建成功"
// @Failure 400 {object} SwagAPIResponse "请求参数错误"
// @Failure 409 {object} SwagAPIResponse "MAC地址已存在"
// @Router /admin/gateways [post]
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)
}
// @Summary 更新网关信息
// @Description 更新指定网关的详细信息,自动同步到产品库存
// @Tags 网关管理
// @Accept json
// @Produce json
// @Param id path int true "网关ID"
// @Param gateway body gatewayPayload true "更新信息"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "更新成功"
// @Failure 400 {object} SwagAPIResponse "请求参数错误"
// @Failure 404 {object} SwagAPIResponse "网关不存在"
// @Router /admin/gateways/{id} [put]
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)
}
// @Summary 删除网关
// @Description 删除指定网关(已售出的网关禁止删除),自动同步删除产品库存
// @Tags 网关管理
// @Produce json
// @Param id path int true "网关ID"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "删除成功"
// @Failure 403 {object} SwagAPIResponse "已售出无法删除"
// @Failure 404 {object} SwagAPIResponse "网关不存在"
// @Router /admin/gateways/{id} [delete]
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
}