From f22e5751249992ae58170bacf0b22e4ad7562493 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Thu, 30 Jul 2026 23:49:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8F=90=E4=BE=9B=20SQLite=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E6=95=85=E9=9A=9C=E6=81=A2=E5=A4=8D=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 +++++++ CLAUDE.md | 3 +++ backend/main.go | 33 ++++++++++++++++++--------------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 042d872..68b6b27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -436,6 +436,13 @@ WebSocket 无法设置自定义 header → 后端 `JWTAuth` 支持 `?token=` que - 生产模式:MySQL + Redis。 - 告警引擎使用 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 健康检查 - 推荐探针:`GET /version`(无 DB 依赖)。 diff --git a/CLAUDE.md b/CLAUDE.md index 8bf4d78..ccca56c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -266,6 +266,9 @@ WebSocket 无法设自定义 header → 后端 JWTAuth 支持 `?token=` query pa 3. **新增 handler 不绑 model** — 用独立 request struct 4. **GORM 指针字段必须解引用** — `*field` 后再用 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 前端规范 diff --git a/backend/main.go b/backend/main.go index 1ed8774..630f32d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -12,20 +12,20 @@ import ( "syscall" "time" - "ezops/controller" - "ezops/router" "ezops/config" + "ezops/controller" "ezops/model" - "ezops/seed" "ezops/pkg/appdir" "ezops/pkg/auditcleaner" "ezops/pkg/billingsync" - "ezops/pkg/database" - snmppkg "ezops/pkg/snmp" "ezops/pkg/cronscheduler" - "ezops/pkg/systask" + "ezops/pkg/database" "ezops/pkg/logger" ezredis "ezops/pkg/redis" + snmppkg "ezops/pkg/snmp" + "ezops/pkg/systask" + "ezops/router" + "ezops/seed" "github.com/gin-gonic/gin" "github.com/spf13/viper" @@ -41,10 +41,11 @@ var ( // CLI 参数定义 // Logan.Li var ( - flagPort = flag.String("port", "", "服务端口 (默认: 8080)") - flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)") - flagMode = flag.String("mode", "", "运行模式: local (SQLite+redka), development, production, release") - flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error") + flagPort = flag.String("port", "", "服务端口 (默认: 8080)") + flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)") + flagMode = flag.String("mode", "", "运行模式: local (SQLite+redka), development, production, release") + 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)") @@ -63,8 +64,8 @@ var ( flagRedisDriver = flag.String("redis-driver", "", "Redis 驱动: redis (外部Redis), redka (内嵌,需CGO)") // JWT 参数 - flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥") - flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)") + flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥") + flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)") // 显示版本 flagVersion = flag.Bool("version", false, "显示版本信息") @@ -144,8 +145,10 @@ func main() { log.Fatal("数据库连接失败", zap.Error(err)) } - // 自动迁移 - if err := database.AutoMigrate(db); err != nil { + // 自动迁移;大 SQLite 库异常迁移时可显式跳过,先恢复已存在的服务。 // Logan.Li + if *flagSkipAutoMigrate { + log.Warn("已跳过数据库自动迁移,仅限已完成迁移的故障恢复场景") + } else if err := database.AutoMigrate(db); err != nil { log.Fatal("数据库迁移失败", zap.Error(err)) } @@ -409,7 +412,7 @@ func loadConfig() *config.Config { viper.SetDefault("redis.password", "") 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("websocket.read_buffer_size", 1024)