package selftest import ( "context" "database/sql" "fmt" "time" "weatherstation/internal/database" ) // Run 执行启动前自检 // 1) 数据库可用 2) 关键表存在 3) 基础查询可执行 func Run(ctx context.Context) error { db := database.GetDB() if err := pingDB(ctx, db); err != nil { return fmt.Errorf("数据库连通性失败: %w", err) } if err := ensureTables(ctx, db); err != nil { return fmt.Errorf("关键表缺失: %w", err) } if err := basicQuery(ctx, db); err != nil { return fmt.Errorf("基础查询失败: %w", err) } return nil } func pingDB(ctx context.Context, db *sql.DB) error { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() return db.PingContext(ctx) } func ensureTables(ctx context.Context, db *sql.DB) error { required := []string{ "public.stations", "public.rs485_weather_data", "public.rs485_weather_10min", "public.forecast_hourly", } for _, t := range required { var exists bool if err := db.QueryRowContext(ctx, "SELECT to_regclass($1) IS NOT NULL", t).Scan(&exists); err != nil { return err } if !exists { return fmt.Errorf("缺少表: %s", t) } } return nil } func basicQuery(ctx context.Context, db *sql.DB) error { // 若无站点则跳过后续测试 var stationID string _ = db.QueryRowContext(ctx, "SELECT station_id FROM stations LIMIT 1").Scan(&stationID) // 10分钟表可被查询(允许为空) var cnt int if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM rs485_weather_10min").Scan(&cnt); err != nil { return err } // 若有站点,做一次安全时间窗聚合验证(不要求有数据,只要语句可执行) if stationID != "" { from := time.Now().Add(-2 * time.Hour).Truncate(time.Hour) to := time.Now().Truncate(time.Hour) _, err := db.QueryContext(ctx, `WITH base AS ( SELECT * FROM rs485_weather_10min WHERE station_id=$1 AND bucket_start >= $2 AND bucket_start <= $3 ), g AS ( SELECT date_trunc('hour', bucket_start) AS grp, SUM(sample_count) AS n_sum FROM base GROUP BY 1 ) SELECT COUNT(*) FROM g`, stationID, from, to, ) if err != nil { return err } } return nil }