diff --git a/controllers/gateway.go b/controllers/gateway.go index 0556346..5d4db5d 100644 --- a/controllers/gateway.go +++ b/controllers/gateway.go @@ -36,7 +36,7 @@ func NewGatewayAdminController() *GatewayAdminController { } // List 获取网关列表 -// GET /api/gateways?keyword=&isSold=&projectType= +// GET /api/gateways?keyword=&isSold=&projectType=®ionId= func (gc *GatewayAdminController) List(c *gin.Context) { var items []models.Gateway query := gc.DB.Model(&models.Gateway{}).Order("region_id ASC, created_at DESC") @@ -57,6 +57,14 @@ func (gc *GatewayAdminController) List(c *gin.Context) { 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, "查询网关列表失败") @@ -78,6 +86,11 @@ func (gc *GatewayAdminController) Create(c *gin.Context) { 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 @@ -96,7 +109,7 @@ func (gc *GatewayAdminController) Create(c *gin.Context) { Name: strings.TrimSpace(payload.Name), RegionID: payload.RegionID, Location: strings.TrimSpace(payload.Location), - ProjectType: strings.TrimSpace(payload.ProjectType), + ProjectType: projectType.Code, IsSold: payload.IsSold, SoldAt: soldAt, } @@ -131,12 +144,17 @@ func (gc *GatewayAdminController) Update(c *gin.Context) { 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 = strings.TrimSpace(payload.ProjectType) + record.ProjectType = projectType.Code record.IsSold = payload.IsSold // 核心逻辑:处理售出时间 @@ -196,6 +214,24 @@ func (gc *GatewayAdminController) findByID(id string) (models.Gateway, error) { 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 validateGatewayPayload(payload gatewayPayload) error { mac := strings.TrimSpace(payload.MAC) @@ -208,6 +244,9 @@ func validateGatewayPayload(payload gatewayPayload) error { if strings.TrimSpace(payload.Name) == "" { return errors.New("网关名称是必填项") } + if strings.TrimSpace(payload.ProjectType) == "" { + return errors.New("项目类型是必填项") + } return nil } diff --git a/controllers/project_type_admin.go b/controllers/project_type_admin.go new file mode 100644 index 0000000..1d2fc0c --- /dev/null +++ b/controllers/project_type_admin.go @@ -0,0 +1,185 @@ +package controllers + +import ( + "errors" + "hr_receiver/config" + "hr_receiver/models" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type ProjectTypeAdminController struct { + DB *gorm.DB +} + +type projectTypePayload struct { + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + SupportsGateway bool `json:"supportsGateway"` + IsActive bool `json:"isActive"` + Sort int `json:"sort"` +} + +func NewProjectTypeAdminController() *ProjectTypeAdminController { + return &ProjectTypeAdminController{DB: config.DB} +} + +func (pc *ProjectTypeAdminController) List(c *gin.Context) { + var items []models.ProjectType + query := pc.DB.Model(&models.ProjectType{}).Order("sort ASC, id ASC") + + if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" { + likeValue := "%" + keyword + "%" + query = query.Where("code LIKE ? OR name LIKE ? OR description LIKE ?", likeValue, likeValue, likeValue) + } + if supportsGatewayStr := strings.TrimSpace(c.Query("supportsGateway")); supportsGatewayStr != "" { + supportsGateway, err := strconv.ParseBool(supportsGatewayStr) + if err != nil { + writeError(c, http.StatusBadRequest, "invalid supportsGateway") + return + } + query = query.Where("supports_gateway = ?", supportsGateway) + } + if isActiveStr := strings.TrimSpace(c.Query("isActive")); isActiveStr != "" { + isActive, err := strconv.ParseBool(isActiveStr) + if err != nil { + writeError(c, http.StatusBadRequest, "invalid isActive") + return + } + query = query.Where("is_active = ?", isActive) + } + + if err := query.Find(&items).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to query project types") + return + } + writeSuccess(c, http.StatusOK, "query success", items) +} + +func (pc *ProjectTypeAdminController) Create(c *gin.Context) { + var payload projectTypePayload + if err := c.ShouldBindJSON(&payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + if err := validateProjectTypePayload(payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + + record := models.ProjectType{ + Code: normalizeProjectTypeCode(payload.Code), + Name: strings.TrimSpace(payload.Name), + Description: strings.TrimSpace(payload.Description), + SupportsGateway: payload.SupportsGateway, + IsActive: payload.IsActive, + Sort: payload.Sort, + } + if err := pc.DB.Create(&record).Error; err != nil { + writeProjectTypeDBError(c, err) + return + } + writeSuccess(c, http.StatusCreated, "create success", record) +} + +func (pc *ProjectTypeAdminController) Update(c *gin.Context) { + record, err := pc.findByID(c.Param("id")) + if err != nil { + respondProjectTypeLookupError(c, err) + return + } + + var payload projectTypePayload + if err := c.ShouldBindJSON(&payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + if err := validateProjectTypePayload(payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + + record.Code = normalizeProjectTypeCode(payload.Code) + record.Name = strings.TrimSpace(payload.Name) + record.Description = strings.TrimSpace(payload.Description) + record.SupportsGateway = payload.SupportsGateway + record.IsActive = payload.IsActive + record.Sort = payload.Sort + + if err := pc.DB.Save(&record).Error; err != nil { + writeProjectTypeDBError(c, err) + return + } + writeSuccess(c, http.StatusOK, "update success", record) +} + +func (pc *ProjectTypeAdminController) Delete(c *gin.Context) { + record, err := pc.findByID(c.Param("id")) + if err != nil { + respondProjectTypeLookupError(c, err) + return + } + + var gatewayCount int64 + if err := pc.DB.Model(&models.Gateway{}).Where("project_type = ?", record.Code).Count(&gatewayCount).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to check gateway references") + return + } + if gatewayCount > 0 { + writeError(c, http.StatusConflict, "current project type is referenced by gateways") + return + } + + if err := pc.DB.Delete(&record).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to delete project type") + return + } + writeSuccess(c, http.StatusOK, "delete success", nil) +} + +func (pc *ProjectTypeAdminController) findByID(id string) (models.ProjectType, error) { + var record models.ProjectType + numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64) + if err != nil { + return record, gorm.ErrRecordNotFound + } + if err := pc.DB.First(&record, numericID).Error; err != nil { + return record, err + } + return record, nil +} + +func validateProjectTypePayload(payload projectTypePayload) error { + if normalizeProjectTypeCode(payload.Code) == "" { + return errors.New("code is required") + } + if strings.TrimSpace(payload.Name) == "" { + return errors.New("name is required") + } + return nil +} + +func normalizeProjectTypeCode(code string) string { + return strings.TrimSpace(strings.ToLower(code)) +} + +func respondProjectTypeLookupError(c *gin.Context, err error) { + if errors.Is(err, gorm.ErrRecordNotFound) { + writeError(c, http.StatusNotFound, "project type not found") + return + } + writeError(c, http.StatusInternalServerError, "failed to query project type") +} + +func writeProjectTypeDBError(c *gin.Context, err error) { + if strings.Contains(strings.ToLower(err.Error()), "unique") { + writeError(c, http.StatusConflict, "project type code already exists") + return + } + writeError(c, http.StatusInternalServerError, "failed to persist project type") +} diff --git a/main.go b/main.go index 606d969..00aff19 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ func main() { &models.User{}, &models.UserRegionBinding{}, &models.Kindergarten{}, + &models.ProjectType{}, &models.AppFile{}, &models.AppFileShareCode{}, &models.MqttHeartRateRecord{}, @@ -49,6 +50,9 @@ func main() { if err := models.EnsureDefaultAIPricing(config.DB); err != nil { log.Printf("default ai pricing init failed: %v", err) } + if err := models.EnsureDefaultProjectTypes(config.DB); err != nil { + log.Printf("default project types init failed: %v", err) + } if err := mqtt.Start(config.DB, config.App.MQTT); err != nil { log.Printf("mqtt listener start failed: %v", err) diff --git a/models/project_type.go b/models/project_type.go new file mode 100644 index 0000000..b596028 --- /dev/null +++ b/models/project_type.go @@ -0,0 +1,76 @@ +package models + +import ( + "errors" + "strings" + "time" + + "gorm.io/gorm" +) + +type ProjectType struct { + ID uint `gorm:"primaryKey" json:"id"` + Code string `gorm:"size:64;not null;uniqueIndex" json:"code"` + Name string `gorm:"size:255;not null" json:"name"` + Description string `gorm:"size:1024" json:"description"` + SupportsGateway bool `gorm:"not null;default:false;index" json:"supportsGateway"` + IsActive bool `gorm:"not null;default:true;index" json:"isActive"` + Sort int `gorm:"not null;default:0;index" json:"sort"` + CreatedAt int64 `gorm:"not null" json:"created_at"` + UpdatedAt int64 `gorm:"not null" json:"updated_at"` +} + +func (ProjectType) TableName() string { + return "project_types" +} + +func (p *ProjectType) BeforeCreate(tx *gorm.DB) (err error) { + now := time.Now().UnixMilli() + p.Code = strings.TrimSpace(strings.ToLower(p.Code)) + p.Name = strings.TrimSpace(p.Name) + p.Description = strings.TrimSpace(p.Description) + p.CreatedAt = now + p.UpdatedAt = now + return nil +} + +func (p *ProjectType) BeforeUpdate(tx *gorm.DB) (err error) { + p.Code = strings.TrimSpace(strings.ToLower(p.Code)) + p.Name = strings.TrimSpace(p.Name) + p.Description = strings.TrimSpace(p.Description) + p.UpdatedAt = time.Now().UnixMilli() + return nil +} + +func EnsureDefaultProjectTypes(db *gorm.DB) error { + defaults := []ProjectType{ + {Code: "flink", Name: "步频节拍器", Description: "Flink 步频节拍器项目", SupportsGateway: false, IsActive: true, Sort: 10}, + {Code: "light", Name: "心肺耐力测试", Description: "Light 心肺耐力测试项目", SupportsGateway: true, IsActive: true, Sort: 20}, + {Code: "heartrate", Name: "智能心率采集", Description: "智能心率采集项目", SupportsGateway: true, IsActive: true, Sort: 30}, + {Code: "run50", Name: "50米往返跑", Description: "50米往返跑项目", SupportsGateway: false, IsActive: true, Sort: 40}, + } + + for _, item := range defaults { + var existing ProjectType + err := db.Where("code = ?", item.Code).First(&existing).Error + switch { + case err == nil: + existing.Name = item.Name + existing.Description = item.Description + existing.SupportsGateway = item.SupportsGateway + existing.IsActive = item.IsActive + existing.Sort = item.Sort + if err := db.Save(&existing).Error; err != nil { + return err + } + case errors.Is(err, gorm.ErrRecordNotFound): + if err := db.Create(&item).Error; err != nil { + return err + } + default: + return err + } + } + + return nil +} diff --git a/routes/routes.go b/routes/routes.go index 55ca237..9b6ecea 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -15,6 +15,7 @@ func SetupRouter() *gin.Engine { stepTrainController := controllers.NewStepTrainingController() lessonPlanController := controllers.NewLessonPlanController() kindergartenAdminController := controllers.NewKindergartenAdminController() + projectTypeAdminController := controllers.NewProjectTypeAdminController() userAdminController := controllers.NewUserAdminController() gatewayController := controllers.NewGatewayAdminController() systemDebugController := controllers.NewSystemDebugController() @@ -73,6 +74,11 @@ func SetupRouter() *gin.Engine { admin.PUT("/kindergartens/:id", kindergartenAdminController.Update) admin.DELETE("/kindergartens/:id", kindergartenAdminController.Delete) + admin.GET("/project-types", projectTypeAdminController.List) + admin.POST("/project-types", projectTypeAdminController.Create) + admin.PUT("/project-types/:id", projectTypeAdminController.Update) + admin.DELETE("/project-types/:id", projectTypeAdminController.Delete) + admin.GET("/users", userAdminController.List) admin.POST("/users", userAdminController.Create) admin.PUT("/users/:id", userAdminController.Update)