feat: gateway.
This commit is contained in:
+30
-1
@@ -72,6 +72,8 @@ func readDocxContentFromPath(filePath string) (string, error) {
|
|||||||
|
|
||||||
// readCSVContent 读取 .csv 文件内容
|
// readCSVContent 读取 .csv 文件内容
|
||||||
// 修改为先保存临时文件再读取
|
// 修改为先保存临时文件再读取
|
||||||
|
// readCSVContent 读取 .csv 文件内容
|
||||||
|
// 修改压缩策略:每 4 行保留 1 行数据
|
||||||
func readCSVContent(fileHeader *multipart.FileHeader) (string, error) {
|
func readCSVContent(fileHeader *multipart.FileHeader) (string, error) {
|
||||||
// 1. 创建临时文件
|
// 1. 创建临时文件
|
||||||
tempFile, err := os.CreateTemp("", "upload_*.csv")
|
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 "", 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 的提示词
|
// buildAnalysisPrompt 构建发送给 AI 的提示词
|
||||||
@@ -213,6 +237,11 @@ func buildAnalysisPrompt(teachingPlanContent, heartRateContent, analysisType, st
|
|||||||
|
|
||||||
// callAIForAnalysis 调用大模型进行分析
|
// callAIForAnalysis 调用大模型进行分析
|
||||||
func callAIForAnalysis(prompt string) (string, error) {
|
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()
|
baseURL, apiKey, model, err := config.GetAIConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -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, "查询网关时出错")
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ func main() {
|
|||||||
&models.MqttStepCountRecord{},
|
&models.MqttStepCountRecord{},
|
||||||
&models.MqttGatewayStatusRecord{},
|
&models.MqttGatewayStatusRecord{},
|
||||||
&models.MqttTrainingSessionRecord{},
|
&models.MqttTrainingSessionRecord{},
|
||||||
|
&models.Gateway{},
|
||||||
)
|
)
|
||||||
if err := models.BackfillLegacyUserPermissions(config.DB); err != nil {
|
if err := models.BackfillLegacyUserPermissions(config.DB); err != nil {
|
||||||
log.Printf("legacy user permission backfill failed: %v", err)
|
log.Printf("legacy user permission backfill failed: %v", err)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ func SetupRouter() *gin.Engine {
|
|||||||
lessonPlanController := controllers.NewLessonPlanController()
|
lessonPlanController := controllers.NewLessonPlanController()
|
||||||
kindergartenAdminController := controllers.NewKindergartenAdminController()
|
kindergartenAdminController := controllers.NewKindergartenAdminController()
|
||||||
userAdminController := controllers.NewUserAdminController()
|
userAdminController := controllers.NewUserAdminController()
|
||||||
|
gatewayController := controllers.NewGatewayAdminController()
|
||||||
systemDebugController := controllers.NewSystemDebugController()
|
systemDebugController := controllers.NewSystemDebugController()
|
||||||
deviceTokenHandler := func(c *gin.Context) {
|
deviceTokenHandler := func(c *gin.Context) {
|
||||||
clientSecret := c.GetHeader("X-API-Key")
|
clientSecret := c.GetHeader("X-API-Key")
|
||||||
@@ -76,10 +77,16 @@ func SetupRouter() *gin.Engine {
|
|||||||
admin.PUT("/users/:id", userAdminController.Update)
|
admin.PUT("/users/:id", userAdminController.Update)
|
||||||
admin.DELETE("/users/:id", userAdminController.Delete)
|
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.GET("/system-debug/mqtt/status", systemDebugController.MqttStatus)
|
||||||
admin.POST("/system-debug/mqtt/start", systemDebugController.StartMqtt)
|
admin.POST("/system-debug/mqtt/start", systemDebugController.StartMqtt)
|
||||||
admin.POST("/system-debug/mqtt/stop", systemDebugController.StopMqtt)
|
admin.POST("/system-debug/mqtt/stop", systemDebugController.StopMqtt)
|
||||||
}
|
}
|
||||||
|
|
||||||
v1.GET("/admin/system-debug/mqtt/ws", systemDebugController.MqttWebSocket)
|
v1.GET("/admin/system-debug/mqtt/ws", systemDebugController.MqttWebSocket)
|
||||||
v1.GET("/lesson-plans/share/:code/download", lessonPlanController.DownloadByShareCode)
|
v1.GET("/lesson-plans/share/:code/download", lessonPlanController.DownloadByShareCode)
|
||||||
public := v1.Group("")
|
public := v1.Group("")
|
||||||
|
|||||||
Reference in New Issue
Block a user