package dao import ( "errors" "github.com/sirupsen/logrus" "gorm.io/gorm" "hy2xs-admin/model/constant" "hy2xs-admin/model/entity" "time" ) func SaveConfig(config entity.Config) (int64, error) { if tx := sqliteDB.Save(&config); tx.Error != nil { logrus.Errorf("%v", tx.Error) return 0, errors.New(constant.SysError) } return *config.Id, nil } func UpdateConfig(keys []string, updates map[string]interface{}) error { return updateConfigOn(sqliteDB, keys, updates) } // UpsertConfigValue записывает значение ключа, создавая строку, если её нет. // // Заменяет две зеркальные самодельные схемы, которые жили в sqlite.go и обе // были неверны: // // 1. «сначала INSERT, при ошибке UPDATE» — работало, но на КАЖДОЙ первой // загрузке печатало в журнал `duplicated key not allowed` уровня error, по // одной строке на секрет. Оператор видел три ошибки на совершенно здоровом // старте и не имел способа отличить их от настоящих; // // 2. «сначала UPDATE, при ошибке INSERT» — тихо не делало НИЧЕГО, если строки // не было. UPDATE без совпавших строк не является ошибкой: gorm возвращает // nil и RowsAffected = 0, поэтому ветка INSERT не выполнялась, а вызывающий // получал сгенерированный секрет как успешно сохранённый. Для JWT_SECRET // это означало бы подпись токенов ключом, которого нет в базе. Спасала // только предварительная вставка пустых строк в seedBaseConfig — то есть // корректность держалась на порядке выполнения миграций. // // Здесь решение принимается по RowsAffected, а не по наличию ошибки. func UpsertConfigValue(key string, value string) error { if sqliteDB == nil { return errors.New("sqlite is not initialized") } tx := sqliteDB.Model(&entity.Config{}). Where("key = ?", key). Updates(map[string]interface{}{ "value": value, "update_time": time.Now().Format("2006-01-02 15:04:05"), }) if tx.Error != nil { logrus.Errorf("%v", tx.Error) return errors.New(constant.SysError) } if tx.RowsAffected > 0 { return nil } remark := key if _, err := SaveConfig(entity.Config{Key: &key, Value: &value, Remark: &remark}); err != nil { return err } return nil } // ConfigTx — записи в таблицу `config` в границах одной единицы работы. // // Интерфейс повторяет PeerTx и по той же причине: транзакция должна быть // доступна слою service, а *gorm.DB туда протекать не должен. Реализация одна // и та же для глобального соединения и для транзакции, поэтому «обычный» и // «транзакционный» пути не могут разойтись в поведении. type ConfigTx interface { UpdateConfig(keys []string, updates map[string]interface{}) error } type configTx struct { db *gorm.DB } // WithConfigTx выполняет fn в одной транзакции: либо применяются все записи // партии, либо ни одной. // // Что было. updateConfigs валидировал и записывал настройки в одном цикле: // проверил элемент — записал, проверил следующий. Партия из разрешённого // ключа и запрещённого применяла первый и возвращала ошибку на втором, то есть // оператор получал отказ на запрос, который систему уже изменил. Тест на этот // случай существовал, но ставил запрещённый ключ ПЕРВЫМ и не смотрел в базу, // поэтому поймать частичное применение был неспособен по построению. func WithConfigTx(fn func(tx ConfigTx) error) error { if sqliteDB == nil { return errors.New("sqlite is not initialized") } return sqliteDB.Transaction(func(tx *gorm.DB) error { return fn(configTx{db: tx}) }) } func (c configTx) UpdateConfig(keys []string, updates map[string]interface{}) error { return updateConfigOn(c.db, keys, updates) } func updateConfigOn(db *gorm.DB, keys []string, updates map[string]interface{}) error { if len(updates) == 0 { return nil } updates["update_time"] = time.Now().Format("2006-01-02 15:04:05") if tx := db.Model(&entity.Config{}). Where("key in ?", keys). Updates(updates); tx.Error != nil { logrus.Errorf("%v", tx.Error) return errors.New(constant.SysError) } return nil } func GetConfig(query interface{}, args ...interface{}) (entity.Config, error) { var config entity.Config if tx := sqliteDB.Model(&entity.Config{}). Where(query, args...).First(&config); tx.Error != nil { if tx.Error == gorm.ErrRecordNotFound { return config, errors.New(constant.ConfigNotExist) } logrus.Errorf("%v", tx.Error) return config, errors.New(constant.SysError) } return config, nil } func ListConfig(query interface{}, args ...interface{}) ([]entity.Config, error) { var configs []entity.Config if tx := sqliteDB.Model(&entity.Config{}). Where(query, args...).Order("create_time desc").Find(&configs); tx.Error != nil { logrus.Errorf("%v", tx.Error) return configs, errors.New(constant.SysError) } return configs, nil }