fix: 提供 SQLite 迁移故障恢复开关

This commit is contained in:
Logan.Li committed 2026-07-30 23:49:38 +08:00
1 parent 99b5d94dc2
commit f22e575124
3 files changed
+28 -15

No files matched your search

+7
View File
@@ -436,6 +436,13 @@ WebSocket 无法设置自定义 header → 后端 `JWTAuth` 支持 `?token=` que
- 生产模式:MySQL + Redis。 - 生产模式:MySQL + Redis。
- 告警引擎使用 Redis 分布式锁(SetNX + 60s TTL)保证多副本仅 Leader 执行检查。 - 告警引擎使用 Redis 分布式锁(SetNX + 60s TTL)保证多副本仅 Leader 执行检查。
### 7.4.1 数据保留与清理(强制)
- 对监控指标、Agent 原始上报、日志和审计等时序/高频数据,必须实现可配置的保留期与定时清理;禁止无限累积。
- 清理任务不能只调用 `DELETE`:必须记录并校验受影响行数,处理关联的明细/索引数据,并将执行结果写入系统任务状态,确保清理可审计、失败可发现。
- SQLite 的 `DELETE` 仅释放可复用页,不会缩小 `sqlite3.db` 文件。生产维护窗口内必须在备份成功、应用停写后执行 `PRAGMA wal_checkpoint(TRUNCATE)` 和 `VACUUM`,确认 WAL 与主库空间实际回收;不得在高峰期或存在活跃写入时执行 `VACUUM`。
- 大表不得在每次应用启动时无条件触发全表重建。结构变更必须使用可追踪、幂等的迁移,并评估 SQLite 下 GORM `AutoMigrate` 的重建与 WAL 写放大风险。
### 7.5 健康检查 ### 7.5 健康检查
- 推荐探针:`GET /version`(无 DB 依赖)。 - 推荐探针:`GET /version`(无 DB 依赖)。
+3
View File
@@ -266,6 +266,9 @@ WebSocket 无法设自定义 header → 后端 JWTAuth 支持 `?token=` query pa
3. **新增 handler 不绑 model** — 用独立 request struct 3. **新增 handler 不绑 model** — 用独立 request struct
4. **GORM 指针字段必须解引用** — `*field` 后再用 4. **GORM 指针字段必须解引用** — `*field` 后再用
5. **Update handler 禁止复用 CreateRequest** — 用 `map[string]interface{}` 接收 5. **Update handler 禁止复用 CreateRequest** — 用 `map[string]interface{}` 接收
6. **高频数据必须有保留策略** — 监控、Agent 上报、日志和审计数据必须可配置保留期、定时清理并记录影响行数和任务结果;不能无限累积。
7. **SQLite 清理必须回收文件空间** — `DELETE` 后文件不会自动变小。备份成功且应用停写的维护窗口内,执行 `PRAGMA wal_checkpoint(TRUNCATE)` 与 `VACUUM`,并验证主库/WAL 体积;高峰期禁止 `VACUUM`。
8. **避免启动期重建大表** — SQLite 下 GORM `AutoMigrate` 可能通过重建表完成结构变更并放大 WAL 写入;大表结构变更必须可追踪、幂等,不得每次启动无条件重建。
### ⛔ React 前端规范 ### ⛔ React 前端规范
+18 -15
View File
@@ -12,20 +12,20 @@ import (
"syscall" "syscall"
"time" "time"
"ezops/controller"
"ezops/router"
"ezops/config" "ezops/config"
"ezops/controller"
"ezops/model" "ezops/model"
"ezops/seed"
"ezops/pkg/appdir" "ezops/pkg/appdir"
"ezops/pkg/auditcleaner" "ezops/pkg/auditcleaner"
"ezops/pkg/billingsync" "ezops/pkg/billingsync"
"ezops/pkg/database"
snmppkg "ezops/pkg/snmp"
"ezops/pkg/cronscheduler" "ezops/pkg/cronscheduler"
"ezops/pkg/systask" "ezops/pkg/database"
"ezops/pkg/logger" "ezops/pkg/logger"
ezredis "ezops/pkg/redis" ezredis "ezops/pkg/redis"
snmppkg "ezops/pkg/snmp"
"ezops/pkg/systask"
"ezops/router"
"ezops/seed"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -41,10 +41,11 @@ var (
// CLI 参数定义 // Logan.Li // CLI 参数定义 // Logan.Li
var ( var (
flagPort = flag.String("port", "", "服务端口 (默认: 8080)") flagPort = flag.String("port", "", "服务端口 (默认: 8080)")
flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)") flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)")
flagMode = flag.String("mode", "", "运行模式: local (SQLite+redka), development, production, release") flagMode = flag.String("mode", "", "运行模式: local (SQLite+redka), development, production, release")
flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error") flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error")
flagSkipAutoMigrate = flag.Bool("skip-auto-migrate", false, "跳过启动时的数据库自动迁移(仅用于已完成迁移的故障恢复)")
// 数据库参数 // 数据库参数
flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)") flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)")
@@ -63,8 +64,8 @@ var (
flagRedisDriver = flag.String("redis-driver", "", "Redis 驱动: redis (外部Redis), redka (内嵌,需CGO)") flagRedisDriver = flag.String("redis-driver", "", "Redis 驱动: redis (外部Redis), redka (内嵌,需CGO)")
// JWT 参数 // JWT 参数
flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥") flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥")
flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)") flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)")
// 显示版本 // 显示版本
flagVersion = flag.Bool("version", false, "显示版本信息") flagVersion = flag.Bool("version", false, "显示版本信息")
@@ -144,8 +145,10 @@ func main() {
log.Fatal("数据库连接失败", zap.Error(err)) log.Fatal("数据库连接失败", zap.Error(err))
} }
// 自动迁移 // 自动迁移;大 SQLite 库异常迁移时可显式跳过,先恢复已存在的服务。 // Logan.Li
if err := database.AutoMigrate(db); err != nil { if *flagSkipAutoMigrate {
log.Warn("已跳过数据库自动迁移,仅限已完成迁移的故障恢复场景")
} else if err := database.AutoMigrate(db); err != nil {
log.Fatal("数据库迁移失败", zap.Error(err)) log.Fatal("数据库迁移失败", zap.Error(err))
} }
@@ -409,7 +412,7 @@ func loadConfig() *config.Config {
viper.SetDefault("redis.password", "") viper.SetDefault("redis.password", "")
viper.SetDefault("redis.db", 0) viper.SetDefault("redis.db", 0)
viper.SetDefault("jwt.secret", "") // 生产环境必须通过配置或 JWT_SECRET 环境变量设置 viper.SetDefault("jwt.secret", "") // 生产环境必须通过配置或 JWT_SECRET 环境变量设置
viper.SetDefault("jwt.expire_hour", 24) viper.SetDefault("jwt.expire_hour", 24)
viper.SetDefault("websocket.read_buffer_size", 1024) viper.SetDefault("websocket.read_buffer_size", 1024)