From cd7e3296002661b08cdd8c992f42603f9587c2fd Mon Sep 17 00:00:00 2001 From: PaienNate <68044286+PaienNate@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:33:55 +0800 Subject: [PATCH] =?UTF-8?q?refactor(database):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=95=B0=E6=8D=AE=E5=BA=93=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=BA=93=E8=87=B3gorm=20(#1060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: JustAnotherID --- .gitignore | 1 + api/api_bind.go | 18 +- api/censor.go | 9 +- dice/builtin_commands.go | 2 +- dice/dice.go | 15 +- dice/dice_attrs_manager.go | 24 +- dice/dice_censor.go | 4 +- dice/im_session.go | 5 +- dice/model/attr.go | 2 + dice/model/attrs_new.go | 327 ++++++++++------ dice/model/backup.go | 36 +- dice/model/ban.go | 70 ++-- dice/model/censor_log.go | 162 ++++---- dice/model/db.go | 272 ++++++-------- dice/model/db_init.go | 58 ++- dice/model/db_init_cgo.go | 48 ++- dice/model/db_utils.go | 48 ++- dice/model/endpoint_info.go | 61 +-- dice/model/gormcache.go | 132 +++++++ dice/model/group_info.go | 230 ++++++----- dice/model/log.go | 523 +++++++++++++------------- dice/model/sqlhook.go | 83 ---- dice/model/sqlhook_cgo.go | 83 ---- dice/platform_adapter_gocq_actions.go | 1 + dice/storylog/storylog.go | 4 +- go.mod | 25 +- go.sum | 45 ++- main.go | 55 ++- migrate/db_util.go | 15 +- migrate/db_util_cgo.go | 14 +- migrate/v150_attrs.go | 7 +- utils/convertdb.go | 23 ++ utils/kratos/gormlogger.go | 113 ++++++ utils/kratos/zap.go | 25 +- utils/paniclog/paniclog.go | 10 +- 35 files changed, 1493 insertions(+), 1057 deletions(-) create mode 100644 dice/model/gormcache.go delete mode 100644 dice/model/sqlhook.go delete mode 100644 dice/model/sqlhook_cgo.go create mode 100644 utils/convertdb.go create mode 100644 utils/kratos/gormlogger.go diff --git a/.gitignore b/.gitignore index 61484bea..4569a6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ _help_cache .vscode/ !.vscode/settings.json +sealdice-lock.lock diff --git a/api/api_bind.go b/api/api_bind.go index b06ef4af..16c43513 100644 --- a/api/api_bind.go +++ b/api/api_bind.go @@ -190,7 +190,11 @@ func forceStop(c echo.Context) error { dbData := d.DBData if dbData != nil { d.DBData = nil - _ = dbData.Close() + db, err := dbData.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -201,7 +205,11 @@ func forceStop(c echo.Context) error { dbLogs := d.DBLogs if dbLogs != nil { d.DBLogs = nil - _ = dbLogs.Close() + db, err := dbLogs.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -213,7 +221,11 @@ func forceStop(c echo.Context) error { if cm != nil && cm.DB != nil { dbCensor := cm.DB cm.DB = nil - _ = dbCensor.Close() + db, err := dbCensor.DB() + if err != nil { + return + } + _ = db.Close() } })() } diff --git a/api/censor.go b/api/censor.go index f8a14fab..093ad5b5 100644 --- a/api/censor.go +++ b/api/censor.go @@ -64,7 +64,14 @@ func censorStop(c echo.Context) error { (&myDice.Config).EnableCensor = false myDice.MarkModified() - _ = myDice.CensorManager.DB.Close() + db, err2 := myDice.CensorManager.DB.DB() + if err2 != nil { + return Error(&c, "关闭拦截引擎失败", Response{}) + } + err = db.Close() + if err != nil { + return err + } myDice.CensorManager = nil return Success(&c, Response{}) diff --git a/dice/builtin_commands.go b/dice/builtin_commands.go index 4f26b8d8..e29fb0f4 100644 --- a/dice/builtin_commands.go +++ b/dice/builtin_commands.go @@ -1917,7 +1917,7 @@ func (d *Dice) registerCoreCommands() { setCurPlayerName(b) } attrs.LastModifiedTime = time.Now().Unix() - attrs.SaveToDB(am.db, nil) // 直接保存 + attrs.SaveToDB(am.db) // 直接保存 ReplyToSender(ctx, msg, "操作完成") } else { ReplyToSender(ctx, msg, "此角色名已存在") diff --git a/dice/dice.go b/dice/dice.go index f76be485..edbdd3a3 100644 --- a/dice/dice.go +++ b/dice/dice.go @@ -14,13 +14,13 @@ import ( "github.com/dop251/goja_nodejs/eventloop" "github.com/dop251/goja_nodejs/require" "github.com/go-creed/sat" - "github.com/jmoiron/sqlx" wr "github.com/mroth/weightedrand" "github.com/robfig/cron/v3" ds "github.com/sealdice/dicescript" "github.com/tidwall/buntdb" rand2 "golang.org/x/exp/rand" "golang.org/x/exp/slices" + "gorm.io/gorm" "sealdice-core/dice/logger" "sealdice-core/dice/model" @@ -133,8 +133,8 @@ type Dice struct { LastUpdatedTime int64 `yaml:"-"` TextMap map[string]*wr.Chooser `yaml:"-"` BaseConfig BaseConfig `yaml:"-"` - DBData *sqlx.DB `yaml:"-"` // 数据库对象 - DBLogs *sqlx.DB `yaml:"-"` // 数据库对象 + DBData *gorm.DB `yaml:"-"` // 数据库对象 + DBLogs *gorm.DB `yaml:"-"` // 数据库对象 Logger *log.Helper `yaml:"-"` // 日志 LogWriter *log.WriterX `yaml:"-"` // 用于api的log对象 IsDeckLoading bool `yaml:"-"` // 正在加载中 @@ -160,10 +160,11 @@ type Dice struct { AliveNoticeEntry cron.EntryID `yaml:"-" json:"-"` JsPrinter *PrinterFunc `yaml:"-" json:"-"` JsRequire *require.RequireModule `yaml:"-" json:"-"` - JsLoop *eventloop.EventLoop `yaml:"-" json:"-"` - JsScriptList []*JsScriptInfo `yaml:"-" json:"-"` - JsScriptCron *cron.Cron `yaml:"-" json:"-"` - JsScriptCronLock *sync.Mutex `yaml:"-" json:"-"` + + JsLoop *eventloop.EventLoop `yaml:"-" json:"-"` + JsScriptList []*JsScriptInfo `yaml:"-" json:"-"` + JsScriptCron *cron.Cron `yaml:"-" json:"-"` + JsScriptCronLock *sync.Mutex `yaml:"-" json:"-"` // 重载使用的互斥锁 JsReloadLock sync.Mutex `yaml:"-" json:"-"` // 内置脚本摘要表,用于判断内置脚本是否有更新 diff --git a/dice/dice_attrs_manager.go b/dice/dice_attrs_manager.go index ee18d6f3..40960c0a 100644 --- a/dice/dice_attrs_manager.go +++ b/dice/dice_attrs_manager.go @@ -1,20 +1,19 @@ package dice import ( - "database/sql" "errors" "fmt" "time" - "github.com/jmoiron/sqlx" ds "github.com/sealdice/dicescript" + "gorm.io/gorm" "sealdice-core/dice/model" log "sealdice-core/utils/kratos" ) type AttrsManager struct { - db *sqlx.DB + db *gorm.DB logger *log.Helper m SyncMap[string, *AttributesItem] } @@ -169,24 +168,18 @@ func (am *AttrsManager) CheckForSave() (int, int) { return 0, 0 } - tx, err := db.Begin() - if err != nil { - if am.logger != nil { - am.logger.Errorf("定期写入用户数据出错(创建事务): %v", err) - } - return 0, 0 - } + tx := db.Begin() am.m.Range(func(key string, value *AttributesItem) bool { if !value.IsSaved { saved += 1 - value.SaveToDB(db, tx) + value.SaveToDB(tx) } times += 1 return true }) - err = tx.Commit() + err := tx.Commit().Error if err != nil { if am.logger != nil { am.logger.Errorf("定期写入用户数据出错(提交事务): %v", err) @@ -210,7 +203,8 @@ func (am *AttrsManager) CheckAndFreeUnused() { am.m.Range(func(key string, value *AttributesItem) bool { if value.LastUsedTime-currentTime > 60*10 { prepareToFree[key] = 1 - value.SaveToDB(am.db, nil) + // 直接保存 + value.SaveToDB(am.db) } return true }) @@ -279,13 +273,13 @@ type AttributesItem struct { SheetType string } -func (i *AttributesItem) SaveToDB(db *sqlx.DB, tx *sql.Tx) { +func (i *AttributesItem) SaveToDB(db *gorm.DB) { // 使用事务写入 rawData, err := ds.NewDictVal(i.valueMap).V().ToJSON() if err != nil { return } - err = model.AttrsPutById(db, tx, i.ID, rawData, i.Name, i.SheetType) + err = model.AttrsPutById(db, i.ID, rawData, i.Name, i.SheetType) if err != nil { log.Error("保存数据失败", err.Error()) return diff --git a/dice/dice_censor.go b/dice/dice_censor.go index 4b14814e..561b2f5e 100644 --- a/dice/dice_censor.go +++ b/dice/dice_censor.go @@ -9,7 +9,7 @@ import ( "sort" "strings" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" "sealdice-core/dice/censor" "sealdice-core/dice/model" @@ -54,7 +54,7 @@ type CensorManager struct { IsLoading bool Parent *Dice Censor *censor.Censor - DB *sqlx.DB + DB *gorm.DB SensitiveWordsFiles map[string]*censor.WordFile } diff --git a/dice/im_session.go b/dice/im_session.go index 905d481f..87fd462b 100644 --- a/dice/im_session.go +++ b/dice/im_session.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "gorm.io/gorm" + "sealdice-core/dice/model" "sealdice-core/message" log "sealdice-core/utils/kratos" @@ -20,7 +22,6 @@ import ( rand2 "golang.org/x/exp/rand" "github.com/dop251/goja" - "github.com/jmoiron/sqlx" "golang.org/x/time/rate" "gopkg.in/yaml.v3" ) @@ -192,7 +193,7 @@ func (group *GroupInfo) IsActive(ctx *MsgContext) bool { return false } -func (group *GroupInfo) PlayerGet(db *sqlx.DB, id string) *GroupPlayerInfo { +func (group *GroupInfo) PlayerGet(db *gorm.DB, id string) *GroupPlayerInfo { if group.Players == nil { group.Players = new(SyncMap[string, *GroupPlayerInfo]) } diff --git a/dice/model/attr.go b/dice/model/attr.go index 6b0c15f6..1df3ef9b 100644 --- a/dice/model/attr.go +++ b/dice/model/attr.go @@ -6,6 +6,8 @@ import ( "github.com/jmoiron/sqlx" ) +// 废弃代码先不改 + func attrGetAllBase(db *sqlx.DB, bucket string, key string) []byte { var buf []byte diff --git a/dice/model/attrs_new.go b/dice/model/attrs_new.go index b57ff4c5..50904f2f 100644 --- a/dice/model/attrs_new.go +++ b/dice/model/attrs_new.go @@ -1,13 +1,14 @@ package model import ( - "database/sql" "errors" + "fmt" "time" - "sealdice-core/utils" + "github.com/tidwall/gjson" + "gorm.io/gorm" - "github.com/jmoiron/sqlx" + "sealdice-core/utils" ds "github.com/sealdice/dicescript" ) @@ -22,26 +23,33 @@ const ( // 注: 角色表有用sheet也有用sheets的,这里数据结构中使用sheet // AttributesItemModel 新版人物卡。说明一下,这里带s的原因是attrs指的是一个map +// 补全GORM缺少部分 type AttributesItemModel struct { - Id string `json:"id" db:"id"` // 如果是群内,那么是类似 QQ-Group:12345-QQ:678910,群外是nanoid - Data []byte `json:"data" db:"data"` // 序列化后的卡数据,理论上[]byte不会进入字符串缓存,要更好些? - AttrsType string `json:"attrsType" db:"attrs_type"` // 分为: 角色卡(character)、组内用户(group_user)、群组(group)、用户(user) + Id string `json:"id" gorm:"column:id"` // 如果是群内,那么是类似 QQ-Group:12345-QQ:678910,群外是nanoid + Data []byte `json:"data" gorm:"column:data"` // 序列化后的卡数据,理论上[]byte不会进入字符串缓存,要更好些? + AttrsType string `json:"attrsType" gorm:"column:attrs_type;index:idx_attrs_attrs_type_id;default:NULL"` // 分为: 角色卡(character)、组内用户(group_user)、群组(group)、用户(user) // 这些是群组内置卡专用的,其实就是替代了绑卡关系表,作为群组内置卡时,这个字段用于存放绑卡关系 - BindingSheetId string `json:"bindingSheetId" db:"binding_sheet_id"` // 绑定的卡片ID + BindingSheetId string `json:"bindingSheetId" gorm:"column:binding_sheet_id;default:'';index:idx_attrs_binding_sheet_id"` // 绑定的卡片ID // 这些是角色卡专用的 - Name string `json:"name" db:"name"` // 卡片名称 - OwnerId string `json:"ownerId" db:"owner_id"` // 若有明确归属,就是对应的UniformID - SheetType string `json:"sheetType" db:"sheet_type"` // 卡片类型,如dnd5e coc7 - IsHidden bool `json:"isHidden" db:"is_hidden"` // 隐藏的卡片不出现在 pc list 中 + Name string `json:"name" gorm:"column:name"` // 卡片名称 + OwnerId string `json:"ownerId" gorm:"column:owner_id;index:idx_attrs_owner_id_id"` // 若有明确归属,就是对应的UniformID + SheetType string `json:"sheetType" gorm:"column:sheet_type"` // 卡片类型,如dnd5e coc7 + // 手动定义bool类的豹存方式 + IsHidden bool `json:"isHidden" gorm:"column:is_hidden;type:bool"` // 隐藏的卡片不出现在 pc list 中 // 通用属性 - CreatedAt int64 `json:"createdAt" db:"created_at"` - UpdatedAt int64 `json:"updatedAt" db:"updated_at"` + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` + UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"` // 下面的属性并非数据库字段,而是用于内存中的缓存 - BindingGroupsNum int64 `json:"bindingGroupNum"` // 当前绑定中群数 + BindingGroupsNum int64 `json:"bindingGroupNum" gorm:"-"` // 当前绑定中群数 +} + +// 兼容旧版本数据库 +func (*AttributesItemModel) TableName() string { + return "attrs" } func (m *AttributesItemModel) IsDataExists() bool { @@ -52,161 +60,244 @@ func (m *AttributesItemModel) IsDataExists() bool { // PlatformMappingModel 虚拟ID - 平台用户ID 映射表 type PlatformMappingModel struct { - Id string `json:"id" db:"id"` // 虚拟ID,格式为 U:nanoid 意为 User / Uniform / Universal - IMUserID string `json:"IMUserID" db:"im_user_id"` // IM平台的用户ID + Id string `json:"id" gorm:"column:id"` // 虚拟ID,格式为 U:nanoid 意为 User / Uniform / Universal + IMUserID string `json:"IMUserID" gorm:"column:im_user_id"` // IM平台的用户ID } -func AttrsGetById(db *sqlx.DB, id string) (*AttributesItemModel, error) { +func AttrsGetById(db *gorm.DB, id string) (*AttributesItemModel, error) { + // 这里必须使用AttributesItemModel结构体,如果你定义一个只有ID属性的结构体去接收,居然能接收到值,这样就会豹错 var item AttributesItemModel - err := db.Get(&item, `select id, data, COALESCE(attrs_type, '') as attrs_type, binding_sheet_id, name, owner_id, - sheet_type, is_hidden, created_at, updated_at from attrs where id = $1`, id) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + err := db.Model(&AttributesItemModel{}). + Select("id, data, COALESCE(attrs_type, '') as attrs_type, binding_sheet_id, name, owner_id, sheet_type, is_hidden, created_at, updated_at"). + Where("id = ?", id). + Limit(1). + // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回空对象本身就是预期正常的行为 + Find(&item).Error + if err != nil { return nil, err } return &item, nil } // AttrsGetBindingSheetIdByGroupId 获取当前正在绑定的ID -func AttrsGetBindingSheetIdByGroupId(db *sqlx.DB, id string) (string, error) { +func AttrsGetBindingSheetIdByGroupId(db *gorm.DB, id string) (string, error) { + // 这里必须使用AttributesItemModel结构体,如果你定义一个只有ID属性的结构体去接收,居然能接收到值,这样就会豹错 var item AttributesItemModel - err := db.Get(&item, "select binding_sheet_id from attrs where id = $1", id) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + err := db.Model(&AttributesItemModel{}). + Select("binding_sheet_id"). + Where("id = ?", id). + Limit(1). + // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回id=""就是预期正常的行为 + Find(&item).Error + if err != nil { return "", err } return item.BindingSheetId, nil } -func AttrsGetIdByUidAndName(db *sqlx.DB, userId string, name string) (string, error) { +func AttrsGetIdByUidAndName(db *gorm.DB, userId string, name string) (string, error) { + // 这里必须使用AttributesItemModel结构体 + // 如果你定义一个只有ID属性的结构体去接收,居然有概率能接收到值,这样就会和之前的行为不一致了 var item AttributesItemModel - err := db.Get(&item, "select id from attrs where owner_id = $1 and name = $2", userId, name) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + err := db.Model(&AttributesItemModel{}). + Select("id"). + Where("owner_id = ? AND name = ?", userId, name). + Limit(1). + // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回空对象的id=""就是预期正常的行为 + Find(&item).Error + if err != nil { return "", err } return item.Id, nil } -func AttrsPutById(db *sqlx.DB, tx *sql.Tx, id string, data []byte, name, sheetType string) error { - // TODO: 好像还不够,需要nickname 需要sheetType,还有别的吗 - var err error - now := time.Now().Unix() - query := `insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at, name, sheet_type) - values ($1, $2, true, '', $3, $3, $4, $5) - on conflict (id) do update set data = $2, updated_at = $3, name = $4, sheet_type = $5` - args := []any{id, data, now, name, sheetType} - - if tx != nil { - _, err = tx.Exec(query, args...) - } else { - _, err = db.Exec(query, args...) +func AttrsPutById(db *gorm.DB, id string, data []byte, name, sheetType string) error { + now := time.Now().Unix() // 获取当前时间 + // 这里的原本逻辑是:第一次全量创建,第二次修改部分属性 + // 所以使用了Attrs和Assign配合使用 + if err := db.Where("id = ?", id). + Attrs(map[string]any{ + // 第一次全量建表 + "id": id, + // 如果想在[]bytes里输入值,注意传参的时候不能给any传[]bytes,否则会无法读取,同时还没有豹错,浪费大量时间。 + // 这里为了兼容,不使用gob的序列化方法处理结构体(同时,也不知道序列化方法是否可用) + // TODO: 是否在这里string(data)更快更合理? + "data": gjson.ParseBytes(data).String(), + "is_hidden": true, + "binding_sheet_id": "", + "name": name, + "sheet_type": sheetType, + "created_at": now, + "updated_at": now, + }). + // 如果是更新的情况,更新下面这部分,则需要被更新的为: + Assign(map[string]any{ + "data": gjson.ParseBytes(data).String(), + "updated_at": now, + "name": name, + "sheet_type": sheetType, + }).FirstOrCreate(&AttributesItemModel{}).Error; err != nil { + return err // 返回错误 } - return err + return nil // 操作成功,返回 nil } -func AttrsDeleteById(db *sqlx.DB, id string) error { - var err error - query := `delete from attrs where id = ?` - args := []any{id} - - _, err = db.Exec(query, args...) - return err +func AttrsDeleteById(db *gorm.DB, id string) error { + // 使用 GORM 的 Delete 方法删除指定 id 的记录 + if err := db.Where("id = ?", id).Delete(&AttributesItemModel{}).Error; err != nil { + return err // 返回错误 + } + return nil // 操作成功,返回 nil } -func AttrsCharGetBindingList(db *sqlx.DB, id string) ([]string, error) { - rows, err := db.Query(`select id from attrs where binding_sheet_id = $1`, id) - if err != nil { - return nil, err - } - defer rows.Close() +func AttrsCharGetBindingList(db *gorm.DB, id string) ([]string, error) { + // 定义一个切片用于存储结果 + var lst []string - lst := []string{} - for rows.Next() { - item := "" - err = rows.Scan(&item) - if err != nil { - return nil, err - } - lst = append(lst, item) + // 使用 GORM 查询绑定的 id 列表 + if err := db.Model(&AttributesItemModel{}). + Select("id"). + Where("binding_sheet_id = ?", id). + Find(&lst).Error; err != nil { + return nil, err // 返回错误 } - return lst, err + return lst, nil // 返回结果切片 } -func AttrsCharUnbindAll(db *sqlx.DB, id string) (int64, error) { - rows, err := db.Exec(`update attrs set binding_sheet_id = '' where binding_sheet_id = $1`, id) - if err != nil { - return 0, err - } - affected, err := rows.RowsAffected() - if err != nil { - return 0, err +func AttrsCharUnbindAll(db *gorm.DB, id string) (int64, error) { + // 使用 GORM 更新绑定的记录,将 binding_sheet_id 设为空字符串 + result := db.Model(&AttributesItemModel{}). + Where("binding_sheet_id = ?", id). + Update("binding_sheet_id", "") + + if result.Error != nil { + return 0, result.Error // 返回错误 } - return affected, err + return result.RowsAffected, nil // 返回受影响的行数 } // AttrsNewItem 新建一个角色卡/属性容器 -func AttrsNewItem(db *sqlx.DB, item *AttributesItemModel) (*AttributesItemModel, error) { - id := utils.NewID() - now := time.Now().Unix() - item.CreatedAt, item.UpdatedAt = now, now +func AttrsNewItem(db *gorm.DB, item *AttributesItemModel) (*AttributesItemModel, error) { + id := utils.NewID() // 生成新的 ID + now := time.Now().Unix() // 获取当前时间 + item.CreatedAt, item.UpdatedAt = now, now // 设置创建和更新时间 + if item.Id == "" { - item.Id = id + item.Id = id // 如果 ID 为空,则赋值新生成的 ID } - var err error - _, err = db.Exec(` - insert into attrs (id, data, binding_sheet_id, name, owner_id, sheet_type, is_hidden, created_at, updated_at, attrs_type) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, - item.Id, item.Data, item.BindingSheetId, item.Name, item.OwnerId, item.SheetType, item.IsHidden, - item.CreatedAt, item.UpdatedAt, item.AttrsType) - return item, err + // 使用 GORM 的 Create 方法插入新记录 + // 这个木落没有忽略错误,所以说这个可以安心使用Create而不用担心出现问题…… + // 这里使用Create可以正确插入byte数组,注意map[string]any里面不可以用byte数组,否则无法入库 + if err := db.Create(item).Error; err != nil { + return nil, err // 返回错误 + } + return item, nil // 返回新创建的项 } -func AttrsBindCharacter(db *sqlx.DB, charId string, id string) error { +func AttrsBindCharacter(db *gorm.DB, charId string, id string) error { + // 开始事务 + tx := db.Begin() + if tx.Error != nil { + return tx.Error // 返回错误 + } + + defer func() { + if r := recover(); r != nil { + tx.Rollback() // 发生恐慌时回滚 + } + }() + + // 将新字典值转换为 JSON + now := time.Now().Unix() json, err := ds.NewDictVal(nil).V().ToJSON() if err != nil { + tx.Rollback() // 返回错误时回滚 return err } - _, _ = db.Exec(`insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at) - values ($1, $3, true, '', $2, $2)`, id, time.Now().Unix(), json) - ret, err := db.Exec(`update attrs set binding_sheet_id = $1 where id = $2`, charId, id) - if err == nil { - var affected int64 - affected, err = ret.RowsAffected() - if err != nil { - return err - } - if affected == 0 { - return errors.New("群信息不存在: " + id) - } + // 原本代码为: + // _, _ = db.Exec(`insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at) + // values ($1, $3, true, '', $2, $2)`, id, time.Now().Unix(), json) + // + // ret, err := db.Exec(`update attrs set binding_sheet_id = $1 where id = $2`, charId, id) + + result := tx.Where("id = ?", id). + // 按照木落的原版代码,应该是这么个逻辑:查不到的时候能正确执行,查到了就不执行了,所以用Attrs而不是Assign + Attrs(map[string]any{ + "id": id, + // 如果想在[]bytes里输入值,注意传参的时候不能给any传[]bytes,否则会无法读取,同时还没有豹错,浪费大量时间。 + // 这里为了兼容,不使用gob的序列化方法处理结构体(同时,也不知道序列化方法是否可用) + "data": gjson.ParseBytes(json).String(), + "is_hidden": true, + // 如果插入成功,原版代码接下来更新这个值,那么现在就是等价的 + "binding_sheet_id": charId, + "created_at": now, + "updated_at": now, + }). + // 按照原版代码,无论是不是能插入成功,都要更新这个值,所以这么写就是等价的了 + Assign(map[string]any{ + "binding_sheet_id": charId, + }). + FirstOrCreate(&AttributesItemModel{}) + if result.Error != nil { + tx.Rollback() // 返回错误时回滚 + return result.Error + } + // 四种情况:没有数据->初始化成功->返回1条 + // 没有数据->更新失败->返回0条 + // 有数据->更新成功->返回1条 + // 有数据->更新失败->返回0条,但理论上所有返回0条的情况应该都会被丢出去 + // 对于FirstOrCreate来说应该不会遇到下面的情况,但是保底一下 + if result.RowsAffected == 0 { + tx.Rollback() + return errors.New("群信息不存在或发生更新异常: " + id) } - return err + + // 提交事务 + return tx.Commit().Error } -func AttrsGetCharacterListByUserId(db *sqlx.DB, userId string) (lst []*AttributesItemModel, err error) { - rows, err := db.Queryx(` - select id, name, sheet_type, - (select count(id) from attrs where binding_sheet_id = t1.id) - from attrs as t1 where owner_id = $1 and is_hidden is false - `, userId) +func AttrsGetCharacterListByUserId(db *gorm.DB, userId string) ([]*AttributesItemModel, error) { + // Pinenutn: 在Gorm中,如果gorm:"-",优先级似乎很高,经过我自己测试: + // 结构体内若使用gorm="-" ,Scan将无法映射到结果中(GPT胡说八道说可以映射上,我试了半天,被骗。) + // 如果不带任何标签: GORM对结构体名称进行转换,如BindingGroupNum对应映射:binding_group_num,结果里有binding_group_num自动映射 + // 如果带上标签"column:xxxxx",则会使用指定的名称映射,如column:xxxxx对应映射xxxxx + // GPT 说带上JSON标签,可以映射到结果中,但实际上是错误的,无法映射。 + // 所以最终”BindingGroupNum“需要创建这个结构体用来临时存放结果,然后将结果映射到AttributesItemModel结构体上。 + // 在gorm="-"这里的配置还有更多可以使用无写入权限,有读权限的标签,但要求必须BindingGroupNum的结构体名称和数据库查询结果一致 + // 且不能指定columns,否则会建表,没找到更好方案。 + type AttrResult struct { + ID string `gorm:"column:id"` + Name string `gorm:"column:name"` + SheetType string `gorm:"column:sheet_type"` + BindingGroupNum int64 `gorm:"column:binding_group_num"` // 映射 COUNT(a.id) + } + var tempResultList []AttrResult + // 由于是复杂查询,无法直接使用Models,又为了防止以后attrs表名称修改,故不使用Table而是用TableName替换 + model := AttributesItemModel{} + tableName := model.TableName() + // 此处使用了JOIN来避免子查询,数据库一般对JOIN有使用索引的优化,所以有性能提升,但是我没有实际测试过性能差距。 + err := db.Table(fmt.Sprintf("%s AS t1", tableName)). + Select("t1.id, t1.name, t1.sheet_type, COUNT(a.id) AS binding_group_num"). + Joins(fmt.Sprintf("LEFT JOIN %s AS a ON a.binding_sheet_id = t1.id", tableName)). + Where("t1.owner_id = ? AND t1.is_hidden IS FALSE", userId). + Group("t1.id, t1.name, t1.sheet_type"). + // Pinenutn:此处我根据创建时间对创建的卡进行排序,不知道是否有意义? + Order("t1.created_at ASC"). + Scan(&tempResultList).Error if err != nil { return nil, err } - defer rows.Close() - - var items []*AttributesItemModel - for rows.Next() { - item := &AttributesItemModel{} - err := rows.Scan( - &item.Id, - &item.Name, - &item.SheetType, - &item.BindingGroupsNum, - ) - if err != nil { - return nil, err + items := make([]*AttributesItemModel, len(tempResultList)) + for i, tempResult := range tempResultList { + items[i] = &AttributesItemModel{ + Id: tempResult.ID, + Name: tempResult.Name, + SheetType: tempResult.SheetType, + BindingGroupsNum: tempResult.BindingGroupNum, } - items = append(items, item) } - return items, nil + return items, nil // 返回角色列表 } diff --git a/dice/model/backup.go b/dice/model/backup.go index a564a077..131da51a 100644 --- a/dice/model/backup.go +++ b/dice/model/backup.go @@ -1,19 +1,35 @@ package model import ( - "github.com/jmoiron/sqlx" + "strings" + + "gorm.io/gorm" ) -func Vacuum(db *sqlx.DB, path string) error { - _, err := db.Exec("vacuum into $1", path) - return err +// Vacuum 执行数据库的 vacuum 操作 +func Vacuum(db *gorm.DB, path string) error { + // 检查数据库驱动是否为 SQLite + if !strings.Contains(db.Dialector.Name(), "sqlite") { + return nil + } + + // 使用 GORM 执行 vacuum 操作,并将数据库保存到指定路径 + err := db.Exec("VACUUM INTO ?", path).Error + return err // 返回错误 } -func FlushWAL(db *sqlx.DB) error { - _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE);") - if err != nil { - return err +// FlushWAL 执行 WAL 日志的检查点和内存收缩 +func FlushWAL(db *gorm.DB) error { + // 检查数据库驱动是否为 SQLite + if !strings.Contains(db.Dialector.Name(), "sqlite") { + return nil + } + + // 执行 WAL 检查点操作 + if err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil { + return err // 返回错误 } - _, err = db.Exec("PRAGMA shrink_memory") - return err + // 执行内存收缩操作 + err := db.Exec("PRAGMA shrink_memory;").Error + return err // 返回错误 } diff --git a/dice/model/ban.go b/dice/model/ban.go index 6f4d3328..becae7dc 100644 --- a/dice/model/ban.go +++ b/dice/model/ban.go @@ -1,36 +1,62 @@ package model import ( - "github.com/jmoiron/sqlx" + "github.com/tidwall/gjson" + "gorm.io/gorm" ) -func BanItemDel(db *sqlx.DB, id string) error { - _, err := db.Exec("delete from ban_info where id=$1", id) - return err +// BanInfo 模型 +// GORM STRUCT +type BanInfo struct { + ID string `gorm:"primaryKey;column:id"` // 主键列 + BanUpdatedAt int `gorm:"index:idx_ban_info_ban_updated_at;column:ban_updated_at"` // BanUpdatedAt 列 + UpdatedAt int `gorm:"index:idx_ban_info_updated_at;column:updated_at"` // UpdatedAt 列 + Data []byte `gorm:"column:data"` // BLOB 类型 } -func BanItemSave(db *sqlx.DB, id string, updatedAt int64, banUpdatedAt int64, data []byte) error { - _, err := db.NamedExec("replace into ban_info (id, updated_at, ban_updated_at, data) values (:id, :updated_at, :ban_updated_at, :data)", - map[string]interface{}{ - "id": id, - "updated_at": updatedAt, - "ban_updated_at": banUpdatedAt, - "data": data, - }) - return err +func (*BanInfo) TableName() string { + return "ban_info" } -func BanItemList(db *sqlx.DB, callback func(id string, banUpdatedAt int64, data []byte)) error { - var items []struct { - ID string `db:"id"` - BanUpdatedAt int64 `db:"ban_updated_at"` - Data []byte `db:"data"` +// BanItemDel 删除指定 ID 的禁用项 +func BanItemDel(db *gorm.DB, id string) error { + // 使用 GORM 的 Delete 方法删除指定 ID 的记录 + result := db.Where("id = ?", id).Delete(&BanInfo{}) + return result.Error // 返回错误 +} + +// BanItemSave 保存或替换禁用项 这里的[]byte也是json反序列化产物 +func BanItemSave(db *gorm.DB, id string, updatedAt int64, banUpdatedAt int64, data []byte) error { + // 使用 FirstOrCreate ,这里显然,第一次初始化的时候替换ID,而剩余的时候只换ID以外的数据 + 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(), // 禁用项数据 + }). + Assign(map[string]any{ + "updated_at": int(updatedAt), + "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段 + "data": gjson.ParseBytes(data).String(), // 禁用项数据 + }).FirstOrCreate(&BanInfo{}).Error; err != nil { + return err // 返回错误 } - if err := db.Select(&items, "SELECT id, ban_updated_at, data FROM ban_info ORDER BY ban_updated_at DESC"); err != nil { - return err + + return nil // 操作成功,返回 nil +} + +// BanItemList 列出所有禁用项并调用回调函数处理 +func BanItemList(db *gorm.DB, callback func(id string, banUpdatedAt int64, data []byte)) error { + var items []BanInfo + + // 使用 GORM 查询所有禁用项 + if err := db.Order("ban_updated_at DESC").Find(&items).Error; err != nil { + return err // 返回错误 } + + // 遍历每个禁用项并调用回调函数 for _, item := range items { - callback(item.ID, item.BanUpdatedAt, item.Data) + callback(item.ID, int64(item.BanUpdatedAt), item.Data) // 确保类型一致 } - return nil + return nil // 操作成功,返回 nil } diff --git a/dice/model/censor_log.go b/dice/model/censor_log.go index 954c15a9..fb497e40 100644 --- a/dice/model/censor_log.go +++ b/dice/model/censor_log.go @@ -4,111 +4,129 @@ import ( "encoding/json" "time" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" "sealdice-core/dice/censor" + log "sealdice-core/utils/kratos" ) type CensorLog struct { - ID uint64 `json:"id"` - MsgType string `json:"msgType"` - UserID string `json:"userId"` - GroupID string `json:"groupId"` - Content string `json:"content"` - HighestLevel int `json:"highestLevel"` - CreatedAt int `json:"createdAt"` + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + MsgType string `json:"msgType" gorm:"column:msg_type"` + UserID string `json:"userId" gorm:"index:idx_censor_log_user_id;column:user_id"` + GroupID string `json:"groupId" gorm:"column:group_id"` + Content string `json:"content" gorm:"column:content"` + HighestLevel int `json:"highestLevel" gorm:"index:idx_censor_log_level;column:highest_level"` + CreatedAt int `json:"createdAt" gorm:"column:created_at"` + // 补充gorm有的部分: + SensitiveWords string `json:"-" gorm:"column:sensitive_words"` + ClearMark bool `json:"-" gorm:"column:clear_mark;type:bool"` } -func CensorAppend(db *sqlx.DB, msgType string, userID string, groupID string, content string, sensitiveWords interface{}, highestLevel int) bool { - now := time.Now() - nowTimestamp := now.Unix() +func (CensorLog) TableName() string { + return "censor_log" +} + +// 添加一个敏感词记录 +func CensorAppend(db *gorm.DB, msgType string, userID string, groupID string, content string, sensitiveWords interface{}, highestLevel int) bool { + // 获取当前时间的 Unix 时间戳 + nowTimestamp := time.Now().Unix() + // 将敏感词转换为 JSON 字符串 words, err := json.Marshal(sensitiveWords) if err != nil { return false } - _, err = db.Exec(` -INSERT INTO censor_log( - msg_type, - user_id, - group_id, - content, - sensitive_words, - highest_level, - created_at, - clear_mark -) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - msgType, userID, groupID, content, words, highestLevel, nowTimestamp, false) - - if err != nil { + // 创建 CensorLog 实例,手动设置 CreatedAt + censorLog := CensorLog{ + MsgType: msgType, + UserID: userID, + GroupID: groupID, + Content: content, + SensitiveWords: string(words), + HighestLevel: highestLevel, + CreatedAt: int(nowTimestamp), // Unix 时间戳 + ClearMark: false, + } + // 使用 GORM 的 Create 方法插入记录 + if err := db.Create(&censorLog).Error; err != nil { return false } - return err == nil + return true } -func CensorCount(db *sqlx.DB, userID string) map[censor.Level]int { +func CensorCount(db *gorm.DB, userID string) map[censor.Level]int { + // 定义要查询的不同敏感级别 levels := [5]censor.Level{censor.Ignore, censor.Notice, censor.Caution, censor.Warning, censor.Danger} - var temp int + var temp int64 res := make(map[censor.Level]int) + + // 遍历每个敏感级别并执行查询 for _, level := range levels { - _ = db.Get(&temp, `SELECT COUNT(*) FROM censor_log WHERE user_id = ? AND highest_level = ? AND clear_mark = ?`, userID, level, false) - res[level] = temp + // 使用 GORM 的链式查询 + err := db.Model(&CensorLog{}).Where("user_id = ? AND highest_level = ? AND clear_mark = ?", userID, level, false). + Count(&temp).Error + + // 如果查询出现错误,忽略并赋值为 0 + if err != nil { + res[level] = 0 + } else { + res[level] = int(temp) + } } + return res } -func CensorClearLevelCount(db *sqlx.DB, userID string, level censor.Level) { - _, _ = db.Exec(`UPDATE censor_log SET clear_mark = ? WHERE user_id = ? AND highest_level = ?`, true, userID, level) +func CensorClearLevelCount(db *gorm.DB, userID string, level censor.Level) { + // 使用 GORM 的链式查询执行批量更新 + err := db.Model(&CensorLog{}). + Where("user_id = ? AND highest_level = ?", userID, level). + Update("clear_mark", true).Error + if err != nil { + log.Error(err) + } } +// QueryCensorLog 是分页查询的参数 type QueryCensorLog struct { - PageNum int `query:"pageNum"` - PageSize int `query:"pageSize"` - UserID string `query:"userId"` - Level int `query:"level"` + PageNum int `query:"pageNum"` // 当前页码 + PageSize int `query:"pageSize"` // 每页条数 + UserID string `query:"userId"` // 用户ID + Level int `query:"level"` // 敏感级别 } -func CensorGetLogPage(db *sqlx.DB, params QueryCensorLog) (int, []CensorLog, error) { - var total int - res := make([]CensorLog, 0, params.PageSize) +// CensorGetLogPage 使用 GORM 进行分页查询 +func CensorGetLogPage(db *gorm.DB, params QueryCensorLog) (int64, []CensorLog, error) { + var total int64 + var logs []CensorLog - err := db.QueryRow("SELECT COUNT(*) FROM censor_log").Scan(&total) - if err != nil { - return 0, nil, err + // 首先统计总记录数 + query := db.Model(&CensorLog{}) + + // 如果传入了 UserID 和 Level,则添加查询条件 + if params.UserID != "" { + query = query.Where("user_id = ?", params.UserID) } - rows, err := db.Queryx(` -SELECT id, - msg_type, - user_id, - group_id, - content, - highest_level, - created_at -FROM censor_log -ORDER BY created_at DESC -LIMIT ? OFFSET ?`, params.PageSize, (params.PageNum-1)*params.PageSize) - if err != nil { + if params.Level != 0 { + query = query.Where("highest_level = ?", params.Level) + } + + // 统计符合条件的总记录数 + if err := query.Count(&total).Error; err != nil { return 0, nil, err } - defer rows.Close() - - for rows.Next() { - log := CensorLog{} - err := rows.Scan( - &log.ID, - &log.MsgType, - &log.UserID, - &log.GroupID, - &log.Content, - &log.HighestLevel, - &log.CreatedAt, - ) - if err != nil { - return 0, nil, err - } - res = append(res, log) + + // 查询分页数据 + if err := query. + Order("created_at DESC"). // 按照创建时间倒序排列 + Limit(params.PageSize). // 限制返回条数 + Offset((params.PageNum - 1) * params.PageSize). // 偏移 + Find(&logs). // 查询数据 + Error; err != nil { + return 0, nil, err } - return total, res, nil + return total, logs, nil } diff --git a/dice/model/db.go b/dice/model/db.go index d55241bc..56e46cef 100644 --- a/dice/model/db.go +++ b/dice/model/db.go @@ -4,13 +4,16 @@ import ( "fmt" "os" "path/filepath" + "strings" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" + + log "sealdice-core/utils/kratos" ) func DBCheck(dataDir string) { - checkDB := func(db *sqlx.DB) bool { - rows, err := db.Query("PRAGMA integrity_check") //nolint:execinquery + checkDB := func(db *gorm.DB) bool { + rows, err := db.Exec("PRAGMA integrity_check").Rows() if err != nil { return false } @@ -35,9 +38,9 @@ func DBCheck(dataDir string) { } var ok1, ok2, ok3 bool - var dataDB *sqlx.DB - var logsDB *sqlx.DB - var censorDB *sqlx.DB + var dataDB *gorm.DB + var logsDB *gorm.DB + var censorDB *gorm.DB var err error dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) @@ -46,7 +49,9 @@ func DBCheck(dataDir string) { fmt.Fprintln(os.Stdout, "数据库 data.db 无法打开") } else { ok1 = checkDB(dataDB) - dataDB.Close() + db, _ := dataDB.DB() + // 关闭 + db.Close() } dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) @@ -55,7 +60,9 @@ func DBCheck(dataDir string) { fmt.Fprintln(os.Stdout, "数据库 data-logs.db 无法打开") } else { ok2 = checkDB(logsDB) - logsDB.Close() + db, _ := logsDB.DB() + // 关闭db + db.Close() } dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) @@ -64,7 +71,9 @@ func DBCheck(dataDir string) { fmt.Fprintln(os.Stdout, "数据库 data-censor.db 无法打开") } else { ok3 = checkDB(censorDB) - censorDB.Close() + db, _ := censorDB.DB() + // 关闭db + db.Close() } fmt.Fprintln(os.Stdout, "数据库检查结果:") @@ -73,188 +82,119 @@ func DBCheck(dataDir string) { fmt.Fprintln(os.Stdout, "data-censor.db:", ok3) } -func SQLiteDBInit(dataDir string) (dataDB *sqlx.DB, logsDB *sqlx.DB, err error) { - dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db")) - dataDB, err = _SQLiteDBInit(dbDataPath, true) - if err != nil { - return - } - - dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db")) - logsDB, err = _SQLiteDBInit(dbDataLogsPath, true) - if err != nil { - return - } - - // data建表 - texts := []string{ - ` -create table if not exists group_player_info -( - id INTEGER - primary key autoincrement, - group_id TEXT, - user_id TEXT, - name TEXT, - created_at INTEGER, - updated_at INTEGER, - last_command_time INTEGER, - auto_set_name_template TEXT, - dice_side_num TEXT -);`, - `create index if not exists idx_group_player_info_group_id on group_player_info (group_id);`, - `create index if not exists idx_group_player_info_user_id on group_player_info (user_id);`, - `create unique index if not exists idx_group_player_info_group_user on group_player_info (group_id, user_id);`, - ` -create table if not exists group_info -( - id TEXT primary key, - created_at INTEGER, - updated_at INTEGER, - data BLOB -);`, - - ` -create table if not exists ban_info -( - id TEXT primary key, - ban_updated_at INTEGER, - updated_at INTEGER, - data BLOB -);`, - `create index if not exists idx_ban_info_updated_at on ban_info (updated_at);`, - `create index if not exists idx_ban_info_ban_updated_at on ban_info (ban_updated_at);`, - - `CREATE TABLE IF NOT EXISTS endpoint_info ( -user_id TEXT PRIMARY KEY, -cmd_num INTEGER, -cmd_last_time INTEGER, -online_time INTEGER, -updated_at INTEGER -);`, - - ` -CREATE TABLE IF NOT EXISTS attrs ( +var createSql = ` +CREATE TABLE attrs__temp ( id TEXT PRIMARY KEY, data BYTEA, attrs_type TEXT, - - -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化 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 ); -`, - `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`, - `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`, - `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`, +` + +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) + if err != nil { + return nil, nil, err } - for _, i := range texts { - _, _ = dataDB.Exec(i) + // 特殊情况建表语句处置 + 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 + if err != nil { + tx.Rollback() + 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 + if err != nil { + tx.Rollback() + return nil, nil, err + } + tx.Commit() + } + } + // data建表 + err = dataDB.AutoMigrate( + &GroupPlayerInfoBase{}, + &GroupInfo{}, + &BanInfo{}, + &EndpointInfo{}, + &AttributesItemModel{}, + ) + if err != nil { + return nil, nil, err + } + err = dataDB.Exec("VACUUM").Error + if err != nil { + return nil, nil, err } + logsDB, err = LogDBInit(dataDir) + return +} +// 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) + if err != nil { + return + } // logs建表 - texts = []string{ - ` -create table if not exists logs -( - id INTEGER primary key autoincrement, - name TEXT, - group_id TEXT, - extra TEXT, - created_at INTEGER, - updated_at INTEGER, - upload_url TEXT, - upload_time INTEGER -);`, - ` -create index if not exists idx_logs_group - on logs (group_id);`, - ` -create index if not exists idx_logs_update_at - on logs (updated_at);`, - ` -create unique index if not exists idx_log_group_id_name - on logs (group_id, name);`, - // 如果log_items有更改,需同步检查migrate/convert_logs.go - ` -create table if not exists log_items -( - id INTEGER primary key autoincrement, - log_id INTEGER, - group_id TEXT, - nickname TEXT, - im_userid TEXT, - time INTEGER, - message TEXT, - is_dice INTEGER, - command_id INTEGER, - command_info TEXT, - raw_msg_id TEXT, - user_uniform_id TEXT, - removed INTEGER, - parent_id INTEGER -);`, - ` -create index if not exists idx_log_items_group_id - on log_items (log_id);`, - ` -create index if not exists idx_log_items_log_id - on log_items (log_id);`, - - `alter table logs add upload_url text;`, // 测试版特供 - `alter table logs add upload_time integer;`, + if err = logsDB.AutoMigrate(&LogInfo{}, &LogOneItem{}); err != nil { + return nil, err } - - for _, i := range texts { - _, _ = logsDB.Exec(i) + err = logsDB.Exec("VACUUM").Error + if err != nil { + return nil, err } - - return + return logsDB, nil } -func SQLiteCensorDBInit(dataDir string) (censorDB *sqlx.DB, err error) { +func SQLiteCensorDBInit(dataDir string) (censorDB *gorm.DB, err error) { path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db")) if err != nil { - return + return nil, err } censorDB, err = _SQLiteDBInit(path, true) if err != nil { - return + return nil, err } - - texts := []string{` -CREATE TABLE IF NOT EXISTS censor_log -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - msg_type TEXT, - user_id TEXT, - group_id TEXT, - content TEXT, - sensitive_words TEXT, - highest_level INTEGER, - created_at INTEGER, - clear_mark BOOLEAN -); -`, - ` -CREATE INDEX IF NOT EXISTS idx_censor_log_user_id - ON censor_log (user_id); -`, - ` -CREATE INDEX IF NOT EXISTS idx_censor_log_level - ON censor_log (highest_level); -`, + // 创建基本的表结构,并通过标签定义索引 + if err = censorDB.AutoMigrate(&CensorLog{}); err != nil { + return nil, err } - - for _, i := range texts { - _, _ = censorDB.Exec(i) + err = censorDB.Exec("VACUUM").Error + if err != nil { + return nil, err } - return + return censorDB, nil } diff --git a/dice/model/db_init.go b/dice/model/db_init.go index a6ba08ee..b38d9819 100644 --- a/dice/model/db_init.go +++ b/dice/model/db_init.go @@ -4,28 +4,58 @@ package model import ( - _ "github.com/glebarez/go-sqlite" - "github.com/jmoiron/sqlx" + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" ) -func _SQLiteDBInit(path string, useWAL bool) (*sqlx.DB, error) { - db, err := sqlx.Open(zapDriverName, path) +// _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 { - panic(err) + return nil, err + } + // Enable Cache Mode + db, err = GetBuntCacheDB(db) + if err != nil { + return nil, err } - - // _, err = db.Exec("vacuum") - // if err != nil { - // panic(err) - // } - // enable WAL mode if useWAL { - _, err = db.Exec("PRAGMA journal_mode=WAL") + err = db.Exec("PRAGMA journal_mode=WAL").Error if err != nil { - panic(err) + 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 index 34706eab..49ba40bd 100644 --- a/dice/model/db_init_cgo.go +++ b/dice/model/db_init_cgo.go @@ -4,23 +4,57 @@ package model import ( - "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" // sqlite3 driver + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" ) -func _SQLiteDBInit(path string, useWAL bool) (*sqlx.DB, error) { - db, err := sqlx.Open(zapDriverName, path) +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 { - panic(err) + return nil, err + } + // Enable Cache Mode + open, err = GetBuntCacheDB(open) + if err != nil { + return nil, err } - // enable WAL mode if useWAL { - _, err = db.Exec("PRAGMA journal_mode=WAL") + err = open.Exec("PRAGMA journal_mode=WAL").Error if err != nil { panic(err) } } - return db, 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 6e2662b2..eda09cb7 100644 --- a/dice/model/db_utils.go +++ b/dice/model/db_utils.go @@ -1,17 +1,19 @@ package model import ( - "fmt" "os" "path/filepath" "runtime" + "strings" "sync" + log "sealdice-core/utils/kratos" "sealdice-core/utils/spinner" ) +// DBCacheDelete 删除SQLite数据库缓存文件 +// TODO: 判断缓存是否应该被删除 func DBCacheDelete() bool { - // d.BaseConfig.DataDir dataDir := "./data/default" tryDelete := func(fn string) bool { @@ -23,36 +25,36 @@ func DBCacheDelete() bool { return os.Remove(fnPath) == nil } - // 非 windows 不删缓存 + // 非 Windows 系统不删除缓存 if runtime.GOOS != "windows" { return true } - ok := true if ok { ok = tryDelete("data.db-shm") } if ok { - tryDelete("data.db-wal") + ok = tryDelete("data.db-wal") } if ok { - tryDelete("data-logs.db-shm") + ok = tryDelete("data-logs.db-shm") } if ok { - tryDelete("data-logs.db-wal") + ok = tryDelete("data-logs.db-wal") } if ok { - tryDelete("data-censor.db-shm") + ok = tryDelete("data-censor.db-shm") } if ok { - tryDelete("data-censor.db-wal") + ok = tryDelete("data-censor.db-wal") } return ok } +// DBVacuum 整理数据库 func DBVacuum() { done := make(chan interface{}, 1) - fmt.Fprintln(os.Stdout, "开始进行数据库整理") + log.Info("开始进行数据库整理") go spinner.WithLines(done, 3, 10) defer func() { @@ -64,15 +66,29 @@ func DBVacuum() { vacuum := func(path string, wg *sync.WaitGroup) { defer wg.Done() - db, err := _SQLiteDBInit(path, true) - defer func() { _ = db.Close() }() + // 使用 GORM 初始化数据库 + vacuumDB, err := _SQLiteDBInit(path, true) + // 数据库类型不是 SQLite 直接返回 + if !strings.Contains(vacuumDB.Dialector.Name(), "sqlite") { + return + } + defer func() { + rawdb, err2 := vacuumDB.DB() + if err2 != nil { + return + } + err = rawdb.Close() + if err != nil { + return + } + }() if err != nil { - fmt.Fprintf(os.Stdout, "清理 %q 时出现错误:%v", path, err) + log.Errorf("清理 %q 时出现错误:%v", path, err) return } - _, err = db.Exec("VACUUM;") + err = vacuumDB.Exec("VACUUM;").Error if err != nil { - fmt.Fprintf(os.Stdout, "清理 %q 时出现错误:%v", path, err) + log.Errorf("清理 %q 时出现错误:%v", path, err) } } @@ -82,5 +98,5 @@ func DBVacuum() { wg.Wait() - fmt.Fprintln(os.Stdout, "\n数据库整理完成") + log.Info("数据库整理完成") } diff --git a/dice/model/endpoint_info.go b/dice/model/endpoint_info.go index 947f72df..df582488 100644 --- a/dice/model/endpoint_info.go +++ b/dice/model/endpoint_info.go @@ -1,54 +1,59 @@ package model import ( - "database/sql" "errors" - "time" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" + "gorm.io/gorm/clause" ) +var ErrEndpointInfoUIDEmpty = errors.New("user id is empty") + +// 仅修改为gorm格式 type EndpointInfo struct { - UserID string `db:"user_id"` - CmdNum int64 `db:"cmd_num"` - CmdLastTime int64 `db:"cmd_last_time"` - OnlineTime int64 `db:"online_time"` - UpdatedAt int64 `db:"updated_at"` + UserID string `gorm:"column:user_id;primaryKey"` + CmdNum int64 `gorm:"column:cmd_num;"` + CmdLastTime int64 `gorm:"column:cmd_last_time;"` + OnlineTime int64 `gorm:"column:online_time;"` + UpdatedAt int64 `gorm:"column:updated_at;"` } -var ErrEndpointInfoUIDEmpty = errors.New("user id is empty") +func (EndpointInfo) TableName() string { + return "endpoint_info" +} -func (e *EndpointInfo) Query(db *sqlx.DB) error { +func (e *EndpointInfo) Query(db *gorm.DB) error { if len(e.UserID) == 0 { return ErrEndpointInfoUIDEmpty } if db == nil { return errors.New("db is nil") } - row := db.QueryRowx( - `SELECT cmd_num, cmd_last_time, online_time, updated_at FROM endpoint_info WHERE user_id = $1`, - e.UserID, - ) - err := row.Scan(&e.CmdNum, &e.CmdLastTime, &e.OnlineTime, &e.UpdatedAt) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + + err := db.Model(&EndpointInfo{}). + Where("user_id = ?", e.UserID). + Select("cmd_num", "cmd_last_time", "online_time", "updated_at"). + Scan(&e).Error + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } + return nil } -func (e *EndpointInfo) Save(db *sqlx.DB) error { +func (e *EndpointInfo) Save(db *gorm.DB) error { + // 检查 user_id 是否为空 if len(e.UserID) == 0 { return ErrEndpointInfoUIDEmpty } - if db == nil { - return errors.New("db is nil") - } - now := time.Now().Unix() - e.UpdatedAt = now - - _, err := db.Exec( - `REPLACE INTO endpoint_info (user_id, cmd_num, cmd_last_time, online_time, updated_at) VALUES (?, ?, ?, ?, ?)`, - e.UserID, e.CmdNum, e.CmdLastTime, e.OnlineTime, e.UpdatedAt, - ) - return err + // 检查user_id冲突时更新,否则进行创建 + result := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "user_id"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "cmd_num", "cmd_last_time", "online_time", "updated_at", + }), + }).Create(e) + + return result.Error } diff --git a/dice/model/gormcache.go b/dice/model/gormcache.go new file mode 100644 index 00000000..aae79bfb --- /dev/null +++ b/dice/model/gormcache.go @@ -0,0 +1,132 @@ +package model + +import ( + "context" + "errors" + "strconv" + "time" + + "github.com/go-gorm/caches/v4" + "github.com/spaolacci/murmur3" + "github.com/tidwall/buntdb" + "gorm.io/gorm" +) + +type buntDBCacher struct { + db *buntdb.DB +} + +func generateHashKey(key string) string { + hash := murmur3.Sum64([]byte(key)) + return strconv.FormatUint(hash, 16) // 返回十六进制字符串 +} + +// Get 从缓存中获取与给定键关联的数据。 +// 该方法接受一个上下文、一个键和一个查询对象作为参数。 +// 它首先将键转换为哈希键,然后从数据库中获取相应的值。 +// 如果键不存在于数据库中,则返回nil, nil。 +// 如果存在错误,将返回错误信息。 +// 如果成功获取数据,将返回填充了数据的查询对象。 +func (c *buntDBCacher) Get(_ context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) { + // 生成哈希键以确定缓存的位置。 + hashedKey := generateHashKey(key) + + // 尝试查找对应的关联值 + var res string + err := c.db.View(func(tx *buntdb.Tx) error { + var err error + // 从事务中获取与哈希键关联的值。 + res, err = tx.Get(hashedKey) + return err + }) + + // 如果键在数据库中不存在,记录信息并返回nil, nil。 + if errors.Is(err, buntdb.ErrNotFound) { + // 此处不得不忽略,因为这个cache的实现机理就是如此,除非修改gorm cache的源码。 + return nil, nil //nolint:nilnil + } + + // 如果发生其他错误,返回错误信息。 + if err != nil { + return nil, err + } + // 将获取到的值解码为查询对象。 + if err = q.Unmarshal([]byte(res)); err != nil { + return nil, err + } + + return q, nil +} + +// Store 方法用于将查询结果存储到缓存中。 +// 该方法接收一个上下文、一个键和一个查询对象作为参数。 +// 它首先对键进行哈希处理,然后将查询对象序列化为字节切片。 +// 序列化成功后,它将数据存储到缓存数据库中,并设置数据过期时间为5秒。 +// 参数: +// +// _ context.Context: 上下文,本例中未使用。 +// key string: 需要存储的数据的键。 +// val *caches.Query[any]: 需要存储的查询对象。 +// +// 返回值: +// +// error: 在序列化或存储过程中遇到的错误,如果没有错误则返回nil。 +func (c *buntDBCacher) Store(_ context.Context, key string, val *caches.Query[any]) error { + // 生成哈希键以确保键的均匀分布和避免潜在的键冲突。 + hashedKey := generateHashKey(key) + // 将查询对象序列化为字节切片,以便存储到缓存中。 + res, err := val.Marshal() + if err != nil { + return err + } + // 使用数据库的Update方法来原子地设置数据。 + err = c.db.Update(func(tx *buntdb.Tx) error { + // 设置键值对,并指定数据过期时间为5秒。 + _, _, err = tx.Set(hashedKey, string(res), &buntdb.SetOptions{Expires: true, TTL: time.Second * 5}) + return err + }) + + return err +} + +// Invalidate 使缓存器中的所有缓存项失效。 +// 该方法通过删除数据库中所有以 caches.IdentifierPrefix 开头的键来实现。 +// 参数: +// +// _context.Context: 未使用。 +// +// 返回值: +// +// error: 如果在使缓存项失效的过程中发生错误,则返回该错误。 +func (c *buntDBCacher) Invalidate(_ context.Context) error { + // 清理所有缓存 + err := c.db.Update(func(tx *buntdb.Tx) error { + err := tx.DeleteAll() + if err != nil { + return err + } + return nil + }) + return err +} + +func GetBuntCacheDB(db *gorm.DB) (*gorm.DB, error) { + open, err := buntdb.Open(":memory:") + if err != nil { + return nil, err + } + // Easer参数:使用ServantGo任务执行与合并库 + // ServantGo提供了一种简单且惯用的方法来合并同时运行的相同类型的任务。 + // 可以先尝试一下easer=true是否可以加速 + cachesPlugin := &caches.Caches{Conf: &caches.Config{ + Easer: true, + Cacher: &buntDBCacher{ + db: open, + }, + }} + err = db.Use(cachesPlugin) + if err != nil { + return nil, err + } + return db, nil +} diff --git a/dice/model/group_info.go b/dice/model/group_info.go index c75358e9..1ba8074b 100644 --- a/dice/model/group_info.go +++ b/dice/model/group_info.go @@ -1,141 +1,193 @@ package model import ( - log "sealdice-core/utils/kratos" + "time" - "github.com/jmoiron/sqlx" "golang.org/x/time/rate" + "gorm.io/gorm" + "gorm.io/gorm/clause" ds "github.com/sealdice/dicescript" + + log "sealdice-core/utils/kratos" ) -func GroupInfoListGet(db *sqlx.DB, callback func(id string, updatedAt int64, data []byte)) error { - rows, err := db.Queryx("SELECT id, updated_at, data FROM group_info") +// GroupInfo 模型 +type GroupInfo struct { + ID string `gorm:"column:id;primaryKey"` // 主键,字符串类型 + CreatedAt int `gorm:"column:created_at"` // 创建时间 + UpdatedAt *int64 `gorm:"column:updated_at"` // 更新时间,int64类型 + Data []byte `gorm:"column:data"` // BLOB 类型字段,使用 []byte 表示 +} + +func (*GroupInfo) TableName() string { + return "group_info" +} + +// GroupInfoListGet 使用 GORM 实现,遍历 group_info 表中的数据并调用回调函数 +func GroupInfoListGet(db *gorm.DB, callback func(id string, updatedAt int64, data []byte)) error { + // 创建一个保存查询结果的结构体 + var results []struct { + ID string `gorm:"column:id"` // 字段 id + UpdatedAt *int64 `gorm:"column:updated_at"` // 由于可能存在 NULL,定义为指针类型 + Data []byte `gorm:"column:data"` // 字段 data + } + + // 使用 GORM 查询 group_info 表中的 id, updated_at, data 列 + err := db.Model(&GroupInfo{}).Select("id, updated_at, data").Find(&results).Error if err != nil { + // 如果查询发生错误,返回错误信息 return err } - defer rows.Close() - for rows.Next() { - var id string + // 遍历查询结果 + for _, result := range results { var updatedAt int64 - var data []byte - var pUpdatedAt *int64 - - err = rows.Scan(&id, &pUpdatedAt, &data) - if err != nil { - return err + // 如果 updatedAt 是 NULL,需要跳过该字段 + if result.UpdatedAt != nil { + updatedAt = *result.UpdatedAt } - if pUpdatedAt != nil { - updatedAt = *pUpdatedAt - } - callback(id, updatedAt, data) + // 调用回调函数,传递 id, updatedAt, data + callback(result.ID, updatedAt, result.Data) } - return rows.Err() + // 返回 nil 表示操作成功 + return nil } // GroupInfoSave 保存群组信息 -func GroupInfoSave(db *sqlx.DB, groupID string, updatedAt int64, data []byte) error { - // INSERT OR REPLACE 语句可以根据是否已存在对应记录自动插入或更新记录 - _, err := db.Exec("INSERT OR REPLACE INTO group_info (id, updated_at, data) VALUES (?, ?, ?)", groupID, updatedAt, data) - return err -} - -// GroupPlayerNumGet 查询指定群组中玩家数量 -func GroupPlayerNumGet(db *sqlx.DB, groupID string) (int64, error) { - var count int64 - - // 使用Named方法绑定命名参数 - // sqlitex.ExecuteTransient(conn, `select count(id) from group_player_info where group_id=$group_id`, &sqlitex.ExecOptions{ - query, args, err := sqlx.Named("SELECT COUNT(id) FROM group_player_info WHERE group_id = :group_id", map[string]interface{}{"group_id": groupID}) - if err != nil { - return 0, err +func GroupInfoSave(db *gorm.DB, groupID string, updatedAt int64, data []byte) error { + // 使用 gorm 的 Upsert 功能实现插入或更新 + groupInfo := GroupInfo{ + ID: groupID, + UpdatedAt: &updatedAt, + Data: data, } - - // 执行查询并将结果存储到 count 变量中 - if err := db.QueryRowx(query, args...).Scan(&count); err != nil { - return 0, err - } - - return count, nil + result := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"updated_at", "data"}), + }).Create(&groupInfo) + return result.Error } // GroupPlayerInfoBase 群内玩家信息 type GroupPlayerInfoBase struct { - Name string `yaml:"name" jsbind:"name"` // 玩家昵称 - UserID string `yaml:"userId" jsbind:"userId"` - InGroup bool `yaml:"inGroup"` // 是否在群内,有时一个人走了,信息还暂时残留 - LastCommandTime int64 `yaml:"lastCommandTime" jsbind:"lastCommandTime"` // 上次发送指令时间 - RateLimiter *rate.Limiter `yaml:"-"` // 限速器 - RateLimitWarned bool `yaml:"-"` // 是否已经警告过限速 - AutoSetNameTemplate string `yaml:"autoSetNameTemplate" jsbind:"autoSetNameTemplate"` // 名片模板 + 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:"-"` // 是否在群内,有时一个人走了,信息还暂时残留 + LastCommandTime int64 `yaml:"lastCommandTime" jsbind:"lastCommandTime" gorm:"column:last_command_time"` // 上次发送指令时间 + // 非数据库信息 + RateLimiter *rate.Limiter `yaml:"-" gorm:"-"` // 限速器 + // 非数据库信息 + RateLimitWarned bool `yaml:"-" gorm:"-"` // 是否已经警告过限速 + AutoSetNameTemplate string `yaml:"autoSetNameTemplate" jsbind:"autoSetNameTemplate" gorm:"column:auto_set_name_template"` // 名片模板 // level int 权限 - DiceSideNum int `yaml:"diceSideNum"` // 面数,为0时等同于d100 - ValueMapTemp *ds.ValueMap `yaml:"-"` // 玩家的群内临时变量 + DiceSideNum int `yaml:"diceSideNum" gorm:"column:dice_side_num"` // 面数,为0时等同于d100 + // 非数据库信息 + ValueMapTemp *ds.ValueMap `yaml:"-" gorm:"-"` // 玩家的群内临时变量 // ValueMapTemp map[string]*VMValue `yaml:"-"` // 玩家的群内临时变量 - TempValueAlias *map[string][]string `yaml:"-"` // 群内临时变量别名 - 其实这个有点怪的,为什么在这里? + // 非数据库信息 + TempValueAlias *map[string][]string `yaml:"-" gorm:"-"` // 群内临时变量别名 - 其实这个有点怪的,为什么在这里? + + // 非数据库信息 + UpdatedAtTime int64 `yaml:"-" json:"-" gorm:"-"` + // 非数据库信息 + RecentUsedTime int64 `yaml:"-" json:"-" gorm:"-"` + // 缺少信息 -> 这边原来就是int吗? + CreatedAt int `yaml:"-" json:"-" gorm:"column:created_at"` // 创建时间 + UpdatedAt int `yaml:"-" json:"-" gorm:"column:updated_at"` // 更新时间 + GroupID string `yaml:"-" json:"-" gorm:"column:group_id;index:idx_group_player_info_group_id; uniqueIndex:idx_group_player_info_group_user"` +} + +// 兼容设置 +func (GroupPlayerInfoBase) TableName() string { + return "group_player_info" +} + +// GroupPlayerNumGet 获取指定群组的玩家数量 +func GroupPlayerNumGet(db *gorm.DB, groupID string) (int64, error) { + var count int64 - UpdatedAtTime int64 `yaml:"-" json:"-"` - RecentUsedTime int64 `yaml:"-" json:"-"` + // 使用 GORM 的 Table 方法指定表名进行查询 + // db.Table("表名").Where("条件").Count(&count) 是通用的 GORM 用法 + // 将 group_id 作为查询条件 + err := db.Model(&GroupPlayerInfoBase{}).Where("group_id = ?", groupID).Count(&count).Error + if err != nil { + // 如果查询出现错误,返回错误信息 + return 0, err + } + + // 返回统计的数量 + return count, nil } -func GroupPlayerInfoGet(db *sqlx.DB, groupID string, playerID string) *GroupPlayerInfoBase { +// GroupPlayerInfoGet 获取指定群组中的玩家信息 +func GroupPlayerInfoGet(db *gorm.DB, groupID string, playerID string) *GroupPlayerInfoBase { var ret GroupPlayerInfoBase - rows, err := db.NamedQuery("SELECT name, last_command_time, auto_set_name_template, dice_side_num FROM group_player_info WHERE group_id=:group_id AND user_id=:user_id", map[string]interface{}{ - "group_id": groupID, - "user_id": playerID, - }) + // 使用 GORM 查询数据并绑定到结构体中 + // db.Table("表名").Where("条件").First(&ret) 查询一条数据并映射到结构体 + err := db.Model(&GroupPlayerInfoBase{}). + Where("group_id = ? AND user_id = ?", groupID, playerID). + Select("name, last_command_time, auto_set_name_template, dice_side_num"). + Scan(&ret).Error + // 如果查询发生错误,打印错误并返回 nil if err != nil { - log.Errorf("error getting group player info: %v", err) + log.Errorf("error getting group player info: %s", err.Error()) return nil } - defer rows.Close() - - // Name: stmt.ColumnText(0), - // UserId: playerId, - // LastCommandTime: stmt.ColumnInt64(2), - // AutoSetNameTemplate: stmt.ColumnText(3), - // DiceSideNum: int(stmt.ColumnInt64(4)), - - exists := false - for rows.Next() { - exists = true - // 使用Scan方法将查询结果映射到结构体中 - if err := rows.Scan( - &ret.Name, - &ret.LastCommandTime, - &ret.AutoSetNameTemplate, - &ret.DiceSideNum, - ); err != nil { - log.Errorf("error getting group player info: %v", err) - return nil - } - } - - if !exists { + // 如果查询到的数据为空,返回 nil + if db.RowsAffected == 0 { return nil } + + // 将 playerID 赋值给结构体中的 UserID 字段 ret.UserID = playerID + + // 返回查询结果 return &ret } -func GroupPlayerInfoSave(db *sqlx.DB, groupID string, playerID string, info *GroupPlayerInfoBase) error { - _, err := db.NamedExec("REPLACE INTO group_player_info (name, updated_at, last_command_time, auto_set_name_template, dice_side_num, group_id, user_id) VALUES (:name, :updated_at, :last_command_time, :auto_set_name_template, :dice_side_num, :group_id, :user_id)", map[string]interface{}{ +// GroupPlayerInfoSave 保存玩家信息,不再使用 REPLACE INTO 语句 +func GroupPlayerInfoSave(db *gorm.DB, groupID string, playerID string, info *GroupPlayerInfoBase) error { + // 考虑到info是指针,为了防止可能info还会被用到其他地方,这里的给info指针赋值也是有意义的 + // 但强烈建议将这段去除掉,数据库层面理论上不应该混杂业务层逻辑? + now := int(time.Now().Unix()) + info.UserID = playerID + info.GroupID = groupID + info.UpdatedAt = now // 更新当前时间为 UpdatedAt + + // 判断条件:联合主键相同 + conditions := map[string]any{ + "user_id": info.UserID, + "group_id": info.GroupID, + } + data := map[string]any{ "name": info.Name, - "updated_at": info.UpdatedAtTime, + "user_id": info.UserID, "last_command_time": info.LastCommandTime, "auto_set_name_template": info.AutoSetNameTemplate, "dice_side_num": info.DiceSideNum, - "group_id": groupID, - "user_id": playerID, - }) - return err + "group_id": info.GroupID, + "updated_at": info.UpdatedAt, + } + // 原代码逻辑: + // REPLACE INTO group_player_info (name, updated_at, last_command_time, auto_set_name_template, dice_side_num, group_id, user_id) + // VALUES (:name, :updated_at, :last_command_time, :auto_set_name_template, :dice_side_num, :group_id, :user_id) + // 所以它是全局替换,使用Assign方法,无论如何都给我替换 + if err := db. + Where(conditions). + Assign(data).FirstOrCreate(&GroupPlayerInfoBase{}).Error; err != nil { + return err + } + + // 返回 nil 表示操作成功 + return nil } diff --git a/dice/model/log.go b/dice/model/log.go index d2836b90..fe052369 100644 --- a/dice/model/log.go +++ b/dice/model/log.go @@ -5,10 +5,9 @@ import ( "encoding/json" "errors" "fmt" - "strings" "time" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" log "sealdice-core/utils/kratos" ) @@ -20,71 +19,139 @@ type LogOne struct { } type LogOneItem struct { - ID uint64 `json:"id" db:"id"` - Nickname string `json:"nickname" db:"nickname"` - IMUserID string `json:"IMUserId" db:"im_userid"` - Time int64 `json:"time" db:"time"` - Message string `json:"message" db:"message"` - IsDice bool `json:"isDice" db:"is_dice"` - CommandID int64 `json:"commandId" db:"command_id"` - CommandInfo interface{} `json:"commandInfo" db:"command_info"` - RawMsgID interface{} `json:"rawMsgId" db:"raw_msg_id"` - - UniformID string `json:"uniformId" db:"user_uniform_id"` - Channel string `json:"channel"` + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"` + LogID uint64 `json:"-" gorm:"column:log_id;index:idx_log_items_log_id"` + GroupID string `gorm:"index:idx_log_items_group_id;column:group_id;index:idx_log_delete_by_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;type:bool"` + CommandID int64 `json:"commandId" gorm:"column:command_id"` + CommandInfo interface{} `json:"commandInfo" gorm:"-"` + CommandInfoStr string `json:"-" gorm:"column:command_info"` + // 这里的RawMsgID 真的什么都有可能 + RawMsgID interface{} `json:"rawMsgId" gorm:"-"` + RawMsgIDStr string `json:"-" gorm:"column:raw_msg_id;index:idx_raw_msg_id;index:idx_log_delete_by_id"` + UniformID string `json:"uniformId" gorm:"column:user_uniform_id"` + // 数据库里没有的 + Channel string `json:"channel" gorm:"-"` + // 数据库里有,JSON里没有的 + // 允许default=NULL + Removed *int `gorm:"column:removed" json:"-"` + ParentID *int `gorm:"column:parent_id" json:"-"` +} + +// BeforeSave 钩子函数: 查询前,interface{}转换为json +func (item *LogOneItem) BeforeSave(_ *gorm.DB) (err error) { + // 将 CommandInfo 转换为 JSON 字符串保存到 CommandInfoStr + if item.CommandInfo != nil { + if data, err := json.Marshal(item.CommandInfo); err == nil { + item.CommandInfoStr = string(data) + } else { + return err + } + } + + // 将 RawMsgID 转换为 string 字符串,保存到 RawMsgIDStr + if item.RawMsgID != nil { + item.RawMsgIDStr = fmt.Sprintf("%v", item.RawMsgID) + } + + return nil +} + +// AfterFind 钩子函数: 查询后,interface{}转换为json +func (item *LogOneItem) AfterFind(_ *gorm.DB) (err error) { + // 将 CommandInfoStr 从 JSON 字符串反序列化为 CommandInfo + if item.CommandInfoStr != "" { + if err := json.Unmarshal([]byte(item.CommandInfoStr), &item.CommandInfo); err != nil { + return err + } + } + + // 将 RawMsgIDStr string 直接赋值给 RawMsgID + if item.RawMsgIDStr != "" { + item.RawMsgID = item.RawMsgIDStr + } + + return nil } type LogInfo struct { - ID uint64 `json:"id" db:"id"` - Name string `json:"name" db:"name"` - GroupID string `json:"groupId" db:"groupId"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - UpdatedAt int64 `json:"updatedAt" db:"updated_at"` - Size int `json:"size" db:"size"` + 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"` + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` + UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at;index:idx_logs_update_at"` + // 允许数据库NULL值 + // 原版代码中,此处标记了db:size,但实际上,该列并不存在。 + // 考虑到该处数据将会为未来log查询提供优化手段,保留该结构体定义,但不使用。 + // 使用GORM:<-:false 无写入权限,这样它就不会建库,但请注意,下面LogGetLogPage处,如果你查询出的名称不是size + // 不能在这里绑定column,因为column会给你建立那一列。 + // TODO: 将这个字段使用上会不会比后台查询就JOIN更合适? + Size *int `json:"size" gorm:"<-:false"` + // 数据库里有,json不展示的 + // 允许数据库NULL值(该字段当前不使用) + Extra *string `json:"-" gorm:"column:extra"` + // 原本标记为:测试版特供,由于原代码每次都会执行,故直接启用此处column记录。 + UploadURL string `json:"-" gorm:"column:upload_url"` // 测试版特供 + UploadTime int `json:"-" gorm:"column:upload_time"` // 测试版特供 +} + +// 兼容旧版本的数据库设计 +func (*LogOneItem) TableName() string { + return "log_items" +} + +func (*LogInfo) TableName() string { + return "logs" } -func LogGetInfo(db *sqlx.DB) ([]int, error) { +// LogGetInfo 查询日志简略信息,使用通用函数替代SQLITE专属函数 +func LogGetInfo(db *gorm.DB) ([]int, error) { lst := []int{0, 0, 0, 0} - err := db.Get(&lst[0], "SELECT seq FROM sqlite_sequence WHERE name == 'logs'") + + var maxID sql.NullInt64 // 使用sql.NullInt64来处理NULL值 + var itemsMaxID sql.NullInt64 // 使用sql.NullInt64来处理NULL值 + // 获取 logs 表的记录数和最大 ID + err := db.Model(&LogInfo{}).Select("COUNT(*)").Scan(&lst[2]).Error if err != nil { return nil, err } - err = db.Get(&lst[1], "SELECT seq FROM sqlite_sequence WHERE name == 'log_items'") + + err = db.Model(&LogInfo{}).Select("MAX(id)").Scan(&maxID).Error if err != nil { return nil, err } - err = db.Get(&lst[2], "SELECT COUNT(*) FROM logs") + lst[0] = int(maxID.Int64) + + // 获取 log_items 表的记录数和最大 ID + err = db.Model(&LogOneItem{}).Select("COUNT(*)").Scan(&lst[3]).Error if err != nil { return nil, err } - err = db.Get(&lst[3], "SELECT COUNT(*) FROM log_items") + + err = db.Model(&LogOneItem{}).Select("MAX(id)").Scan(&itemsMaxID).Error if err != nil { return nil, err } + lst[1] = int(itemsMaxID.Int64) + return lst, nil } // Deprecated: replaced by page -func LogGetLogs(db *sqlx.DB) ([]*LogInfo, error) { +func LogGetLogs(db *gorm.DB) ([]*LogInfo, error) { var lst []*LogInfo - rows, err := db.Queryx("SELECT id,name,group_id,created_at, updated_at FROM logs") - if err != nil { + + // 使用 GORM 查询 logs 表 + if err := db.Model(&LogInfo{}). + Select("id, name, group_id, created_at, updated_at"). + Find(&lst).Error; err != nil { return nil, err } - defer rows.Close() - for rows.Next() { - log := &LogInfo{} - if err := rows.Scan( - &log.ID, - &log.Name, - &log.GroupID, - &log.CreatedAt, - &log.UpdatedAt, - ); err != nil { - return nil, err - } - lst = append(lst, log) - } + return lst, nil } @@ -98,181 +165,141 @@ type QueryLogPage struct { } // LogGetLogPage 获取分页 -func LogGetLogPage(db *sqlx.DB, param *QueryLogPage) (int, []*LogInfo, error) { - countQuery := `SELECT count(*) FROM logs` - query := ` -SELECT logs.id as id, - logs.name as name, - logs.group_id as group_id, - logs.created_at as created_at, - logs.updated_at as updated_at, - count(logs.id) as size -FROM logs - LEFT JOIN log_items items ON logs.id = items.log_id -` - var conditions []string +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") + + // 添加条件 if param.Name != "" { - conditions = append(conditions, "logs.name like '%' || :name || '%'") + query = query.Where("logs.name LIKE ?", "%"+param.Name+"%") } if param.GroupID != "" { - conditions = append(conditions, "logs.group_id like '%' || :group_id || '%'") + query = query.Where("logs.group_id LIKE ?", "%"+param.GroupID+"%") } if param.CreatedTimeBegin != "" { - conditions = append(conditions, "logs.created_at >= :created_time_begin") + query = query.Where("logs.created_at >= ?", param.CreatedTimeBegin) } if param.CreatedTimeEnd != "" { - conditions = append(conditions, "logs.created_at <= :created_time_end") + query = query.Where("logs.created_at <= ?", param.CreatedTimeEnd) } - if len(conditions) > 0 { - where := " WHERE " + strings.Join(conditions, " AND ") - query += where - countQuery += where - } - - query += fmt.Sprintf(" GROUP BY logs.id LIMIT %d, %d", (param.PageNum-1)*param.PageSize, param.PageSize) - var total int - count, err := db.NamedQuery(countQuery, param) - if err != nil { - return 0, nil, err - } - defer count.Close() - count.Next() - err = count.Scan(&total) - if err != nil { + // 获取总数 + var count int64 + if err := db.Model(&LogInfo{}).Count(&count).Error; err != nil { return 0, nil, err } - lst := make([]*LogInfo, 0, param.PageSize) - rows, err := db.NamedQuery(query, param) - if err != nil { + // 分页查询 + query = query.Group("logs.id").Limit(param.PageSize).Offset((param.PageNum - 1) * param.PageSize) + + // 执行查询 + if err := query.Scan(&lst).Error; err != nil { return 0, nil, err } - defer rows.Close() - for rows.Next() { - log := &LogInfo{} - if err := rows.Scan( - &log.ID, - &log.Name, - &log.GroupID, - &log.CreatedAt, - &log.UpdatedAt, - &log.Size, - ); err != nil { - return 0, nil, err - } - lst = append(lst, log) - } - return total, lst, nil + + return int(count), lst, nil } // LogGetList 获取列表 -func LogGetList(db *sqlx.DB, groupID string) ([]string, error) { +func LogGetList(db *gorm.DB, groupID string) ([]string, error) { var lst []string - err := db.Select(&lst, "SELECT name FROM logs WHERE group_id = $1 ORDER BY updated_at DESC", groupID) - if err != nil { + + // 执行查询 + if err := db.Model(&LogInfo{}). + Select("name"). + Where("group_id = ?", groupID). + Order("updated_at DESC"). + Pluck("name", &lst).Error; err != nil { return nil, err } + return lst, nil } // LogGetIDByGroupIDAndName 获取ID -func LogGetIDByGroupIDAndName(db *sqlx.DB, groupID string, logName string) (logID int64, err error) { - err = db.Get(&logID, "SELECT id FROM logs WHERE group_id = $1 AND name = $2", groupID, logName) +func LogGetIDByGroupIDAndName(db *gorm.DB, groupID string, logName string) (logID uint64, err error) { + err = db.Model(&LogInfo{}). + Select("id"). + Where("group_id = ? AND name = ?", groupID, logName). + Scan(&logID).Error + if err != nil { // 如果出现错误,判断是否没有找到对应的记录 - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, gorm.ErrRecordNotFound) { return 0, nil } return 0, err } + return logID, nil } -func LogGetUploadInfo(db *sqlx.DB, groupID string, logName string) (url string, uploadTime, updateTime int64, err error) { - res, err := db.Queryx( - `SELECT updated_at, upload_url, upload_time FROM logs WHERE group_id = $1 AND name = $2`, - groupID, logName, - ) +// LogGetUploadInfo 获取上传信息 +func LogGetUploadInfo(db *gorm.DB, groupID string, logName string) (url string, uploadTime, updateTime int64, err error) { + var logInfo struct { + UpdatedAt int64 `gorm:"column:updated_at"` + UploadURL string `gorm:"column:upload_url"` + UploadTime int64 `gorm:"column:upload_time"` + } + + err = db.Model(&LogInfo{}). + Select("updated_at, upload_url, upload_time"). + Where("group_id = ? AND name = ?", groupID, logName). + Scan(&logInfo).Error + if err != nil { return "", 0, 0, err } - defer res.Close() - for res.Next() { - err = res.Scan(&updateTime, &url, &uploadTime) - if err != nil { - return "", 0, 0, err - } - } + // 提取结果 + updateTime = logInfo.UpdatedAt + url = logInfo.UploadURL + uploadTime = logInfo.UploadTime return } -func LogSetUploadInfo(db *sqlx.DB, groupID string, logName string, url string) error { +// LogSetUploadInfo 设置上传信息 +func LogSetUploadInfo(db *gorm.DB, groupID string, logName string, url string) error { if len(url) == 0 { return nil } now := time.Now().Unix() - _, err := db.Exec( - `UPDATE logs SET upload_url = $1, upload_time = $2 WHERE group_id = $3 AND name = $4`, - url, now, groupID, logName, - ) + // 使用 GORM 更新上传信息 + err := db.Model(&LogInfo{}).Where("group_id = ? AND name = ?", groupID, logName). + Update("upload_url", url). + Update("upload_time", now). + Error + return err } // LogGetAllLines 获取log的所有行数据 -func LogGetAllLines(db *sqlx.DB, groupID string, logName string) ([]*LogOneItem, error) { +func LogGetAllLines(db *gorm.DB, groupID string, logName string) ([]*LogOneItem, error) { // 获取log的ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return nil, err } - // 查询行数据 - rows, err := db.Queryx(`SELECT id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id - FROM log_items WHERE log_id=$1 ORDER BY time ASC`, logID) - if err != nil { - return nil, err - } - defer rows.Close() - - var ret []*LogOneItem - for rows.Next() { - item := &LogOneItem{} - var commandInfoStr []byte - - // 使用Scan方法将查询结果映射到结构体中 - if err := rows.Scan( - &item.ID, - &item.Nickname, - &item.IMUserID, - &item.Time, - &item.Message, - &item.IsDice, - &item.CommandID, - &commandInfoStr, - &item.RawMsgID, - &item.UniformID, - ); err != nil { - return nil, err - } + var items []*LogOneItem - // 反序列化commandInfo - if commandInfoStr != nil { - _ = json.Unmarshal(commandInfoStr, &item.CommandInfo) - } - - ret = append(ret, item) - } + // 查询行数据 + err = db.Model(&LogOneItem{}). + Select("id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id"). + Where("log_id = ?", logID). + Order("time ASC"). + Scan(&items).Error - if err := rows.Err(); err != nil { + if err != nil { return nil, err } - - return ret, nil + return items, nil } type QueryLogLinePage struct { @@ -283,73 +310,33 @@ type QueryLogLinePage struct { } // LogGetLinePage 获取log的行分页 -func LogGetLinePage(db *sqlx.DB, param *QueryLogLinePage) ([]*LogOneItem, error) { +func LogGetLinePage(db *gorm.DB, param *QueryLogLinePage) ([]*LogOneItem, error) { // 获取log的ID logID, err := LogGetIDByGroupIDAndName(db, param.GroupID, param.LogName) if err != nil { return nil, err } + var items []*LogOneItem + // 查询行数据 - rows, err := db.Queryx(` -SELECT id, - nickname, - im_userid, - time, - message, - is_dice, - command_id, - command_info, - raw_msg_id, - user_uniform_id -FROM log_items -WHERE log_id =$1 -ORDER BY time ASC -LIMIT $2, $3;`, logID, (param.PageNum-1)*param.PageSize, param.PageSize) + err = db.Model(&LogOneItem{}). + Select("id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id"). + Where("log_id = ?", logID). + Order("time ASC"). + Limit(param.PageSize). + Offset((param.PageNum - 1) * param.PageSize). + Scan(&items).Error if err != nil { return nil, err } - defer rows.Close() - - var ret []*LogOneItem - for rows.Next() { - item := &LogOneItem{} - var commandInfoStr []byte - - // 使用Scan方法将查询结果映射到结构体中 - if err := rows.Scan( - &item.ID, - &item.Nickname, - &item.IMUserID, - &item.Time, - &item.Message, - &item.IsDice, - &item.CommandID, - &commandInfoStr, - &item.RawMsgID, - &item.UniformID, - ); err != nil { - return nil, err - } - - // 反序列化commandInfo - if commandInfoStr != nil { - _ = json.Unmarshal(commandInfoStr, &item.CommandInfo) - } - ret = append(ret, item) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return ret, nil + return items, nil } // LogLinesCountGet 获取日志行数 -func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) { +func LogLinesCountGet(db *gorm.DB, groupID string, logName string) (int64, bool) { // 获取日志 ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil || logID == 0 { @@ -358,9 +345,10 @@ func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) // 获取日志行数 var count int64 - err = db.Get(&count, ` - SELECT COUNT(id) FROM log_items WHERE log_id=$1 AND removed IS NULL - `, logID) + err = db.Model(&LogOneItem{}). + Where("log_id = ? and removed IS NULL", logID). + Count(&count).Error + if err != nil { return 0, false } @@ -369,17 +357,16 @@ func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) } // LogDelete 删除log -func LogDelete(db *sqlx.DB, groupID string, logName string) bool { - // 获取 log id +func LogDelete(db *gorm.DB, groupID string, logName string) bool { + // 获取 log ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil || logID == 0 { return false } - // 获取文本 - // 通过BeginTxx方法开启事务 - tx, err := db.Beginx() - if err != nil { + // 开启事务 + tx := db.Begin() + if err = tx.Error; err != nil { return false } defer func() { @@ -388,41 +375,38 @@ func LogDelete(db *sqlx.DB, groupID string, logName string) bool { } }() - // 删除log_id相关的log_items记录 - _, err = tx.Exec("DELETE FROM log_items WHERE log_id = $1", logID) - if err != nil { + // 删除 log_id 相关的 log_items 记录 + if err = tx.Where("log_id = ?", logID).Delete(&LogOneItem{}).Error; err != nil { return false } - // 删除log_id相关的logs记录 - _, err = tx.Exec("DELETE FROM logs WHERE id = $1", logID) - if err != nil { + // 删除 log_id 相关的 logs 记录 + if err = tx.Where("id = ?", logID).Delete(&LogInfo{}).Error; err != nil { return false } // 提交事务 - err = tx.Commit() + err = tx.Commit().Error return err == nil } // LogAppend 向指定的log中添加一条信息 -func LogAppend(db *sqlx.DB, groupID string, logName string, logItem *LogOneItem) bool { - // 获取 log id +func LogAppend(db *gorm.DB, groupID string, logName string, logItem *LogOneItem) bool { + // 获取 log ID logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return false } - // 如果不存在,创建 + // 获取当前时间戳 now := time.Now() nowTimestamp := now.Unix() // 开始事务 - tx, err := db.Beginx() - if err != nil { + tx := db.Begin() + if err = tx.Error; err != nil { return false } - // 执行事务时发生错误时回滚 defer func() { if err != nil { _ = tx.Rollback() @@ -430,65 +414,68 @@ func LogAppend(db *sqlx.DB, groupID string, logName string, logItem *LogOneItem) }() if logID == 0 { - // 创建一个新的log - query := "INSERT INTO logs (name, group_id, created_at, updated_at) VALUES (?, ?, ?, ?)" - rst, errNew := tx.Exec(query, logName, groupID, nowTimestamp, nowTimestamp) - if errNew != nil { - return false - } - // 获取新创建log的ID - logID, errNew = rst.LastInsertId() - if errNew != nil { + // 创建一个新的 log + newLog := LogInfo{Name: logName, GroupID: groupID, CreatedAt: nowTimestamp, UpdatedAt: nowTimestamp} + if err = tx.Create(&newLog).Error; err != nil { return false } + logID = newLog.ID } - // 向log_items表中添加一条信息 - data, err := json.Marshal(logItem.CommandInfo) - query := "INSERT INTO log_items (log_id, group_id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + // 向 log_items 表中添加一条信息 + // Pinenutn: 由此可以推知,CommandInfo必然是一个 map[string]interface{} - rid := "" - if logItem.RawMsgID != nil { - rid = fmt.Sprintf("%v", logItem.RawMsgID) + if err != nil { + return false } - // fmt.Println("log append", logId, rid, "|", groupId, logName) - _, err = tx.Exec(query, logID, groupID, logItem.Nickname, logItem.IMUserID, nowTimestamp, logItem.Message, logItem.IsDice, logItem.CommandID, data, rid, logItem.UniformID) - _, err = tx.Exec("UPDATE logs SET updated_at = ? WHERE id = ?", nowTimestamp, logID) - if err != nil { + newLogItem := LogOneItem{ + LogID: logID, + GroupID: groupID, + Nickname: logItem.Nickname, + IMUserID: logItem.IMUserID, + Time: nowTimestamp, + Message: logItem.Message, + IsDice: logItem.IsDice, + CommandID: logItem.CommandID, + CommandInfo: logItem.CommandInfo, + RawMsgID: logItem.RawMsgID, + UniformID: logItem.UniformID, + } + + if err = tx.Create(&newLogItem).Error; err != nil { + return false + } + + // 更新 logs 表中的 updated_at 字段 + if err = tx.Model(&LogInfo{}).Where("id = ?", logID).Update("updated_at", nowTimestamp).Error; err != nil { return false } // 提交事务 - err = tx.Commit() + err = tx.Commit().Error return err == nil } // LogMarkDeleteByMsgID 撤回删除 -func LogMarkDeleteByMsgID(db *sqlx.DB, groupID string, logName string, rawID interface{}) error { +func LogMarkDeleteByMsgID(db *gorm.DB, groupID string, logName string, rawID interface{}) error { // 获取 log id logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return err } - - // 删除记录 - rid := "" - if rawID != nil { - rid = fmt.Sprintf("%v", rawID) - } - - // fmt.Printf("log delete %v %d\n", rawId, logId) - _, err = db.Exec("DELETE FROM log_items WHERE log_id=? AND raw_msg_id=?", logID, rid) - if err != nil { - log.Error("log delete error", err.Error()) + 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 { + log.Errorf("log delete error %s", err.Error()) return err } return nil } -func LogEditByMsgID(db *sqlx.DB, groupID, logName, newContent string, rawID interface{}) error { +// LogEditByMsgID 编辑日志 +func LogEditByMsgID(db *gorm.DB, groupID, logName, newContent string, rawID interface{}) error { logID, err := LogGetIDByGroupIDAndName(db, groupID, logName) if err != nil { return err @@ -499,11 +486,11 @@ func LogEditByMsgID(db *sqlx.DB, groupID, logName, newContent string, rawID inte rid = fmt.Sprintf("%v", rawID) } - _, err = db.Exec(`UPDATE log_items -SET message = ? -WHERE log_id = ? AND raw_msg_id = ?`, newContent, logID, rid) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { + // 更新 log_items 表中的内容 + if err := db.Model(&LogOneItem{}). + Where("log_id = ? AND raw_msg_id = ?", logID, rid). + Update("message", newContent).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return fmt.Errorf("log edit: %w", err) diff --git a/dice/model/sqlhook.go b/dice/model/sqlhook.go deleted file mode 100644 index a01464e7..00000000 --- a/dice/model/sqlhook.go +++ /dev/null @@ -1,83 +0,0 @@ -//go:build !cgo -// +build !cgo - -package model - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/glebarez/go-sqlite" - "github.com/qustavo/sqlhooks/v2" - - log "sealdice-core/utils/kratos" -) - -// 覆盖驱动名 sqlite3 会导致 panic, 因此需要创建新的驱动. -// -// database/sql/sql.go:51 -const zapDriverName = "sqlite3-log" - -func InitZapHook(log *log.Helper) { - hook := &zapHook{Helper: log, IsPrintSQLDuration: true} - sql.Register(zapDriverName, sqlhooks.Wrap(new(sqlite.Driver), hook)) -} - -// make sure zapHook implement all sqlhooks interface. -var _ interface { - sqlhooks.Hooks - sqlhooks.OnErrorer -} = (*zapHook)(nil) - -// zapHook 使用 zap 记录 SQL 查询和参数 -type zapHook struct { - *log.Helper - - // 是否打印 SQL 耗时 - IsPrintSQLDuration bool -} - -// sqlDurationKey 是 context.valueCtx Key -type sqlDurationKey struct{} - -func buildQueryArgsFields(query string, args ...interface{}) []interface{} { - if len(args) == 0 { - return []interface{}{"查询", query} - } - return []interface{}{"查询", query, "参数", args} -} - -func (z *zapHook) Before(ctx context.Context, _ string, _ ...interface{}) (context.Context, error) { - if z == nil || z.Helper == nil { - return ctx, nil - } - - if z.IsPrintSQLDuration { - ctx = context.WithValue(ctx, (*sqlDurationKey)(nil), time.Now()) - } - return ctx, nil -} - -func (z *zapHook) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) { - if z == nil || z.Helper == nil { - return ctx, nil - } - - var durationField string - if v, ok := ctx.Value((*sqlDurationKey)(nil)).(time.Time); ok { - durationField = fmt.Sprintf("%v", time.Since(v)) - } - - z.Debugf("SQL 执行后: %v 耗时: %v 秒", buildQueryArgsFields(query, args...), durationField) - return ctx, nil -} - -func (z *zapHook) OnError(_ context.Context, err error, query string, args ...interface{}) error { - if z == nil || z.Helper == nil { - return nil - } - z.Debugf("SQL 执行出错日志 %v 报错为: %v", buildQueryArgsFields(query, args...), err) - return nil -} diff --git a/dice/model/sqlhook_cgo.go b/dice/model/sqlhook_cgo.go deleted file mode 100644 index 6bd11479..00000000 --- a/dice/model/sqlhook_cgo.go +++ /dev/null @@ -1,83 +0,0 @@ -//go:build cgo -// +build cgo - -package model - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/mattn/go-sqlite3" - "github.com/qustavo/sqlhooks/v2" - - log "sealdice-core/utils/kratos" -) - -// 覆盖驱动名 sqlite3 会导致 panic, 因此需要创建新的驱动. -// -// database/sql/sql.go:51 -const zapDriverName = "sqlite3-log" - -func InitZapHook(log *log.Helper) { - hook := &zapHook{Helper: log, IsPrintSQLDuration: true} - sql.Register(zapDriverName, sqlhooks.Wrap(new(sqlite3.SQLiteDriver), hook)) -} - -// make sure zapHook implement all sqlhooks interface. -var _ interface { - sqlhooks.Hooks - sqlhooks.OnErrorer -} = (*zapHook)(nil) - -// zapHook 使用 zap 记录 SQL 查询和参数 -type zapHook struct { - *log.Helper - - // 是否打印 SQL 耗时 - IsPrintSQLDuration bool -} - -// sqlDurationKey 是 context.valueCtx Key -type sqlDurationKey struct{} - -func buildQueryArgsFields(query string, args ...interface{}) []interface{} { - if len(args) == 0 { - return []interface{}{"查询", query} - } - return []interface{}{"查询", query, "参数", args} -} - -func (z *zapHook) Before(ctx context.Context, _ string, _ ...interface{}) (context.Context, error) { - if z == nil || z.Helper == nil { - return ctx, nil - } - - if z.IsPrintSQLDuration { - ctx = context.WithValue(ctx, (*sqlDurationKey)(nil), time.Now()) - } - return ctx, nil -} - -func (z *zapHook) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) { - if z == nil || z.Helper == nil { - return ctx, nil - } - - var durationField string - if v, ok := ctx.Value((*sqlDurationKey)(nil)).(time.Time); ok { - durationField = fmt.Sprintf("%v", time.Since(v)) - } - - z.Debugf("SQL 执行后: %v 耗时: %v 秒", buildQueryArgsFields(query, args...), durationField) - return ctx, nil -} - -func (z *zapHook) OnError(_ context.Context, err error, query string, args ...interface{}) error { - if z == nil || z.Helper == nil { - return nil - } - z.Debugf("SQL 执行出错日志 %v 报错为: %v", buildQueryArgsFields(query, args...), err) - return nil -} diff --git a/dice/platform_adapter_gocq_actions.go b/dice/platform_adapter_gocq_actions.go index 78d6b819..9b839533 100644 --- a/dice/platform_adapter_gocq_actions.go +++ b/dice/platform_adapter_gocq_actions.go @@ -139,6 +139,7 @@ func socketSendText(socket *gowebsocket.Socket, s string) { }() if socket != nil { + // 什么也不做,这样就能用来做不发话的测试 socket.SendText(s) } } diff --git a/dice/storylog/storylog.go b/dice/storylog/storylog.go index 1741e026..28dd9bef 100644 --- a/dice/storylog/storylog.go +++ b/dice/storylog/storylog.go @@ -9,7 +9,7 @@ import ( "net/http" "strconv" - "github.com/jmoiron/sqlx" + "gorm.io/gorm" "sealdice-core/dice/model" log "sealdice-core/utils/kratos" @@ -17,7 +17,7 @@ import ( type UploadEnv struct { Dir string - Db *sqlx.DB + Db *gorm.DB Log *log.Helper Backends []string Version StoryVersion diff --git a/go.mod b/go.mod index 24d58418..5162982d 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/fyrchik/go-shlex v0.0.0-20210215145004-cd7f49bfd959 github.com/gen2brain/beeep v0.0.0-20230907135156-1a38885a97fc github.com/glebarez/go-sqlite v1.22.0 + github.com/glebarez/sqlite v1.11.0 github.com/go-creed/sat v1.0.3 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/golang-module/carbon v1.7.3 @@ -38,7 +39,7 @@ require ( github.com/lonelyevil/kook/log_adapter/plog v0.0.31 github.com/lxn/win v0.0.0-20210218163916-a377121e959e github.com/matoous/go-nanoid/v2 v2.1.0 - github.com/mattn/go-sqlite3 v1.14.23 + github.com/mattn/go-sqlite3 v1.14.24 github.com/mitchellh/mapstructure v1.5.0 github.com/monaco-io/request v1.0.16 github.com/mozillazg/go-pinyin v0.20.0 @@ -49,7 +50,6 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 github.com/phuslu/log v1.0.88 github.com/pkg/errors v0.9.1 - github.com/qustavo/sqlhooks/v2 v2.1.0 github.com/robfig/cron/v3 v3.0.1 github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105 github.com/sahilm/fuzzy v0.1.1 @@ -61,9 +61,7 @@ require ( github.com/sunshineplan/imgconv v1.1.4 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a github.com/tdewolff/minify/v2 v2.20.37 - github.com/tidwall/buntdb v1.3.2 - github.com/tidwall/gjson v1.18.0 - github.com/tidwall/sjson v1.2.5 + github.com/tidwall/buntdb v1.3.1 github.com/vmihailenco/msgpack v4.0.4+incompatible github.com/xuri/excelize/v2 v2.9.0 github.com/yuin/goldmark v1.7.4 @@ -77,10 +75,20 @@ require ( gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/go-gorm/caches/v4 v4.0.5 + github.com/spaolacci/murmur3 v1.1.0 + github.com/tidwall/gjson v1.17.0 + github.com/tidwall/sjson v1.2.5 moul.io/zapfilter v1.7.0 ) require ( + github.com/BurntSushi/toml v1.2.1 // indirect github.com/RoaringBitmap/roaring v1.2.3 // indirect github.com/bits-and-blooms/bitset v1.2.2 // indirect github.com/bits-and-blooms/bloom/v3 v3.2.0 // indirect @@ -110,7 +118,7 @@ require ( github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect - github.com/go-ole/go-ole v1.2.4 // indirect + 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-stack/stack v1.8.0 // indirect @@ -119,6 +127,7 @@ require ( github.com/gobuffalo/packd v0.3.0 // indirect github.com/gobuffalo/packr v1.30.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/geo v0.0.0-20230404232722-c4acd7a044dc // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -126,8 +135,11 @@ 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/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/joho/godotenv v1.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -180,6 +192,7 @@ require ( replace ( github.com/Szzrain/dodo-open-go v0.2.7 => github.com/sealdice/dodo-open-go v0.2.8 + github.com/glebarez/sqlite v1.11.0 => github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685 github.com/lonelyevil/kook v0.0.31 => github.com/sealdice/kook v0.0.3 github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105 => github.com/fy0/GoWebsocket v0.0.0-20231128163937-aa5c110b25c6 ) diff --git a/go.sum b/go.sum index 9f0ddd4a..93e11222 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,14 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Milly/go-base2048 v0.1.0 h1:7ZgpCR3cjcAAVqIo+B8Q3P1+VFHRS8zilzAq062rUUk= github.com/Milly/go-base2048 v0.1.0/go.mod h1:kl6eYBwGnoIjv8k9UmgS+bekm6870ojptcVnT11e3jE= +github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685 h1:O6OPpCufcEJD+eWENAwuwVZHONKmP77X2wlzMTQZ5gg= +github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685/go.mod h1:GajiCpqLxU0a1gP13oAEiJAx9r87kVSdfEQy4O69ZTo= github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY= github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a h1:NPnGVqpua4c1iEFVdxnBJA9viP5bo2Zp2jfflbcjdto= @@ -124,15 +127,17 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/go-creed/sat v1.0.3 h1:V1IkiYYFDPKXaRhdg95oAh5IHZ3Qhs5AEVlhteM+6XA= github.com/go-creed/sat v1.0.3/go.mod h1:ZxAhQ0ikMzjqeMbFeoMdCr6es8p10Y87F2nHkqNjSbY= -github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-gorm/caches/v4 v4.0.5 h1:Sdj9vxbEM0sCmv5+s5o6GzoVMuraWF0bjJJvUU+7c1U= +github.com/go-gorm/caches/v4 v4.0.5/go.mod h1:Ms8LnWVoW4GkTofpDzFH8OfDGNTjLxQDyxBmRN67Ujw= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= 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.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 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= @@ -154,6 +159,8 @@ github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIavi github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U= @@ -202,6 +209,10 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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= @@ -218,8 +229,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -230,7 +241,6 @@ github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+k github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lonelyevil/kook v0.0.29/go.mod h1:WjHC7AmbmNjInT/U/etBVOmAw7T6EqdCwApceRGs1sk= @@ -249,10 +259,9 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= -github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -288,7 +297,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0 h1:DL64ORGMk6AUB8q5LbRp8KRFn4oHhdrSepBmbMrtmNo= github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= @@ -304,13 +312,12 @@ github.com/phuslu/log v1.0.80/go.mod h1:kzJN3LRifrepxThMjufQwS7S35yFAB+jAV1qgA7e github.com/phuslu/log v1.0.88 h1:kivXMpYQ2hd9BxiJNhRM5xnaEZaGunQYlnRQdk/aBw8= github.com/phuslu/log v1.0.88/go.mod h1:F8osGJADo5qLK/0F88djWwdyoZZ9xDJQL1HYRHFEkS0= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qustavo/sqlhooks/v2 v2.1.0 h1:54yBemHnGHp/7xgT+pxwmIlMSDNYKx5JW5dfRAiCZi0= -github.com/qustavo/sqlhooks/v2 v2.1.0/go.mod h1:aMREyKo7fOKTwiLuWPsaHRXEmtqG4yREztO0idF83AU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= @@ -326,6 +333,7 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -394,13 +402,13 @@ github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg= -github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= +github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o= +github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= @@ -503,6 +511,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -599,6 +608,10 @@ 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/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.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= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= diff --git a/main.go b/main.go index 46ad1169..55c71738 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io/fs" "mime" @@ -17,6 +18,7 @@ import ( // _ "net/http/pprof" + "github.com/gofrs/flock" "github.com/jessevdk/go-flags" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -33,15 +35,17 @@ import ( "sealdice-core/utils/paniclog" ) -/** +/* +* 二进制目录结构: data/configs data/extensions data/logs - extensions/ */ +var sealLock = flock.New("sealdice-lock.lock") + func cleanupCreate(diceManager *dice.DiceManager) func() { return func() { log.Info("程序即将退出,进行清理……") @@ -54,6 +58,10 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { exec.Command("pause") // windows专属 } } + err = sealLock.Unlock() + if err != nil { + log.Errorf("文件锁归还出现异常 %v", err) + } if !diceManager.CleanupFlag.CompareAndSwap(0, 1) { // 尝试更新cleanup标记,如果已经为1则退出 @@ -91,7 +99,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { dbData := d.DBData if dbData != nil { d.DBData = nil - _ = dbData.Close() + db, err := dbData.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -102,7 +114,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { dbLogs := d.DBLogs if dbLogs != nil { d.DBLogs = nil - _ = dbLogs.Close() + db, err := dbLogs.DB() + if err != nil { + return + } + _ = db.Close() } })() @@ -114,7 +130,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() { if cm != nil && cm.DB != nil { dbCensor := cm.DB cm.DB = nil - _ = dbCensor.Close() + db, err := dbCensor.DB() + if err != nil { + return + } + _ = db.Close() } })() } @@ -205,8 +225,19 @@ func main() { paniclog.InitPanicLog() // 3. 提示日志打印 log.Info("运行日志开始记录,海豹出现故障时可查看 data/main.log 与 data/panic.log 获取更多信息") - logger := log.NewCustomHelper("SQLX", false, nil) - model.InitZapHook(logger) + // 初始化文件加锁系统 + + locked, err := sealLock.TryLock() + // 如果有错误,或者未能取到锁 + if err != nil || !locked { + // 打日志的时候防止打出nil + if err == nil { + err = errors.New("海豹正在运行中") + } + log.Errorf("获取锁文件失败,原因为: %v", err) + showMsgBox("获取锁文件失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份海豹程序!") + return + } judge, osr := oschecker.OldVersionCheck() // 预留收集信息的接口,如果有需要可以考虑从这里拿数据。不从这里做提示的原因是Windows和Linux的展示方式不同。 if judge { @@ -374,11 +405,11 @@ func main() { } // 删除遗留的shm和wal文件 - if !model.DBCacheDelete() { - log.Error("数据库缓存文件删除失败") - showMsgBox("数据库缓存文件删除失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份程序,或有其他程序正在使用数据库文件!") - return - } + // if !model.DBCacheDelete() { + // log.Error("数据库缓存文件删除失败") + // showMsgBox("数据库缓存文件删除失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份程序,或有其他程序正在使用数据库文件!") + // return + // } // 尝试进行升级 migrate.TryMigrateToV12() diff --git a/migrate/db_util.go b/migrate/db_util.go index 465f0f58..9ed44029 100644 --- a/migrate/db_util.go +++ b/migrate/db_util.go @@ -5,11 +5,24 @@ package migrate import ( _ "github.com/glebarez/go-sqlite" + "github.com/glebarez/sqlite" "github.com/jmoiron/sqlx" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/utils" ) func openDB(path string) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite", path) + gdb, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + panic(err) + } + db, err := utils.GetSQLXDB(gdb) + // db, err := sqlx.Open("sqlite", path) if err != nil { panic(err) } diff --git a/migrate/db_util_cgo.go b/migrate/db_util_cgo.go index 4c9f17e4..4b8ba915 100644 --- a/migrate/db_util_cgo.go +++ b/migrate/db_util_cgo.go @@ -6,10 +6,22 @@ package migrate import ( "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "sealdice-core/utils" ) func openDB(path string) (*sqlx.DB, error) { - db, err := sqlx.Open("sqlite3", path) + gdb, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + // 注意,这里虽然是Info,但实际上打印就变成了Debug. + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + panic(err) + } + db, err := utils.GetSQLXDB(gdb) if err != nil { panic(err) } diff --git a/migrate/v150_attrs.go b/migrate/v150_attrs.go index 406eb51e..62cfbf35 100644 --- a/migrate/v150_attrs.go +++ b/migrate/v150_attrs.go @@ -418,22 +418,19 @@ func V150Upgrade() bool { fmt.Fprintln(os.Stdout, "1.5 数据迁移") sheetIdBindByGroupUserId = map[string]string{} - + // Pinenutn: 2024-10-28 我要把这个注释全文背诵,它扰乱了GORM的初始化逻辑 + // -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化 sqls := []string{ ` CREATE TABLE IF NOT EXISTS attrs ( id TEXT PRIMARY KEY, data BYTEA, attrs_type TEXT, - - -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化 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 ); diff --git a/utils/convertdb.go b/utils/convertdb.go new file mode 100644 index 00000000..97e5c773 --- /dev/null +++ b/utils/convertdb.go @@ -0,0 +1,23 @@ +package utils + +import ( + "github.com/jmoiron/sqlx" + "gorm.io/gorm" +) + +// GetSQLXDB 将 GORM 的 *gorm.DB 转换为 *sqlx.DB,并自动获取驱动名称,用于需要sqlx的场景 +func GetSQLXDB(db *gorm.DB) (*sqlx.DB, error) { + // 获取底层的 *sql.DB + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // 获取 GORM 使用的驱动名称 + driverName := db.Dialector.Name() + + // 使用 sqlx.NewDb 传递现有的 *sql.DB 和驱动名称 + sqlxDB := sqlx.NewDb(sqlDB, driverName) + + return sqlxDB, nil +} diff --git a/utils/kratos/gormlogger.go b/utils/kratos/gormlogger.go new file mode 100644 index 00000000..ab541dee --- /dev/null +++ b/utils/kratos/gormlogger.go @@ -0,0 +1,113 @@ +package log + +import ( + "context" + "errors" + "fmt" + "time" + + "go.uber.org/zap/zapcore" + gormlogger "gorm.io/gorm/logger" +) + +// gorm的格式化字符串抄过来 +var ( + infoStr = "[info] " + warnStr = "[warn] " + errStr = "[error] " + traceStr = "[%.3fms] [rows:%v] %s" + traceWarnStr = "%s\n[%.3fms] [rows:%v] %s" + traceErrStr = "%s\n[%.3fms] [rows:%v] %s" +) + +type ContextFn func(ctx context.Context) []zapcore.Field + +type GORMLogger struct { + // 要被传入的KartosLogger + ZapLogger *Helper + // 原本的Logger有 + LogLevel gormlogger.LogLevel + // 原本的logger有 + SlowThreshold time.Duration + // 原本的logger有 + IgnoreRecordNotFoundError bool + // logger缺少的 + ParameterizedQueries bool + SkipCallerLookup bool + Context ContextFn +} + +func NewGormLogger(zapLogger *Helper) GORMLogger { + return GORMLogger{ + ZapLogger: zapLogger, + LogLevel: gormlogger.Warn, + SlowThreshold: 100 * time.Millisecond, + IgnoreRecordNotFoundError: false, + Context: nil, + } +} + +func (l GORMLogger) SetAsDefault() { + gormlogger.Default = l +} + +func (l GORMLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface { + return GORMLogger{ + ZapLogger: l.ZapLogger, + SlowThreshold: l.SlowThreshold, + LogLevel: level, + SkipCallerLookup: l.SkipCallerLookup, + IgnoreRecordNotFoundError: l.IgnoreRecordNotFoundError, + Context: l.Context, + } +} + +func (l GORMLogger) Info(_ context.Context, msg string, args ...interface{}) { + if l.LogLevel >= gormlogger.Info { + l.ZapLogger.Infof(infoStr+msg, args...) + } +} + +func (l GORMLogger) Warn(_ context.Context, msg string, args ...interface{}) { + if l.LogLevel >= gormlogger.Warn { + l.ZapLogger.Warnf(warnStr+msg, args...) + } +} + +func (l GORMLogger) Error(_ context.Context, msg string, args ...interface{}) { + if l.LogLevel >= gormlogger.Error { + l.ZapLogger.Errorf(errStr+msg, args...) + } +} + +func (l GORMLogger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) { + if l.LogLevel <= gormlogger.Silent { + return + } + + elapsed := time.Since(begin) + switch { + case err != nil && l.LogLevel >= gormlogger.Error && (!errors.Is(err, gormlogger.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError): + sql, rows := fc() + if rows == -1 { + l.ZapLogger.Errorf(traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.ZapLogger.Errorf(traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= gormlogger.Warn: + sql, rows := fc() + slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold) + if rows == -1 { + l.ZapLogger.Warnf(traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.ZapLogger.Warnf(traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + case l.LogLevel == gormlogger.Info: + sql, rows := fc() + if rows == -1 { + l.ZapLogger.Debugf(traceStr, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.ZapLogger.Debugf(traceStr, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + } +} diff --git a/utils/kratos/zap.go b/utils/kratos/zap.go index 64c8c733..b3883390 100644 --- a/utils/kratos/zap.go +++ b/utils/kratos/zap.go @@ -104,18 +104,35 @@ func InitZapWithKartosLog(level zapcore.Level) { // 创建带有调用者信息的日志记录器,注意跳过两层,这样就能正常提供给log originZapLogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2)) - // 设置全局日志记录器,默认全局记录器为SEAL命名空间 global.SetLogger(NewZapLogger(originZapLogger.Named(LOG_SEAL))) + // GORM部分 + SetDefaultGoRMLogger() +} + +func SetDefaultGoRMLogger() { + gormpath := "./data/database.log" + gormpathlumlog := &lumberjack.Logger{ + Filename: gormpath, // 日志文件的名称和路径 + MaxSize: 10, // 每个日志文件最大10MB + MaxBackups: 3, // 最多保留3个旧日志文件 + MaxAge: 7, // 日志文件保存7天 + } + gormCore := zapcore.NewCore(getEncoder(), zapcore.AddSync(gormpathlumlog), zapcore.DebugLevel) + // 层层进行包装 + gormZapLogger := NewHelper(NewZapLogger(zap.New(gormCore).Named("GORM").WithOptions(zap.WithCaller(true), zap.AddCallerSkip(6)))) + + NewGormLogger(gormZapLogger).SetAsDefault() } func GetWebLogger() *Helper { webpath := "./data/web.log" + // WEB的可以少一点点~ weblumlog := &lumberjack.Logger{ Filename: webpath, // 日志文件的名称和路径 - MaxSize: 10, // 每个日志文件最大10MB - MaxBackups: 3, // 最多保留3个旧日志文件 - MaxAge: 7, // 日志文件保存7天 + MaxSize: 5, // 每个日志文件最大5MB + MaxBackups: 3, // 最多保留1个旧日志文件 + MaxAge: 3, // 日志文件保存3天 } webCore := zapcore.NewCore(getEncoder(), zapcore.AddSync(weblumlog), zapcore.DebugLevel) webZapLogger := zap.New(webCore, zap.WithCaller(false)) diff --git a/utils/paniclog/paniclog.go b/utils/paniclog/paniclog.go index e8aadaa7..f5ea89ee 100644 --- a/utils/paniclog/paniclog.go +++ b/utils/paniclog/paniclog.go @@ -10,22 +10,26 @@ import ( ) func InitPanicLog() { + // TODO: 全局写死写入在data目录,这东西几乎没有任何值得配置的 + if err := os.MkdirAll("./data", 0755); err != nil { + log.Fatalf("未发现data文件夹,且未能创建data文件夹,请检查写入权限: %v", err) + } f, err := os.OpenFile("./data/panic.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640) if err != nil { - log.Fatalf("Failed to open log file: %v", err) + log.Fatalf("未能创建panic日志文件,请检查写入权限: %v", err) } // Copied from https://github.com/rclone/rclone/tree/master/fs/log // 这里GPT说,因为使用了APPEND,所以保证了不需要使用SEEK。但是rclone既然这么用了,我决定相信rclone的处理。 _, err = f.Seek(0, io.SeekEnd) if err != nil { - log.Errorf("Failed to seek log file to end: %v", err) + log.Errorf("移动写入位置到末尾失败,请检查写入权限: %v", err) } currentTime := time.Now().Format("2006-01-02 15:04:05") separator := fmt.Sprintf("\n-------- %s --------\n", currentTime) // 将分割线写入文件 _, err = f.WriteString(separator) if err != nil { - log.Fatalf("Failed to write separator to log file: %v", err) + log.Fatalf("写入Panic日志分割线失败,请检查写入权限: %v", err) } redirectStderr(f) }