diff --git a/.env b/.env new file mode 100644 index 00000000..920b3df9 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +# DB_TYPE=mysql +# DB_DSN="root:root@tcp(127.0.0.1:3306)/nokoti?charset=utf8mb4&parseTime=True&loc=Local" + +# DB_TYPE=postgres +# DB_DSN="postgres://postgres:pinenut666@localhost:5432/nokoti?sslmode=disable" +# SQLITE IS DIFFERENT +DB_TYPE=sqlite +# DATA_DIR="./data/default" diff --git a/dice/config.go b/dice/config.go index f191ba13..56fd1e82 100644 --- a/dice/config.go +++ b/dice/config.go @@ -2521,7 +2521,12 @@ func (d *Dice) Save(isAuto bool) { if groupInfo.Players != nil { groupInfo.Players.Range(func(key string, value *GroupPlayerInfo) bool { if value.UpdatedAtTime != 0 { - _ = model.GroupPlayerInfoSave(d.DBData, groupInfo.GroupID, key, (*model.GroupPlayerInfoBase)(value)) + // 解离数据库层的操作到调用处,设置对应的信息 + now := int(time.Now().Unix()) + value.UserID = key + value.GroupID = groupInfo.GroupID + value.UpdatedAt = now // 更新当前时间为 UpdatedAt + _ = model.GroupPlayerInfoSave(d.DBData, (*model.GroupPlayerInfoBase)(value)) value.UpdatedAtTime = 0 } return true diff --git a/dice/dice.go b/dice/dice.go index aee72f9c..f0f3803b 100644 --- a/dice/dice.go +++ b/dice/dice.go @@ -229,7 +229,7 @@ func (d *Dice) Init() { d.CocExtraRules = map[int]*CocRuleInfo{} var err error - d.DBData, d.DBLogs, err = model.SQLiteDBInit(d.BaseConfig.DataDir) + d.DBData, d.DBLogs, err = model.DatabaseInit() if err != nil { d.Logger.Errorf("Failed to init database: %v", err) } diff --git a/dice/dice_censor.go b/dice/dice_censor.go index 561b2f5e..55f49194 100644 --- a/dice/dice_censor.go +++ b/dice/dice_censor.go @@ -59,7 +59,7 @@ type CensorManager struct { } func (d *Dice) NewCensorManager() { - db, err := model.SQLiteCensorDBInit(d.BaseConfig.DataDir) + db, err := model.CensorDBInit() if err != nil { panic(err) } diff --git a/dice/model/attr.go b/dice/model/attr.go deleted file mode 100644 index 1df3ef9b..00000000 --- a/dice/model/attr.go +++ /dev/null @@ -1,41 +0,0 @@ -package model - -import ( - log "sealdice-core/utils/kratos" - - "github.com/jmoiron/sqlx" -) - -// 废弃代码先不改 - -func attrGetAllBase(db *sqlx.DB, bucket string, key string) []byte { - var buf []byte - - query := `SELECT updated_at, data FROM ` + bucket + ` WHERE id=:id` - rows, err := db.NamedQuery(query, map[string]interface{}{"id": key}) - if err != nil { - log.Errorf("Failed to execute query: %v", err) - return buf - } - - defer rows.Close() - - for rows.Next() { - var updatedAt int64 - var data []byte - - err := rows.Scan(&updatedAt, &data) - if err != nil { - log.Errorf("Failed to scan row: %v", err) - break - } - - buf = data - } - - return buf -} - -func AttrUserGetAll(db *sqlx.DB, userID string) []byte { - return attrGetAllBase(db, "attrs_user", userID) -} diff --git a/dice/model/attrs_new.go b/dice/model/attrs_new.go index 50904f2f..eae1ce87 100644 --- a/dice/model/attrs_new.go +++ b/dice/model/attrs_new.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/tidwall/gjson" "gorm.io/gorm" "sealdice-core/utils" @@ -119,10 +118,8 @@ func AttrsPutById(db *gorm.DB, id string, data []byte, name, sheetType string) e Attrs(map[string]any{ // 第一次全量建表 "id": id, - // 如果想在[]bytes里输入值,注意传参的时候不能给any传[]bytes,否则会无法读取,同时还没有豹错,浪费大量时间。 - // 这里为了兼容,不使用gob的序列化方法处理结构体(同时,也不知道序列化方法是否可用) - // TODO: 是否在这里string(data)更快更合理? - "data": gjson.ParseBytes(data).String(), + // 使用BYTE规避无法插入的问题 + "data": BYTE(data), "is_hidden": true, "binding_sheet_id": "", "name": name, @@ -132,7 +129,7 @@ func AttrsPutById(db *gorm.DB, id string, data []byte, name, sheetType string) e }). // 如果是更新的情况,更新下面这部分,则需要被更新的为: Assign(map[string]any{ - "data": gjson.ParseBytes(data).String(), + "data": BYTE(data), "updated_at": now, "name": name, "sheet_type": sheetType, @@ -229,7 +226,7 @@ func AttrsBindCharacter(db *gorm.DB, charId string, id string) error { "id": id, // 如果想在[]bytes里输入值,注意传参的时候不能给any传[]bytes,否则会无法读取,同时还没有豹错,浪费大量时间。 // 这里为了兼容,不使用gob的序列化方法处理结构体(同时,也不知道序列化方法是否可用) - "data": gjson.ParseBytes(json).String(), + "data": BYTE(json), "is_hidden": true, // 如果插入成功,原版代码接下来更新这个值,那么现在就是等价的 "binding_sheet_id": charId, diff --git a/dice/model/ban.go b/dice/model/ban.go index becae7dc..755ff942 100644 --- a/dice/model/ban.go +++ b/dice/model/ban.go @@ -1,7 +1,6 @@ package model import ( - "github.com/tidwall/gjson" "gorm.io/gorm" ) @@ -31,17 +30,16 @@ func BanItemSave(db *gorm.DB, id string, updatedAt int64, banUpdatedAt int64, da if err := db.Where("id = ?", id).Attrs(map[string]any{ "id": id, "updated_at": int(updatedAt), - "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 - "data": gjson.ParseBytes(data).String(), // 禁用项数据 + "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 + "data": BYTE(data), // 禁用项数据 }). Assign(map[string]any{ "updated_at": int(updatedAt), - "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 - "data": gjson.ParseBytes(data).String(), // 禁用项数据 + "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 + "data": BYTE(data), // 禁用项数据 }).FirstOrCreate(&BanInfo{}).Error; err != nil { return err // 返回错误 } - return nil // 操作成功,返回 nil } diff --git a/dice/model/const.go b/dice/model/const.go new file mode 100644 index 00000000..cd2cb7ff --- /dev/null +++ b/dice/model/const.go @@ -0,0 +1,7 @@ +package model + +const ( + SQLITE = "sqlite" + MYSQL = "mysql" + POSTGRESQL = "postgres" +) diff --git a/dice/model/gormcache.go b/dice/model/database/cache/gormcache.go similarity index 99% rename from dice/model/gormcache.go rename to dice/model/database/cache/gormcache.go index aae79bfb..4e964717 100644 --- a/dice/model/gormcache.go +++ b/dice/model/database/cache/gormcache.go @@ -1,4 +1,4 @@ -package model +package cache import ( "context" diff --git a/dice/model/database/mysql.go b/dice/model/database/mysql.go new file mode 100644 index 00000000..c7310bcd --- /dev/null +++ b/dice/model/database/mysql.go @@ -0,0 +1,36 @@ +package database + +import ( + "log" + "os" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func MySQLDBInit(dsn string) (*gorm.DB, error) { + // 构建 MySQL DSN (Data Source Name) + // 使用 GORM 连接 MySQL + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // 慢 SQL 阈值 + LogLevel: logger.Info, // 记录所有SQL操作 + Colorful: true, // 是否启用彩色打印 + }, + )}) + if err != nil { + return nil, err + } + // 存疑,MYSQL是否需要使用缓存 + cacheDB, err := cache.GetBuntCacheDB(db) + if err != nil { + return nil, err + } + // 返回数据库连接 + return cacheDB, nil +} diff --git a/dice/model/database/pgsql.go b/dice/model/database/pgsql.go new file mode 100644 index 00000000..1f74cb05 --- /dev/null +++ b/dice/model/database/pgsql.go @@ -0,0 +1,41 @@ +package database + +import ( + "log" + "os" + "time" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func PostgresDBInit(dsn string) (*gorm.DB, error) { + // 构建 PostgreSQL DSN (Data Source Name) + + // 使用 GORM 连接 PostgreSQL + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // 慢 SQL 阈值 + LogLevel: logger.Info, // 记录所有SQL操作 + Colorful: true, // 是否启用彩色打印 + }, + ), + }) + if err != nil { + return nil, err + } + + // GetBuntCacheDB 逻辑保持不变 + cacheDB, err := cache.GetBuntCacheDB(db) + if err != nil { + return nil, err + } + + // 返回数据库连接 + return cacheDB, nil +} diff --git a/dice/model/database/sqlite.go b/dice/model/database/sqlite.go new file mode 100644 index 00000000..3e87ac9f --- /dev/null +++ b/dice/model/database/sqlite.go @@ -0,0 +1,38 @@ +//go:build !cgo +// +build !cgo + +package database + +import ( + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + // https://github.com/glebarez/sqlite/issues/52 尚未遇见问题,可以先考虑不使用 + // sqlDB, _ := db.DB() + // sqlDB.SetMaxOpenConns(1) + if err != nil { + return nil, err + } + // Enable Cache Mode + db, err = cache.GetBuntCacheDB(db) + if err != nil { + return nil, err + } + // enable WAL mode + if useWAL { + err = db.Exec("PRAGMA journal_mode=WAL").Error + if err != nil { + return nil, err + } + } + return db, err +} diff --git a/dice/model/database/sqlite_cgo.go b/dice/model/database/sqlite_cgo.go new file mode 100644 index 00000000..37fa52ff --- /dev/null +++ b/dice/model/database/sqlite_cgo.go @@ -0,0 +1,36 @@ +//go:build cgo +// +build cgo + +package database + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/dice/model/database/cache" +) + +func SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) { + open, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return nil, err + } + // Enable Cache Mode + open, err = cache.GetBuntCacheDB(open) + if err != nil { + return nil, err + } + // enable WAL mode + if useWAL { + err = open.Exec("PRAGMA journal_mode=WAL").Error + if err != nil { + panic(err) + } + } + + return open, err +} diff --git a/dice/model/db.go b/dice/model/db.go index 80d2dea0..902abeca 100644 --- a/dice/model/db.go +++ b/dice/model/db.go @@ -1,280 +1,119 @@ package model import ( - "fmt" "os" - "path/filepath" - "strings" + "sync" "gorm.io/gorm" log "sealdice-core/utils/kratos" ) -func DBCheck(dataDir string) { - checkDB := func(db *gorm.DB) bool { - rows, err := db.Exec("PRAGMA integrity_check").Rows() - if err != nil { - return false - } - defer rows.Close() - var ok bool - for rows.Next() { - var s string - if errR := rows.Scan(&s); errR != nil { - ok = false - break - } - fmt.Fprintln(os.Stdout, s) - if s == "ok" { - ok = true - } - } +var ( + engine DatabaseOperator + once sync.Once + errEngineInstance error +) - if errR := rows.Err(); errR != nil { - ok = false - } - return ok +// initEngine 初始化数据库引擎,仅执行一次 +func initEngine() { + dbType := os.Getenv("DB_TYPE") + switch dbType { + case SQLITE: + log.Info("当前选择使用: SQLITE数据库") + engine = &SQLiteEngine{} + case MYSQL: + log.Info("当前选择使用: MYSQL数据库") + engine = &MYSQLEngine{} + case POSTGRESQL: + log.Info("当前选择使用: POSTGRESQL数据库") + engine = &PGSQLEngine{} + default: + log.Warn("未配置数据库类型,默认使用: SQLITE数据库") + engine = &SQLiteEngine{} + } + + errEngineInstance = engine.Init() + if errEngineInstance != nil { + log.Error("数据库引擎初始化失败:", errEngineInstance) } +} - var ok1, ok2, ok3 bool - var dataDB *gorm.DB - var logsDB *gorm.DB - var censorDB *gorm.DB - var err error - - dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) - dataDB, err = _SQLiteDBInit(dbDataPath, false) - if err != nil { - fmt.Fprintln(os.Stdout, "数据库 data.db 无法打开") - } else { - ok1 = checkDB(dataDB) - db, _ := dataDB.DB() - // 关闭 - db.Close() - } +// getEngine 获取数据库引擎,确保只初始化一次 +func getEngine() (DatabaseOperator, error) { + once.Do(initEngine) + return engine, errEngineInstance +} - dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) - logsDB, err = _SQLiteDBInit(dbDataLogsPath, false) +// DatabaseInit 初始化数据和日志数据库 +func DatabaseInit() (dataDB *gorm.DB, logsDB *gorm.DB, err error) { + engine, err = getEngine() if err != nil { - fmt.Fprintln(os.Stdout, "数据库 data-logs.db 无法打开") - } else { - ok2 = checkDB(logsDB) - db, _ := logsDB.DB() - // 关闭db - db.Close() + return nil, nil, err } - dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) - censorDB, err = _SQLiteDBInit(dbDataCensorPath, false) + dataDB, err = engine.DataDBInit() if err != nil { - fmt.Fprintln(os.Stdout, "数据库 data-censor.db 无法打开") - } else { - ok3 = checkDB(censorDB) - db, _ := censorDB.DB() - // 关闭db - db.Close() + return nil, nil, err } - fmt.Fprintln(os.Stdout, "数据库检查结果:") - fmt.Fprintln(os.Stdout, "data.db:", ok1) - fmt.Fprintln(os.Stdout, "data-logs.db:", ok2) - fmt.Fprintln(os.Stdout, "data-censor.db:", ok3) -} - -var createSql = ` -CREATE TABLE attrs__temp ( - id TEXT PRIMARY KEY, - data BYTEA, - attrs_type TEXT, - binding_sheet_id TEXT default '', - name TEXT default '', - owner_id TEXT default '', - sheet_type TEXT default '', - is_hidden BOOLEAN default FALSE, - created_at INTEGER default 0, - updated_at INTEGER default 0 -); -` - -func SQLiteDBInit(dataDir string) (dataDB *gorm.DB, logsDB *gorm.DB, err error) { - dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) - dataDB, err = _SQLiteDBInit(dbDataPath, true) + logsDB, err = engine.LogDBInit() if err != nil { return nil, nil, err } - // 特殊情况建表语句处置 - if strings.Contains(dataDB.Dialector.Name(), "sqlite") { - tx := dataDB.Begin() - // 检查是否有这个影响的注释 - var count int64 - err = dataDB.Raw("SELECT count(*) FROM `sqlite_master` WHERE tbl_name = 'attrs' AND `sql` LIKE '%这个方法太严格了%'").Count(&count).Error + // TODO: 将这段逻辑挪移到Migrator上 + var ids []uint64 + var logItemSums []struct { + LogID uint64 + Count int64 + } + logsDB.Model(&LogInfo{}).Where("size IS NULL").Pluck("id", &ids) + if len(ids) > 0 { + // 根据 LogInfo 表中的 IDs 查找对应的 LogOneItem 记录 + err = logsDB.Model(&LogOneItem{}). + Where("log_id IN ?", ids). + Group("log_id"). + Select("log_id, COUNT(*) AS count"). // 如果需要求和其他字段,可以使用 Sum + Scan(&logItemSums).Error if err != nil { - tx.Rollback() + // 错误处理 + log.Infof("Error querying LogOneItem: %v", err) return nil, nil, err } - if count > 0 { - log.Warn("数据库 attrs 表结构为前置测试版本150,重建中") - // 创建临时表 - err = tx.Exec(createSql).Error - if err != nil { - tx.Rollback() - return nil, nil, err - } - // 迁移数据 - err = tx.Exec("INSERT INTO `attrs__temp` SELECT * FROM `attrs`").Error - if err != nil { - tx.Rollback() - return nil, nil, err - } - // 删除旧的表 - err = tx.Exec("DROP TABLE `attrs`").Error - if err != nil { - tx.Rollback() - return nil, nil, err - } - // 改名 - err = tx.Exec("ALTER TABLE `attrs__temp` RENAME TO `attrs`").Error + + // 2. 更新 LogInfo 表的 Size 字段 + for _, sum := range logItemSums { + // 将求和结果更新到对应的 LogInfo 的 Size 字段 + err = logsDB.Model(&LogInfo{}). + Where("id = ?", sum.LogID). + UpdateColumn("size", sum.Count).Error // 或者是 sum.Time 等,如果要是其他字段的求和 if err != nil { - tx.Rollback() + // 错误处理 + log.Errorf("Error updating LogInfo: %v", err) return nil, nil, err } - tx.Commit() } } - // data建表 - err = dataDB.AutoMigrate( - &GroupPlayerInfoBase{}, - &GroupInfo{}, - &BanInfo{}, - &EndpointInfo{}, - &AttributesItemModel{}, - ) - if err != nil { - return nil, nil, err - } - logsDB, err = LogDBInit(dataDir) - return + return dataDB, logsDB, nil } -// LogDBInit SQLITE初始化 -func LogDBInit(dataDir string) (logsDB *gorm.DB, err error) { - dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) - logsDB, err = _SQLiteDBInit(dbDataLogsPath, true) +// DBCheck 检查数据库状态 +func DBCheck() { + dbEngine, err := getEngine() if err != nil { + log.Error("数据库引擎获取失败:", err) return } - // logs建表 - if err = logsDB.AutoMigrate(&LogInfo{}); err != nil { - return nil, err - } - - itemsAutoMigrate := false - dialect := logsDB.Dialector.Name() - if dialect != "sqlite" { - itemsAutoMigrate = true - } else { - if logsDB.Migrator().HasTable(&LogOneItem{}) { - if err = logItemsSQLiteMigrate(logsDB); err != nil { - return nil, err - } - } else { - itemsAutoMigrate = true - } - } - if itemsAutoMigrate { - if err = logsDB.AutoMigrate(&LogOneItem{}); err != nil { - return nil, err - } - } - - if err != nil { - return nil, err - } - return logsDB, nil + dbEngine.DBCheck() } -func logItemsSQLiteMigrate(db *gorm.DB) error { - type DBColumn struct { - Name string - Type string - } - - // 获取当前列信息 - var currentColumns []DBColumn - err := db.Raw("PRAGMA table_info(log_items)").Scan(¤tColumns).Error - if err != nil { - return err - } - - // 获取模型定义的列信息 - var modelColumns []DBColumn - stmt := &gorm.Statement{DB: db} - err = stmt.Parse(&LogOneItem{}) +// CensorDBInit 初始化敏感词数据库 +func CensorDBInit() (censorDB *gorm.DB, err error) { + censorEngine, err := getEngine() if err != nil { - return err - } - for _, field := range stmt.Schema.Fields { - if field.DBName != "" { - x := db.Migrator().FullDataTypeOf(field) - col := strings.SplitN(x.SQL, " ", 2)[0] - modelColumns = append(modelColumns, DBColumn{field.DBName, strings.ToLower(col)}) - } - } - - // 比较列是否有变化 - needMigrate := false - if len(currentColumns) != len(modelColumns) { - needMigrate = true - } else { - columnMap := make(map[string]string) - for _, col := range currentColumns { - columnMap[col.Name] = strings.ToLower(col.Type) - } - - for _, col := range modelColumns { - newType := col.Type - currentType := columnMap[col.Name] - - // 特殊处理 is_dice 列,允许 bool 或 numeric 类型 - if col.Name == "is_dice" { - if currentType != "bool" && currentType != "numeric" { - needMigrate = true - break - } - continue - } - - if currentType != newType { - needMigrate = true - break - } - } - } - - // 如果需要迁移则执行 - if needMigrate { - log.Info("现在进行log_items表的迁移,如果数据库较大,会花费较长时间,请耐心等待") - log.Info("若是迁移后观察到数据库体积显著膨胀,可以关闭骰子使用 sealdice-core --vacuum 进行数据库整理,这同样会花费较长时间") - return db.AutoMigrate() - } - - return nil -} - -func SQLiteCensorDBInit(dataDir string) (censorDB *gorm.DB, err error) { - path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) - if err != nil { - return nil, err - } - censorDB, err = _SQLiteDBInit(path, true) - if err != nil { - return nil, err - } - // 创建基本的表结构,并通过标签定义索引 - if err = censorDB.AutoMigrate(&CensorLog{}); err != nil { return nil, err } - return censorDB, nil + + return censorEngine.CensorDBInit() } diff --git a/dice/model/db_init.go b/dice/model/db_init.go deleted file mode 100644 index b38d9819..00000000 --- a/dice/model/db_init.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build !cgo -// +build !cgo - -package model - -import ( - "github.com/glebarez/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -// _SQLiteDBInit 初始化 SQLite 数据库连接 -// 警告:这个替代品的封装应该有建表问题,修正之前请谨慎使用它 -// 非CGO的另一个替代品使用了WASM方案:https://github.com/ncruces/go-sqlite3/tree/main/gormlite -func _SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) { - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{ - // 注意,这里虽然是Info,但实际上打印就变成了Debug. - Logger: logger.Default.LogMode(logger.Info), - }) - // https://github.com/glebarez/sqlite/issues/52 尚未遇见问题,可以先考虑不使用 - // sqlDB, _ := db.DB() - // sqlDB.SetMaxOpenConns(1) - if err != nil { - return nil, err - } - // Enable Cache Mode - db, err = GetBuntCacheDB(db) - if err != nil { - return nil, err - } - // enable WAL mode - if useWAL { - err = db.Exec("PRAGMA journal_mode=WAL").Error - if err != nil { - return nil, err - } - } - return db, err -} - -// _MySQLDBInit 初始化 MySQL 数据库连接 暂时不用它 -// func _MySQLDBInit(user, password, host, dbName string) (*gorm.DB, error) { -// // 构建 MySQL DSN (Data Source Name) -// dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, dbName) -// -// // 使用 GORM 连接 MySQL -// db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.New( -// log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer -// logger.Config{ -// SlowThreshold: time.Second, // 慢 SQL 阈值 -// LogLevel: logger.Info, // 记录所有SQL操作 -// Colorful: true, // 是否启用彩色打印 -// }, -// )}) -// if err != nil { -// return nil, err -// } -// -// // 返回数据库连接 -// return db, nil -// } diff --git a/dice/model/db_init_cgo.go b/dice/model/db_init_cgo.go deleted file mode 100644 index 49ba40bd..00000000 --- a/dice/model/db_init_cgo.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build cgo -// +build cgo - -package model - -import ( - _ "github.com/mattn/go-sqlite3" // sqlite3 driver - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -func _SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) { - open, err := gorm.Open(sqlite.Open(path), &gorm.Config{ - // 注意,这里虽然是Info,但实际上打印就变成了Debug. - Logger: logger.Default.LogMode(logger.Info), - }) - if err != nil { - return nil, err - } - // Enable Cache Mode - open, err = GetBuntCacheDB(open) - if err != nil { - return nil, err - } - // enable WAL mode - if useWAL { - err = open.Exec("PRAGMA journal_mode=WAL").Error - if err != nil { - panic(err) - } - } - - return open, err -} - -// _MySQLDBInit 初始化 MySQL 数据库连接 测试专用 -// func _MySQLDBInit(user, password, host, dbName string) (*gorm.DB, error) { -// // 构建 MySQL DSN (Data Source Name) -// dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, dbName) -// -// // 使用 GORM 连接 MySQL -// db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.New( -// log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer -// logger.Config{ -// SlowThreshold: time.Second, // 慢 SQL 阈值 -// LogLevel: logger.Info, // 记录所有SQL操作 -// Colorful: true, // 是否启用彩色打印 -// }, -// )}) -// if err != nil { -// return nil, err -// } -// cacheDB, err := GetBuntCacheDB(db) -// if err != nil { -// return nil, err -// } -// // 返回数据库连接 -// return cacheDB, nil -// } diff --git a/dice/model/db_utils.go b/dice/model/db_utils.go index eda09cb7..e24eea7d 100644 --- a/dice/model/db_utils.go +++ b/dice/model/db_utils.go @@ -1,54 +1,46 @@ package model import ( - "os" - "path/filepath" - "runtime" + "database/sql/driver" + "errors" + "fmt" "strings" "sync" + "sealdice-core/dice/model/database" log "sealdice-core/utils/kratos" "sealdice-core/utils/spinner" ) -// DBCacheDelete 删除SQLite数据库缓存文件 -// TODO: 判断缓存是否应该被删除 -func DBCacheDelete() bool { - dataDir := "./data/default" +// BYTES类 +// 如果我们使用FirstOrCreate,不可避免的会遇到这样的问题: +// 传入的是BYTE数组,由于使用了any会被转换为[]int8,而gorm又不会处理这种数据,进而导致转换失败 +// 通过强制设置一个封装,可以确认any的类型,进而避免转换失败 - tryDelete := func(fn string) bool { - fnPath, _ := filepath.Abs(filepath.Join(dataDir, fn)) - if _, err := os.Stat(fnPath); err != nil { - // 文件不在了,就当作删除成功 - return true - } - return os.Remove(fnPath) == nil - } +// 定义一个新的类型 JSON,封装 []byte +type BYTE []byte - // 非 Windows 系统不删除缓存 - if runtime.GOOS != "windows" { - return true - } - ok := true - if ok { - ok = tryDelete("data.db-shm") - } - if ok { - ok = tryDelete("data.db-wal") - } - if ok { - ok = tryDelete("data-logs.db-shm") +// Scan 实现 sql.Scanner 接口,用于扫描数据库中的 JSON 数据 +func (j *BYTE) Scan(value interface{}) error { + // 将数据库中的值转换为 []byte + bytes, ok := value.([]byte) + if !ok { + return errors.New(fmt.Sprint("Failed to unmarshal JSON value:", value)) } - if ok { - ok = tryDelete("data-logs.db-wal") - } - if ok { - ok = tryDelete("data-censor.db-shm") - } - if ok { - ok = tryDelete("data-censor.db-wal") + + // 将 []byte 赋值给 JSON 类型的指针 + *j = bytes + return nil +} + +// Value 实现 driver.Valuer 接口,用于将 JSON 类型存储到数据库中 +func (j BYTE) Value() (driver.Value, error) { + // 如果 BYTE 数据为空,则返回 nil + if len(j) == 0 { + return nil, nil //nolint:nilnil } - return ok + // 返回原始的 []byte + return []byte(j), nil } // DBVacuum 整理数据库 @@ -67,7 +59,7 @@ func DBVacuum() { vacuum := func(path string, wg *sync.WaitGroup) { defer wg.Done() // 使用 GORM 初始化数据库 - vacuumDB, err := _SQLiteDBInit(path, true) + vacuumDB, err := database.SQLiteDBInit(path, true) // 数据库类型不是 SQLite 直接返回 if !strings.Contains(vacuumDB.Dialector.Name(), "sqlite") { return diff --git a/dice/model/engine_interface.go b/dice/model/engine_interface.go new file mode 100644 index 00000000..28281f9b --- /dev/null +++ b/dice/model/engine_interface.go @@ -0,0 +1,13 @@ +package model + +import "gorm.io/gorm" + +// DatabaseOperator 本来是单独放了个文件夹的,但是由于现在所有的model都和处理逻辑在一起,如果放在单独文件夹必然会循环依赖 +// 只能放在外面 +type DatabaseOperator interface { + Init() error + DBCheck() + DataDBInit() (*gorm.DB, error) + LogDBInit() (*gorm.DB, error) + CensorDBInit() (*gorm.DB, error) +} diff --git a/dice/model/engine_mysql.go b/dice/model/engine_mysql.go new file mode 100644 index 00000000..06fbf46d --- /dev/null +++ b/dice/model/engine_mysql.go @@ -0,0 +1,163 @@ +package model + +import ( + "errors" + "fmt" + "os" + + "gorm.io/gorm" + + "sealdice-core/dice/model/database" + log "sealdice-core/utils/kratos" +) + +type MYSQLEngine struct { + DSN string + DB *gorm.DB +} + +type LogInfoHookMySQL struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + Name string `json:"name" gorm:"column:name"` + GroupID string `json:"groupId" gorm:"column:group_id"` + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` + UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"` + Size *int `json:"size" gorm:"<-:false"` + Extra *string `json:"-" gorm:"column:extra"` + UploadURL string `json:"-" gorm:"column:upload_url"` + UploadTime int `json:"-" gorm:"column:upload_time"` +} + +func (*LogInfoHookMySQL) TableName() string { + return "logs" +} + +type LogOneItemHookMySQL struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + LogID uint64 `json:"-" gorm:"column:log_id"` + GroupID string `gorm:"column:group_id"` + Nickname string `json:"nickname" gorm:"column:nickname"` + IMUserID string `json:"IMUserId" gorm:"column:im_userid"` + Time int64 `json:"time" gorm:"column:time"` + Message string `json:"message" gorm:"column:message"` + IsDice bool `json:"isDice" gorm:"column:is_dice"` + CommandID int64 `json:"commandId" gorm:"column:command_id"` + CommandInfo interface{} `json:"commandInfo" gorm:"-"` + CommandInfoStr string `json:"-" gorm:"column:command_info"` + RawMsgID interface{} `json:"rawMsgId" gorm:"-"` + RawMsgIDStr string `json:"-" gorm:"column:raw_msg_id"` + UniformID string `json:"uniformId" gorm:"column:user_uniform_id"` + Channel string `json:"channel" gorm:"-"` + Removed *int `gorm:"column:removed" json:"-"` + ParentID *int `gorm:"column:parent_id" json:"-"` +} + +func (*LogOneItemHookMySQL) TableName() string { + return "log_items" +} + +// 利用前缀索引,规避索引BUG +// 创建不出来也没关系,反正MYSQL数据库 +func createIndexForLogInfo(db *gorm.DB) (err error) { + // 创建前缀索引 + // 检查并创建索引 + if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_log_name") { + err = db.Exec("CREATE INDEX idx_log_name ON logs (name(20));").Error + if err != nil { + log.Errorf("创建idx_log_name索引失败,原因为 %v", err) + } + } + + if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_logs_group") { + err = db.Exec("CREATE INDEX idx_logs_group ON logs (group_id(20));").Error + if err != nil { + log.Errorf("创建idx_logs_group索引失败,原因为 %v", err) + } + } + + if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_logs_updated_at") { + err = db.Exec("CREATE INDEX idx_logs_updated_at ON logs (updated_at);").Error + if err != nil { + log.Errorf("创建idx_logs_updated_at索引失败,原因为 %v", err) + } + } + return nil +} + +func createIndexForLogOneItem(db *gorm.DB) (err error) { + // 创建前缀索引 + // 检查并创建索引 + if !db.Migrator().HasIndex(&LogOneItemHookMySQL{}, "idx_log_items_group_id") { + err = db.Exec("CREATE INDEX idx_log_items_group_id ON log_items(group_id(20))").Error + if err != nil { + log.Errorf("创建idx_logs_group索引失败,原因为 %v", err) + } + } + if !db.Migrator().HasIndex(&LogOneItemHookMySQL{}, "idx_raw_msg_id") { + err = db.Exec("CREATE INDEX idx_raw_msg_id ON log_items(raw_msg_id(20))").Error + if err != nil { + log.Errorf("创建idx_log_group_id_name索引失败,原因为 %v", err) + } + } + // MYSQL似乎不能创建前缀联合索引,放弃所有的前缀联合索引 + return nil +} + +func (s *MYSQLEngine) Init() error { + s.DSN = os.Getenv("DB_DSN") + if s.DSN == "" { + return errors.New("DB_DSN is missing") + } + var err error + s.DB, err = database.MySQLDBInit(s.DSN) + if err != nil { + return err + } + return nil +} + +// DBCheck DB检查 +func (s *MYSQLEngine) DBCheck() { + fmt.Fprintln(os.Stdout, "MYSQL 海豹不提供检查,请自行检查数据库!") +} + +// DataDBInit 初始化 +func (s *MYSQLEngine) DataDBInit() (*gorm.DB, error) { + err := s.DB.AutoMigrate( + // TODO: 这个的索引有没有必要进行修改 + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *MYSQLEngine) LogDBInit() (*gorm.DB, error) { + // logs特殊建表 + if err := s.DB.AutoMigrate(&LogInfoHookMySQL{}, &LogOneItemHookMySQL{}); err != nil { + return nil, err + } + // logs建立索引 + err := createIndexForLogInfo(s.DB) + if err != nil { + return nil, err + } + err = createIndexForLogOneItem(s.DB) + if err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *MYSQLEngine) CensorDBInit() (*gorm.DB, error) { + // 创建基本的表结构,并通过标签定义索引 + if err := s.DB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err + } + return s.DB, nil +} diff --git a/dice/model/engine_pgsql.go b/dice/model/engine_pgsql.go new file mode 100644 index 00000000..a734840e --- /dev/null +++ b/dice/model/engine_pgsql.go @@ -0,0 +1,66 @@ +package model + +import ( + "errors" + "fmt" + "os" + + "gorm.io/gorm" + + "sealdice-core/dice/model/database" +) + +type PGSQLEngine struct { + DSN string + DB *gorm.DB +} + +func (s *PGSQLEngine) Init() error { + s.DSN = os.Getenv("DB_DSN") + if s.DSN == "" { + return errors.New("DB_DSN is missing") + } + var err error + s.DB, err = database.PostgresDBInit(s.DSN) + if err != nil { + return err + } + return nil +} + +// DBCheck DB检查 +func (s *PGSQLEngine) DBCheck() { + fmt.Fprintln(os.Stdout, "PostGRESQL 海豹不提供检查,请自行检查数据库!") +} + +// DataDBInit 初始化 +func (s *PGSQLEngine) DataDBInit() (*gorm.DB, error) { + // data建表 + err := s.DB.AutoMigrate( + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *PGSQLEngine) LogDBInit() (*gorm.DB, error) { + // logs建表 + if err := s.DB.AutoMigrate(&LogInfo{}, &LogOneItem{}); err != nil { + return nil, err + } + return s.DB, nil +} + +func (s *PGSQLEngine) CensorDBInit() (*gorm.DB, error) { + // 创建基本的表结构,并通过标签定义索引 + if err := s.DB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err + } + return s.DB, nil +} diff --git a/dice/model/engine_sqlite.go b/dice/model/engine_sqlite.go new file mode 100644 index 00000000..57caac34 --- /dev/null +++ b/dice/model/engine_sqlite.go @@ -0,0 +1,291 @@ +package model + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "gorm.io/gorm" + + "sealdice-core/dice/model/database" + log "sealdice-core/utils/kratos" +) + +type SQLiteEngine struct { + DataDir string +} + +const defaultDataDir = "./data/default" + +const createSql = ` +CREATE TABLE attrs__temp ( + id TEXT PRIMARY KEY, + data BYTEA, + attrs_type TEXT, + binding_sheet_id TEXT default '', + name TEXT default '', + owner_id TEXT default '', + sheet_type TEXT default '', + is_hidden BOOLEAN default FALSE, + created_at INTEGER default 0, + updated_at INTEGER default 0 +); +` + +func (s *SQLiteEngine) Init() error { + s.DataDir = os.Getenv("DATADIR") + if s.DataDir == "" { + log.Debug("未能发现SQLITE定义位置,使用默认data地址") + s.DataDir = defaultDataDir + } + return nil +} + +// DB检查 BUG FIXME +func (s *SQLiteEngine) DBCheck() { + dataDir := s.DataDir + checkDB := func(db *gorm.DB) bool { + rows, err := db.Exec("PRAGMA integrity_check").Rows() + if err != nil { + return false + } + defer rows.Close() + var ok bool + for rows.Next() { + var s string + if errR := rows.Scan(&s); errR != nil { + ok = false + break + } + fmt.Fprintln(os.Stdout, s) + if s == "ok" { + ok = true + } + } + + if errR := rows.Err(); errR != nil { + ok = false + } + return ok + } + + var ok1, ok2, ok3 bool + var dataDB *gorm.DB + var logsDB *gorm.DB + var censorDB *gorm.DB + var err error + + dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) + dataDB, err = database.SQLiteDBInit(dbDataPath, false) + if err != nil { + fmt.Fprintln(os.Stdout, "数据库 data.db 无法打开") + } else { + ok1 = checkDB(dataDB) + db, _ := dataDB.DB() + // 关闭 + db.Close() + } + + dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) + logsDB, err = database.SQLiteDBInit(dbDataLogsPath, false) + if err != nil { + fmt.Fprintln(os.Stdout, "数据库 data-logs.db 无法打开") + } else { + ok2 = checkDB(logsDB) + db, _ := logsDB.DB() + // 关闭db + db.Close() + } + + dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) + censorDB, err = database.SQLiteDBInit(dbDataCensorPath, false) + if err != nil { + fmt.Fprintln(os.Stdout, "数据库 data-censor.db 无法打开") + } else { + ok3 = checkDB(censorDB) + db, _ := censorDB.DB() + // 关闭db + db.Close() + } + + fmt.Fprintln(os.Stdout, "数据库检查结果:") + fmt.Fprintln(os.Stdout, "data.db:", ok1) + fmt.Fprintln(os.Stdout, "data-logs.db:", ok2) + fmt.Fprintln(os.Stdout, "data-censor.db:", ok3) +} + +// 初始化 +func (s *SQLiteEngine) DataDBInit() (*gorm.DB, error) { + dbDataPath, _ := filepath.Abs(filepath.Join(s.DataDir, "data.db")) + dataDB, err := database.SQLiteDBInit(dbDataPath, true) + if err != nil { + return nil, err + } + // 特殊情况建表语句处置 + tx := dataDB.Begin() + // 检查是否有这个影响的注释 + var count int64 + err = dataDB.Raw("SELECT count(*) FROM `sqlite_master` WHERE tbl_name = 'attrs' AND `sql` LIKE '%这个方法太严格了%'").Count(&count).Error + if err != nil { + tx.Rollback() + return nil, err + } + if count > 0 { + log.Warn("数据库 attrs 表结构为前置测试版本150,重建中") + // 创建临时表 + err = tx.Exec(createSql).Error + if err != nil { + tx.Rollback() + return nil, err + } + // 迁移数据 + err = tx.Exec("INSERT INTO `attrs__temp` SELECT * FROM `attrs`").Error + if err != nil { + tx.Rollback() + return nil, err + } + // 删除旧的表 + err = tx.Exec("DROP TABLE `attrs`").Error + if err != nil { + tx.Rollback() + return nil, err + } + // 改名 + err = tx.Exec("ALTER TABLE `attrs__temp` RENAME TO `attrs`").Error + if err != nil { + tx.Rollback() + return nil, err + } + tx.Commit() + } + + // data建表 + err = dataDB.AutoMigrate( + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, err + } + return dataDB, nil +} + +func (s *SQLiteEngine) LogDBInit() (*gorm.DB, error) { + dbDataLogsPath, _ := filepath.Abs(filepath.Join(s.DataDir, "data-logs.db")) + logsDB, err := database.SQLiteDBInit(dbDataLogsPath, true) + if err != nil { + return nil, err + } + // logs建表 + if err = logsDB.AutoMigrate(&LogInfo{}); err != nil { + return nil, err + } + + itemsAutoMigrate := false + // 用于确认是否需要重建LogOneItem数据库 + if logsDB.Migrator().HasTable(&LogOneItem{}) { + if err = logItemsSQLiteMigrate(logsDB); err != nil { + return nil, err + } + } else { + itemsAutoMigrate = true + } + if itemsAutoMigrate { + if err = logsDB.AutoMigrate(&LogOneItem{}); err != nil { + return nil, err + } + } + return logsDB, nil +} + +func (s *SQLiteEngine) CensorDBInit() (*gorm.DB, error) { + dataDir := os.Getenv("DATA_DIR") + if dataDir == "" { + dataDir = defaultDataDir + } + path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) + if err != nil { + return nil, err + } + censorDB, err := database.SQLiteDBInit(path, true) + if err != nil { + return nil, err + } + // 创建基本的表结构,并通过标签定义索引 + if err = censorDB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err + } + return censorDB, nil +} + +func logItemsSQLiteMigrate(db *gorm.DB) error { + type DBColumn struct { + Name string + Type string + } + + // 获取当前列信息 + var currentColumns []DBColumn + err := db.Raw("PRAGMA table_info(log_items)").Scan(¤tColumns).Error + if err != nil { + return err + } + + // 获取模型定义的列信息 + var modelColumns []DBColumn + stmt := &gorm.Statement{DB: db} + err = stmt.Parse(&LogOneItem{}) + if err != nil { + return err + } + for _, field := range stmt.Schema.Fields { + if field.DBName != "" { + x := db.Migrator().FullDataTypeOf(field) + col := strings.SplitN(x.SQL, " ", 2)[0] + modelColumns = append(modelColumns, DBColumn{field.DBName, strings.ToLower(col)}) + } + } + + // 比较列是否有变化 + needMigrate := false + if len(currentColumns) != len(modelColumns) { + needMigrate = true + } else { + columnMap := make(map[string]string) + for _, col := range currentColumns { + columnMap[col.Name] = strings.ToLower(col.Type) + } + + for _, col := range modelColumns { + newType := col.Type + currentType := columnMap[col.Name] + + // 特殊处理 is_dice 列,允许 bool 或 numeric 类型 + if col.Name == "is_dice" { + if currentType != "bool" && currentType != "numeric" { + needMigrate = true + break + } + continue + } + + if currentType != newType { + needMigrate = true + break + } + } + } + + // 如果需要迁移则执行 + if needMigrate { + log.Info("现在进行log_items表的迁移,如果数据库较大,会花费较长时间,请耐心等待") + log.Info("若是迁移后观察到数据库体积显著膨胀,可以关闭骰子使用 sealdice-core --vacuum 进行数据库整理,这同样会花费较长时间") + return db.AutoMigrate() + } + + return nil +} diff --git a/dice/model/group_info.go b/dice/model/group_info.go index 1ba8074b..1c7320e0 100644 --- a/dice/model/group_info.go +++ b/dice/model/group_info.go @@ -1,8 +1,6 @@ package model import ( - "time" - "golang.org/x/time/rate" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -74,7 +72,9 @@ func GroupInfoSave(db *gorm.DB, groupID string, updatedAt int64, data []byte) er // GroupPlayerInfoBase 群内玩家信息 type GroupPlayerInfoBase struct { - Name string `yaml:"name" jsbind:"name" gorm:"column:name"` // 玩家昵称 + // 补充这个字段,从而保证包含主键ID + ID uint `yaml:"-" jsbind:"-" gorm:"column:id;primaryKey;autoIncrement"` // 主键ID字段,自增 + Name string `yaml:"name" jsbind:"name" gorm:"column:name"` // 玩家昵称 UserID string `yaml:"userId" jsbind:"userId" gorm:"column:user_id;index:idx_group_player_info_user_id; uniqueIndex:idx_group_player_info_group_user"` // 非数据库信息:是否在群内 InGroup bool `yaml:"inGroup" gorm:"-"` // 是否在群内,有时一个人走了,信息还暂时残留 @@ -156,15 +156,11 @@ func GroupPlayerInfoGet(db *gorm.DB, groupID string, playerID string) *GroupPlay } // GroupPlayerInfoSave 保存玩家信息,不再使用 REPLACE INTO 语句 -func GroupPlayerInfoSave(db *gorm.DB, groupID string, playerID string, info *GroupPlayerInfoBase) error { +func GroupPlayerInfoSave(db *gorm.DB, info *GroupPlayerInfoBase) error { // 考虑到info是指针,为了防止可能info还会被用到其他地方,这里的给info指针赋值也是有意义的 // 但强烈建议将这段去除掉,数据库层面理论上不应该混杂业务层逻辑? - now := int(time.Now().Unix()) - info.UserID = playerID - info.GroupID = groupID - info.UpdatedAt = now // 更新当前时间为 UpdatedAt - // 判断条件:联合主键相同 + // TODO: 那自增的ID是干嘛的…… conditions := map[string]any{ "user_id": info.UserID, "group_id": info.GroupID, diff --git a/dice/model/log.go b/dice/model/log.go index cfd86ee0..68628fad 100644 --- a/dice/model/log.go +++ b/dice/model/log.go @@ -85,8 +85,8 @@ func (item *LogOneItem) AfterFind(_ *gorm.DB) (err error) { type LogInfo struct { ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` - Name string `json:"name" gorm:"index:idx_log_group_id_name,unique;size:200"` - GroupID string `json:"groupId" gorm:"index:idx_logs_group;index:idx_log_group_id_name,unique;size:200"` + Name string `json:"name" gorm:"index:idx_log_group_id_name,unique"` + GroupID string `json:"groupId" gorm:"index:idx_logs_group;index:idx_log_group_id_name,unique"` CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at;index:idx_logs_update_at"` // 允许数据库NULL值 @@ -95,7 +95,7 @@ type LogInfo struct { // 使用GORM:<-:false 无写入权限,这样它就不会建库,但请注意,下面LogGetLogPage处,如果你查询出的名称不是size // 不能在这里绑定column,因为column会给你建立那一列。 // TODO: 将这个字段使用上会不会比后台查询就JOIN更合适? - Size *int `json:"size" gorm:"<-:false"` + Size *int `json:"size" gorm:"column:size"` // 数据库里有,json不展示的 // 允许数据库NULL值(该字段当前不使用) Extra *string `json:"-" gorm:"column:extra"` @@ -168,10 +168,8 @@ type QueryLogPage struct { func LogGetLogPage(db *gorm.DB, param *QueryLogPage) (int, []*LogInfo, error) { var lst []*LogInfo - // 构建查询 - query := db.Model(&LogInfo{}).Select("logs.id, logs.name, logs.group_id, logs.created_at, logs.updated_at, COUNT(log_items.id) as size"). - Joins("LEFT JOIN log_items ON logs.id = log_items.log_id") - + // 构建基础查询 + query := db.Model(&LogInfo{}).Select("logs.id, logs.name, logs.group_id, logs.created_at, logs.updated_at,COALESCE(logs.size, 0) as size").Order("logs.updated_at desc") // 添加条件 if param.Name != "" { query = query.Where("logs.name LIKE ?", "%"+param.Name+"%") @@ -258,7 +256,6 @@ func LogGetUploadInfo(db *gorm.DB, groupID string, logName string) (url string, updateTime = logInfo.UpdatedAt url = logInfo.UploadURL uploadTime = logInfo.UploadTime - return } @@ -447,8 +444,13 @@ func LogAppend(db *gorm.DB, groupID string, logName string, logItem *LogOneItem) return false } - // 更新 logs 表中的 updated_at 字段 - if err = tx.Model(&LogInfo{}).Where("id = ?", logID).Update("updated_at", nowTimestamp).Error; err != nil { + // 更新 logs 表中的 updated_at 字段 和 size 字段 + if err = tx.Model(&LogInfo{}). + Where("id = ?", logID). + Updates(map[string]interface{}{ + "updated_at": nowTimestamp, + "size": gorm.Expr("COALESCE(size, 0) + ?", 1), + }).Error; err != nil { return false } @@ -465,13 +467,26 @@ func LogMarkDeleteByMsgID(db *gorm.DB, groupID string, logName string, rawID int return err } rid := fmt.Sprintf("%v", rawID) - // TODO:如果索引工作不理想,我们或许要在这里使用Index Hint指定索引,目前好像还没出问题。 - if err = db.Where("log_id = ? AND raw_msg_id = ?", logID, rid).Delete(&LogOneItem{}).Error; err != nil { + tx := db.Begin() + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + if err = tx.Where("log_id = ? AND raw_msg_id = ?", logID, rid).Delete(&LogOneItem{}).Error; err != nil { log.Errorf("log delete error %s", err.Error()) return err } - - return nil + // 更新 logs 表中的 updated_at 字段 和 size 字段 + // 真的有默认为NULL还能触发删除的情况吗?! + if err = tx.Model(&LogInfo{}).Where("id = ?", logID).Updates(map[string]interface{}{ + "updated_at": time.Now().Unix(), + "size": gorm.Expr("COALESCE(size, 0) - ?", 1), + }).Error; err != nil { + return err + } + err = tx.Commit().Error + return err } // LogEditByMsgID 编辑日志 diff --git a/go.mod b/go.mod index 23b453cb..c2bf0975 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.2.1 // indirect github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/bits-and-blooms/bitset v1.12.0 // indirect @@ -124,6 +125,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect github.com/gobuffalo/envy v1.7.0 // indirect @@ -137,9 +139,13 @@ require ( github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect github.com/hhrutter/lzw v1.0.0 // indirect github.com/hhrutter/tiff v1.0.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.3.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect @@ -186,6 +192,8 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/driver/postgres v1.5.11 // indirect modernc.org/libc v1.37.6 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect diff --git a/go.sum b/go.sum index 1f12c53d..cf0420ed 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,7 @@ github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqx github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= @@ -212,6 +213,14 @@ github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -222,6 +231,8 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juliangruber/go-intersect v1.1.0 h1:sc+y5dCjMMx0pAdYk/N6KBm00tD/f3tq+Iox7dYDUrY= @@ -612,8 +623,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276 h1:IHpexPpZZkm4NqbKneioNEYxTpOGZnDm8HPjabyX+Uw= gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= diff --git a/main.go b/main.go index 37190dc6..bbc452c1 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/gofrs/flock" "github.com/jessevdk/go-flags" + "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "go.uber.org/zap/zapcore" @@ -213,8 +214,12 @@ func main() { LogLevel int8 `long:"log-level" description:"设置日志等级" default:"0" choice:"-1" choice:"0" choice:"1" choice:"2" choice:"3" choice:"4" choice:"5"` ContainerMode bool `long:"container-mode" description:"容器模式,该模式下禁用内置客户端"` } - - _, err := flags.ParseArgs(&opts, os.Args) + // 读取env参数 + err := godotenv.Load() + if err != nil { + log.Errorf("未读取到.env参数,若您未使用docker或第三方数据库,可安全忽略。") + } + _, err = flags.ParseArgs(&opts, os.Args) if err != nil { return } @@ -248,7 +253,7 @@ func main() { return } if opts.DBCheck { - model.DBCheck("data/default") + model.DBCheck() return } if opts.VacuumDB {