diff --git a/.env b/.env index 6bb1b4ce..920b3df9 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ # DB_TYPE=mysql -# DB_DSN="mysql://user:password@tcp(localhost:3306)/dbname" +# 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" +# DB_TYPE=postgres +# DB_DSN="postgres://postgres:pinenut666@localhost:5432/nokoti?sslmode=disable" # SQLITE IS DIFFERENT -# DB_TYPE=sqlite +DB_TYPE=sqlite # DATA_DIR="./data/default" diff --git a/dice/model/database/mysql.go b/dice/model/database/mysql.go index 2fd47d29..c7310bcd 100644 --- a/dice/model/database/mysql.go +++ b/dice/model/database/mysql.go @@ -14,8 +14,6 @@ import ( func MySQLDBInit(dsn 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 @@ -28,6 +26,7 @@ func MySQLDBInit(dsn string) (*gorm.DB, error) { if err != nil { return nil, err } + // 存疑,MYSQL是否需要使用缓存 cacheDB, err := cache.GetBuntCacheDB(db) if err != nil { return nil, err diff --git a/dice/model/db.go b/dice/model/db.go index 944154e3..f7a5b621 100644 --- a/dice/model/db.go +++ b/dice/model/db.go @@ -2,92 +2,118 @@ package model import ( "os" + "sync" "gorm.io/gorm" log "sealdice-core/utils/kratos" ) -func DatabaseInit() (dataDB *gorm.DB, logsDB *gorm.DB, err error) { +var ( + engine DatabaseOperator + once sync.Once + engineError error +) + +// initEngine 初始化数据库引擎,仅执行一次 +func initEngine() { dbType := os.Getenv("DB_TYPE") - var engine DatabaseOperator switch dbType { case SQLITE: + log.Info("当前选择使用: SQLITE数据库") engine = &SQLiteEngine{} case MYSQL: - // TODO - log.Warn("当前配置默认使用: SQLITE数据库") - engine = &SQLiteEngine{} + log.Info("当前选择使用: MYSQL数据库") + engine = &MYSQLEngine{} case POSTGRESQL: + log.Info("当前选择使用: POSTGRESQL数据库") engine = &PGSQLEngine{} default: - - log.Warn("当前配置默认使用: SQLITE数据库") + log.Warn("未配置数据库类型,默认使用: SQLITE数据库") engine = &SQLiteEngine{} } - err = engine.Init() + + engineError = engine.Init() + if engineError != nil { + log.Error("数据库引擎初始化失败:", engineError) + } +} + +// getEngine 获取数据库引擎,确保只初始化一次 +func getEngine() (DatabaseOperator, error) { + once.Do(initEngine) + return engine, engineError +} + +// DatabaseInit 初始化数据和日志数据库 +func DatabaseInit() (dataDB *gorm.DB, logsDB *gorm.DB, err error) { + engine, err = getEngine() if err != nil { return nil, nil, err } + dataDB, err = engine.DataDBInit() if err != nil { return nil, nil, err } + logsDB, err = engine.LogDBInit() if err != nil { return nil, nil, err } + // 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 { + // 错误处理 + log.Infof("Error querying LogOneItem: %v", err) + return nil, nil, err + } + + // 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 { + // 错误处理 + log.Errorf("Error updating LogInfo: %v", err) + return nil, nil, err + } + } + } return dataDB, logsDB, nil } +// DBCheck 检查数据库状态 func DBCheck() { - dbType := os.Getenv("DB_TYPE") - var engine DatabaseOperator - switch dbType { - case SQLITE: - log.Info("当前选择使用:SQLITE数据库") - engine = &SQLiteEngine{} - case MYSQL: - // TODO - log.Warn("当前配置:MYSQL未能实现,默认使用: SQLITE数据库") - engine = &SQLiteEngine{} - case POSTGRESQL: - log.Info("当前选择使用:PGSQL 数据库") - engine = &PGSQLEngine{} - default: - log.Warn("当前配置默认使用: SQLITE数据库") - engine = &SQLiteEngine{} - } - err := engine.Init() + dbEngine, err := getEngine() if err != nil { + log.Error("数据库引擎获取失败:", err) return } - engine.DBCheck() + + dbEngine.DBCheck() } +// CensorDBInit 初始化敏感词数据库 func CensorDBInit() (censorDB *gorm.DB, err error) { - dbType := os.Getenv("DB_TYPE") - var engine DatabaseOperator - switch dbType { - case SQLITE: - engine = &SQLiteEngine{} - case MYSQL: - // TODO - log.Warn("当前配置默认使用: SQLITE数据库") - engine = &SQLiteEngine{} - case POSTGRESQL: - engine = &PGSQLEngine{} - default: - log.Warn("当前配置默认使用: SQLITE数据库") - engine = &SQLiteEngine{} - } - err = engine.Init() + censorEngine, err := getEngine() if err != nil { return nil, err } - init, err := engine.CensorDBInit() - if err != nil { - return nil, err - } - return init, nil + + return censorEngine.CensorDBInit() } diff --git a/dice/model/db_utils.go b/dice/model/db_utils.go index 5e917d5d..4f81e31b 100644 --- a/dice/model/db_utils.go +++ b/dice/model/db_utils.go @@ -4,9 +4,6 @@ import ( "database/sql/driver" "errors" "fmt" - "os" - "path/filepath" - "runtime" "strings" "sync" @@ -46,46 +43,6 @@ func (j BYTE) Value() (driver.Value, error) { return []byte(j), nil } -// DBCacheDelete 删除SQLite数据库缓存文件 -// TODO: 判断缓存是否应该被删除 -func DBCacheDelete() bool { - dataDir := "./data/default" - - 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 - } - - // 非 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") - } - if ok { - ok = tryDelete("data-logs.db-wal") - } - if ok { - ok = tryDelete("data-censor.db-shm") - } - if ok { - ok = tryDelete("data-censor.db-wal") - } - return ok -} - // DBVacuum 整理数据库 func DBVacuum() { done := make(chan interface{}, 1) 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 index aa0b36e4..a734840e 100644 --- a/dice/model/engine_pgsql.go +++ b/dice/model/engine_pgsql.go @@ -12,6 +12,7 @@ import ( type PGSQLEngine struct { DSN string + DB *gorm.DB } func (s *PGSQLEngine) Init() error { @@ -19,22 +20,23 @@ func (s *PGSQLEngine) Init() error { 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 } -// DB检查 +// DBCheck DB检查 func (s *PGSQLEngine) DBCheck() { fmt.Fprintln(os.Stdout, "PostGRESQL 海豹不提供检查,请自行检查数据库!") } -// 初始化 +// DataDBInit 初始化 func (s *PGSQLEngine) DataDBInit() (*gorm.DB, error) { - dataDB, err := database.PostgresDBInit(s.DSN) - if err != nil { - return nil, err - } // data建表 - err = dataDB.AutoMigrate( + err := s.DB.AutoMigrate( &GroupPlayerInfoBase{}, &GroupInfo{}, &BanInfo{}, @@ -44,29 +46,21 @@ func (s *PGSQLEngine) DataDBInit() (*gorm.DB, error) { if err != nil { return nil, err } - return dataDB, nil + return s.DB, nil } func (s *PGSQLEngine) LogDBInit() (*gorm.DB, error) { - logsDB, err := database.PostgresDBInit(s.DSN) - if err != nil { - return nil, err - } // logs建表 - if err = logsDB.AutoMigrate(&LogInfo{}); err != nil { + if err := s.DB.AutoMigrate(&LogInfo{}, &LogOneItem{}); err != nil { return nil, err } - return logsDB, nil + return s.DB, nil } func (s *PGSQLEngine) CensorDBInit() (*gorm.DB, error) { - censorDB, err := database.PostgresDBInit(s.DSN) - if err != nil { - return nil, err - } // 创建基本的表结构,并通过标签定义索引 - if err = censorDB.AutoMigrate(&CensorLog{}); err != nil { + if err := s.DB.AutoMigrate(&CensorLog{}); err != nil { return nil, err } - return censorDB, nil + return s.DB, nil } 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 编辑日志