From f269502eac9a4daa2d0bbd0d42e3706dba804edd Mon Sep 17 00:00:00 2001 From: laoboli <1293528695@qq.com> Date: Wed, 29 Apr 2026 16:56:34 +0800 Subject: [PATCH] feat: gateway. --- controllers/ai.go | 31 +++++- controllers/gateway.go | 221 +++++++++++++++++++++++++++++++++++++++++ main.go | 1 + models/device.go | 39 ++++++++ routes/routes.go | 7 ++ 5 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 controllers/gateway.go create mode 100644 models/device.go diff --git a/controllers/ai.go b/controllers/ai.go index 71f3c36..719e726 100644 --- a/controllers/ai.go +++ b/controllers/ai.go @@ -72,6 +72,8 @@ func readDocxContentFromPath(filePath string) (string, error) { // readCSVContent 读取 .csv 文件内容 // 修改为先保存临时文件再读取 +// readCSVContent 读取 .csv 文件内容 +// 修改压缩策略:每 4 行保留 1 行数据 func readCSVContent(fileHeader *multipart.FileHeader) (string, error) { // 1. 创建临时文件 tempFile, err := os.CreateTemp("", "upload_*.csv") @@ -100,7 +102,29 @@ func readCSVContent(fileHeader *multipart.FileHeader) (string, error) { return "", fmt.Errorf("failed to read CSV content from temporary file: %w", err) } - return string(content), nil + // --- 修改逻辑开始:每 4 行保留 1 行 --- + lines := strings.Split(string(content), "\n") + var compressedLines []string + + for i, line := range lines { + // 1. 必须保留第一行(表头),让 AI 知道每一列是什么 + if i == 0 { + compressedLines = append(compressedLines, line) + continue + } + + // 2. 跳过空行 + if strings.TrimSpace(line) == "" { + continue + } + + if (i-1)%4 == 0 { + compressedLines = append(compressedLines, line) + } + } + + resultContent := strings.Join(compressedLines, "\n") + return resultContent, nil } // buildAnalysisPrompt 构建发送给 AI 的提示词 @@ -213,6 +237,11 @@ func buildAnalysisPrompt(teachingPlanContent, heartRateContent, analysisType, st // callAIForAnalysis 调用大模型进行分析 func callAIForAnalysis(prompt string) (string, error) { + sizeInBytes := len(prompt) + sizeInKB := float64(sizeInBytes) / 1024.0 + + // 在日志中打印大小,保留两位小数 + log.Printf("=== 发送给 AI 的内容大小: %.2f KB (%d 字节) ===", sizeInKB, sizeInBytes) baseURL, apiKey, model, err := config.GetAIConfig() if err != nil { return "", err diff --git a/controllers/gateway.go b/controllers/gateway.go new file mode 100644 index 0000000..0556346 --- /dev/null +++ b/controllers/gateway.go @@ -0,0 +1,221 @@ +package controllers + +import ( + "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/gateways?keyword=&isSold=&projectType= +func (gc *GatewayAdminController) List(c *gin.Context) { + var items []models.Gateway + query := gc.DB.Model(&models.Gateway{}).Order("region_id ASC, created_at DESC") + + // 模糊搜索 + 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 err := query.Find(&items).Error; err != nil { + writeError(c, http.StatusInternalServerError, "查询网关列表失败") + return + } + writeSuccess(c, http.StatusOK, "查询成功", items) +} + +// 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 + } + + // 处理售出逻辑:如果已售出但未提供时间,默认为当前时间 (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: strings.TrimSpace(payload.ProjectType), + 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 + } + 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 + } + + // 更新字段 + record.Name = strings.TrimSpace(payload.Name) + record.RegionID = payload.RegionID + record.Location = strings.TrimSpace(payload.Location) + record.ProjectType = strings.TrimSpace(payload.ProjectType) + 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 + } + 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 + } + 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 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("网关名称是必填项") + } + return nil +} + +// 错误处理函数 +func respondGatewayLookupError(c *gin.Context, err error) { + if errors.Is(err, gorm.ErrRecordNotFound) { + writeError(c, http.StatusNotFound, "未找到该网关") + return + } + writeError(c, http.StatusInternalServerError, "查询网关时出错") +} diff --git a/main.go b/main.go index 6395027..e07b91b 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func main() { &models.MqttStepCountRecord{}, &models.MqttGatewayStatusRecord{}, &models.MqttTrainingSessionRecord{}, + &models.Gateway{}, ) if err := models.BackfillLegacyUserPermissions(config.DB); err != nil { log.Printf("legacy user permission backfill failed: %v", err) diff --git a/models/device.go b/models/device.go new file mode 100644 index 0000000..b2f9484 --- /dev/null +++ b/models/device.go @@ -0,0 +1,39 @@ +package models + +import ( + "gorm.io/gorm" + "time" +) + +// Gateway 代表一个物联网网关设备 +type Gateway struct { + gorm.Model + // MAC地址是网关的唯一标识 + MAC string `gorm:"size:32;uniqueIndex;not null" json:"mac"` + + // 网关名称 + Name string `gorm:"size:255" json:"name"` + + // 所属区域ID,用于关联幼儿园或区域 + RegionID uint32 `gorm:"index" json:"regionId"` + + // 位置描述(如:一楼大厅、操场) + Location string `gorm:"size:255" json:"location"` + + // 新增字段:是否已经售出 + IsSold bool `json:"isSold"` + + ProjectType string `gorm:"size:255;default:'heartrate'" json:"projectType"` + + // 新增字段:售出时间 + // 注意:使用 *time.Time 允许为空(未售出时为空) + SoldAt *time.Time `json:"soldAt,omitempty"` + + // 是否启用 + IsActive bool `json:"isActive"` +} + +// TableName 指定数据库表名 +func (Gateway) TableName() string { + return "gateways" +} diff --git a/routes/routes.go b/routes/routes.go index a911e74..3707171 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -16,6 +16,7 @@ func SetupRouter() *gin.Engine { lessonPlanController := controllers.NewLessonPlanController() kindergartenAdminController := controllers.NewKindergartenAdminController() userAdminController := controllers.NewUserAdminController() + gatewayController := controllers.NewGatewayAdminController() systemDebugController := controllers.NewSystemDebugController() deviceTokenHandler := func(c *gin.Context) { clientSecret := c.GetHeader("X-API-Key") @@ -76,10 +77,16 @@ func SetupRouter() *gin.Engine { admin.PUT("/users/:id", userAdminController.Update) admin.DELETE("/users/:id", userAdminController.Delete) + admin.GET("/gateways", gatewayController.List) + admin.POST("/gateways", gatewayController.Create) + admin.PUT("/gateways/:id", gatewayController.Update) + admin.DELETE("/gateways/:id", gatewayController.Delete) + admin.GET("/system-debug/mqtt/status", systemDebugController.MqttStatus) admin.POST("/system-debug/mqtt/start", systemDebugController.StartMqtt) admin.POST("/system-debug/mqtt/stop", systemDebugController.StopMqtt) } + v1.GET("/admin/system-debug/mqtt/ws", systemDebugController.MqttWebSocket) v1.GET("/lesson-plans/share/:code/download", lessonPlanController.DownloadByShareCode) public := v1.Group("")