Files
2026-05-04 16:20:46 +08:00

464 lines
14 KiB
Go
Raw Permalink 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}
}
// @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
}