Compare commits

..

2 Commits

Author SHA1 Message Date
47b8c5bc65 增加 timescaledb 处理逻辑和gin索引 2025-09-24 16:34:16 +08:00
b668f3fbb5 定义一个配置记录是不是timescaledb 2025-09-24 16:06:05 +08:00
7 changed files with 72 additions and 1 deletions

View File

@@ -1,5 +1,11 @@
# 猪场管理系统 # 猪场管理系统
## 安装说明
### 推荐使用 TimescaleDB
TimescaleDB 是基于 PostgreSQL 的开源数据库, 专门为处理时序数据而设计的。可以应对后续传海量传感器数据
## 功能介绍 ## 功能介绍
### 一. 猪舍控制 ### 一. 猪舍控制

View File

@@ -29,6 +29,7 @@ database:
password: "pig-farm-controller" password: "pig-farm-controller"
dbname: "pig-farm-controller" dbname: "pig-farm-controller"
sslmode: "disable" # 在生产环境中建议使用 "require" sslmode: "disable" # 在生产环境中建议使用 "require"
is_timescaledb: false
max_open_conns: 25 # 最大开放连接数 max_open_conns: 25 # 最大开放连接数
max_idle_conns: 10 # 最大空闲连接数 max_idle_conns: 10 # 最大空闲连接数
conn_max_lifetime: 600 # 连接最大生命周期(秒) conn_max_lifetime: 600 # 连接最大生命周期(秒)

View File

@@ -81,6 +81,9 @@ type DatabaseConfig struct {
// SSLMode SSL模式 // SSLMode SSL模式
SSLMode string `yaml:"sslmode"` SSLMode string `yaml:"sslmode"`
// IsTimescaleDB is timescaledb
IsTimescaleDB bool `yaml:"is_timescaledb"`
// MaxOpenConns 最大开放连接数 // MaxOpenConns 最大开放连接数
MaxOpenConns int `yaml:"max_open_conns"` MaxOpenConns int `yaml:"max_open_conns"`

View File

@@ -16,6 +16,7 @@ import (
// 使用GORM作为ORM库 // 使用GORM作为ORM库
type PostgresStorage struct { type PostgresStorage struct {
db *gorm.DB db *gorm.DB
isTimescaleDB bool
connectionString string connectionString string
maxOpenConns int maxOpenConns int
maxIdleConns int maxIdleConns int
@@ -25,9 +26,10 @@ type PostgresStorage struct {
// NewPostgresStorage 创建并返回一个新的PostgreSQL存储实例 // NewPostgresStorage 创建并返回一个新的PostgreSQL存储实例
// 它接收一个 logger 实例,而不是自己创建 // 它接收一个 logger 实例,而不是自己创建
func NewPostgresStorage(connectionString string, maxOpenConns, maxIdleConns, connMaxLifetime int, logger *logs.Logger) *PostgresStorage { func NewPostgresStorage(connectionString string, isTimescaleDB bool, maxOpenConns, maxIdleConns, connMaxLifetime int, logger *logs.Logger) *PostgresStorage {
return &PostgresStorage{ return &PostgresStorage{
connectionString: connectionString, connectionString: connectionString,
isTimescaleDB: isTimescaleDB,
maxOpenConns: maxOpenConns, maxOpenConns: maxOpenConns,
maxIdleConns: maxIdleConns, maxIdleConns: maxIdleConns,
connMaxLifetime: connMaxLifetime, connMaxLifetime: connMaxLifetime,
@@ -116,5 +118,31 @@ func (ps *PostgresStorage) Migrate(models ...interface{}) error {
return fmt.Errorf("数据库表结构迁移失败: %w", err) return fmt.Errorf("数据库表结构迁移失败: %w", err)
} }
ps.logger.Info("数据库表结构迁移完成") ps.logger.Info("数据库表结构迁移完成")
// -- 处理gorm做不到的初始化逻辑 --
// 创建GIN索引(用于优化JSONB查询)
ps.logger.Info("正在为 sensor_data 表的 data 字段创建 GIN 索引")
// 使用 IF NOT EXISTS 保证幂等性
// 如果索引已存在,此命令不会报错
ginIndexSQL := "CREATE INDEX IF NOT EXISTS idx_sensor_data_data_gin ON sensor_data USING GIN (data);"
if err := ps.db.Exec(ginIndexSQL).Error; err != nil {
ps.logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err)
return fmt.Errorf("为 sensor_data 的 data 字段创建 GIN 索引失败: %w", err)
}
ps.logger.Info("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
// 如果是 TimescaleDB, 则将 sensor_data 转换为 hypertable
if ps.isTimescaleDB {
ps.logger.Info("检测到 TimescaleDB, 准备转换 sensor_data 为超表")
// 使用 if_not_exists => TRUE 保证幂等性
// 如果 sensor_data 已经是超表,此命令不会报错
// 'time' 是 SensorData 模型中定义的时间列
sql := "SELECT create_hypertable('sensor_data', 'time', if_not_exists => TRUE);"
if err := ps.db.Exec(sql).Error; err != nil {
ps.logger.Errorw("将 sensor_data 转换为超表失败", "error", err)
return fmt.Errorf("将 sensor_data 转换为超表失败: %w", err)
}
ps.logger.Info("成功将 sensor_data 转换为超表 (或已转换)")
}
return nil return nil
} }

View File

@@ -42,9 +42,11 @@ func NewStorage(cfg config.DatabaseConfig, logger *logs.Logger) Storage {
cfg.SSLMode, cfg.SSLMode,
) )
// 当前默认返回PostgreSQL存储实现并将 logger 注入
// 当前默认返回PostgreSQL存储实现并将 logger 注入 // 当前默认返回PostgreSQL存储实现并将 logger 注入
return NewPostgresStorage( return NewPostgresStorage(
connectionString, connectionString,
cfg.IsTimescaleDB, // <--- 添加 IsTimescaleDB
cfg.MaxOpenConns, cfg.MaxOpenConns,
cfg.MaxIdleConns, cfg.MaxIdleConns,
cfg.ConnMaxLifetime, cfg.ConnMaxLifetime,

View File

@@ -0,0 +1,30 @@
package models
import (
"time"
"gorm.io/datatypes"
)
// SensorData 存储所有类型的传感器数据,对应数据库中的 'sensor_data' 表。
type SensorData struct {
// Time 是数据记录的时间戳,作为复合主键的一部分。
// GORM 会将其映射到 'time' TIMESTAMPTZ 列。
Time time.Time `gorm:"primaryKey" json:"time"`
// DeviceID 是传感器的唯一标识符,作为复合主键的另一部分。
// GORM 会将其映射到 'device_id' VARCHAR(50) 列。
DeviceID uint `gorm:"primaryKey" json:"device_id"`
// RegionalControllerID 是上报此数据的区域主控的ID。
// 我们为其添加了数据库索引以优化按区域查询的性能。
RegionalControllerID uint `json:"regional_controller_id"`
// Data 存储一个或多个传感器读数,格式为 JSON。
// GORM 会使用 'jsonb' 类型来创建此列。
Data datatypes.JSON `gorm:"type:jsonb" json:"data"`
}
func (SensorData) TableName() string {
return "sensor_data"
}

View File

@@ -12,5 +12,6 @@ func GetAllModels() []interface{} {
&PlanExecutionLog{}, &PlanExecutionLog{},
&TaskExecutionLog{}, &TaskExecutionLog{},
&PendingTask{}, &PendingTask{},
&SensorData{},
} }
} }