Commit dfc0d632 by Calcium-Ion Committed by GitHub

Merge commit from fork

* Harden user setting cache updates

* Fix user update test isolation
parent bfddc5fe
...@@ -89,8 +89,7 @@ func UpdateSubscriptionPreference(c *gin.Context) { ...@@ -89,8 +89,7 @@ func UpdateSubscriptionPreference(c *gin.Context) {
} }
current := user.GetSetting() current := user.GetSetting()
current.BillingPreference = pref current.BillingPreference = pref
user.SetSetting(current) if err := model.UpdateUserSetting(user.Id, current); err != nil {
if err := user.Update(false); err != nil {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
......
...@@ -732,8 +732,7 @@ func AdminClearUserBinding(c *gin.Context) { ...@@ -732,8 +732,7 @@ func AdminClearUserBinding(c *gin.Context) {
func UpdateSelf(c *gin.Context) { func UpdateSelf(c *gin.Context) {
var requestData map[string]interface{} var requestData map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&requestData) if err := common.DecodeJson(c.Request.Body, &requestData); err != nil {
if err != nil {
common.ApiErrorI18n(c, i18n.MsgInvalidParams) common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return return
} }
...@@ -755,9 +754,7 @@ func UpdateSelf(c *gin.Context) { ...@@ -755,9 +754,7 @@ func UpdateSelf(c *gin.Context) {
currentSetting.SidebarModules = sidebarModulesStr currentSetting.SidebarModules = sidebarModulesStr
} }
// 保存更新后的设置 if err := model.UpdateUserSetting(user.Id, currentSetting); err != nil {
user.SetSetting(currentSetting)
if err := user.Update(false); err != nil {
common.ApiErrorI18n(c, i18n.MsgUpdateFailed) common.ApiErrorI18n(c, i18n.MsgUpdateFailed)
return return
} }
...@@ -783,9 +780,7 @@ func UpdateSelf(c *gin.Context) { ...@@ -783,9 +780,7 @@ func UpdateSelf(c *gin.Context) {
currentSetting.Language = langStr currentSetting.Language = langStr
} }
// 保存更新后的设置 if err := model.UpdateUserSetting(user.Id, currentSetting); err != nil {
user.SetSetting(currentSetting)
if err := user.Update(false); err != nil {
common.ApiErrorI18n(c, i18n.MsgUpdateFailed) common.ApiErrorI18n(c, i18n.MsgUpdateFailed)
return return
} }
...@@ -796,13 +791,12 @@ func UpdateSelf(c *gin.Context) { ...@@ -796,13 +791,12 @@ func UpdateSelf(c *gin.Context) {
// 原有的用户信息更新逻辑 // 原有的用户信息更新逻辑
var user model.User var user model.User
requestDataBytes, err := json.Marshal(requestData) requestDataBytes, err := common.Marshal(requestData)
if err != nil { if err != nil {
common.ApiErrorI18n(c, i18n.MsgInvalidParams) common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return return
} }
err = json.Unmarshal(requestDataBytes, &user) if err = common.Unmarshal(requestDataBytes, &user); err != nil {
if err != nil {
common.ApiErrorI18n(c, i18n.MsgInvalidParams) common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return return
} }
...@@ -1434,8 +1428,7 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1434,8 +1428,7 @@ func UpdateUserSetting(c *gin.Context) {
} }
// 更新用户设置 // 更新用户设置
user.SetSetting(settings) if err := model.UpdateUserSetting(user.Id, settings); err != nil {
if err := user.Update(false); err != nil {
common.ApiErrorI18n(c, i18n.MsgUpdateFailed) common.ApiErrorI18n(c, i18n.MsgUpdateFailed)
return return
} }
......
...@@ -2,7 +2,6 @@ package model ...@@ -2,7 +2,6 @@ package model
import ( import (
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
...@@ -83,7 +82,7 @@ func (user *User) SetAccessToken(token string) { ...@@ -83,7 +82,7 @@ func (user *User) SetAccessToken(token string) {
func (user *User) GetSetting() dto.UserSetting { func (user *User) GetSetting() dto.UserSetting {
setting := dto.UserSetting{} setting := dto.UserSetting{}
if user.Setting != "" { if user.Setting != "" {
err := json.Unmarshal([]byte(user.Setting), &setting) err := common.Unmarshal([]byte(user.Setting), &setting)
if err != nil { if err != nil {
common.SysLog("failed to unmarshal setting: " + err.Error()) common.SysLog("failed to unmarshal setting: " + err.Error())
} }
...@@ -92,7 +91,7 @@ func (user *User) GetSetting() dto.UserSetting { ...@@ -92,7 +91,7 @@ func (user *User) GetSetting() dto.UserSetting {
} }
func (user *User) SetSetting(setting dto.UserSetting) { func (user *User) SetSetting(setting dto.UserSetting) {
settingBytes, err := json.Marshal(setting) settingBytes, err := common.Marshal(setting)
if err != nil { if err != nil {
common.SysLog("failed to marshal setting: " + err.Error()) common.SysLog("failed to marshal setting: " + err.Error())
return return
...@@ -100,6 +99,21 @@ func (user *User) SetSetting(setting dto.UserSetting) { ...@@ -100,6 +99,21 @@ func (user *User) SetSetting(setting dto.UserSetting) {
user.Setting = string(settingBytes) user.Setting = string(settingBytes)
} }
func UpdateUserSetting(userId int, setting dto.UserSetting) error {
if userId == 0 {
return errors.New("id 为空!")
}
settingBytes, err := common.Marshal(setting)
if err != nil {
return err
}
settingValue := string(settingBytes)
if err = DB.Model(&User{}).Where("id = ?", userId).Update("setting", settingValue).Error; err != nil {
return err
}
return updateUserSettingCache(userId, settingValue)
}
// 根据用户角色生成默认的边栏配置 // 根据用户角色生成默认的边栏配置
func generateDefaultSidebarConfigForRole(userRole int) string { func generateDefaultSidebarConfigForRole(userRole int) string {
defaultConfig := map[string]interface{}{} defaultConfig := map[string]interface{}{}
...@@ -153,7 +167,7 @@ func generateDefaultSidebarConfigForRole(userRole int) string { ...@@ -153,7 +167,7 @@ func generateDefaultSidebarConfigForRole(userRole int) string {
// 普通用户不包含admin区域 // 普通用户不包含admin区域
// 转换为JSON字符串 // 转换为JSON字符串
configBytes, err := json.Marshal(defaultConfig) configBytes, err := common.Marshal(defaultConfig)
if err != nil { if err != nil {
common.SysLog("生成默认边栏配置失败: " + err.Error()) common.SysLog("生成默认边栏配置失败: " + err.Error())
return "" return ""
...@@ -524,11 +538,14 @@ func (user *User) UpdateWithTx(tx *gorm.DB, updatePassword bool) error { ...@@ -524,11 +538,14 @@ func (user *User) UpdateWithTx(tx *gorm.DB, updatePassword bool) error {
} }
} }
newUser := *user newUser := *user
tx.First(&user, user.Id) current := User{}
if err = tx.Model(user).Updates(newUser).Error; err != nil { if err = tx.First(&current, user.Id).Error; err != nil {
return err return err
} }
return nil if err = tx.Model(&current).Omit("quota", "used_quota", "request_count").Updates(newUser).Error; err != nil {
return err
}
return tx.First(user, user.Id).Error
} }
func (user *User) Edit(updatePassword bool) error { func (user *User) Edit(updatePassword bool) error {
...@@ -558,11 +575,14 @@ func (user *User) EditWithTx(tx *gorm.DB, updatePassword bool) error { ...@@ -558,11 +575,14 @@ func (user *User) EditWithTx(tx *gorm.DB, updatePassword bool) error {
updates["password"] = newUser.Password updates["password"] = newUser.Password
} }
tx.First(&user, user.Id) current := User{}
if err = tx.Model(user).Updates(updates).Error; err != nil { if err = tx.First(&current, user.Id).Error; err != nil {
return err return err
} }
return nil if err = tx.Model(&current).Updates(updates).Error; err != nil {
return err
}
return tx.First(user, user.Id).Error
} }
func (user *User) ClearBinding(bindingType string) error { func (user *User) ClearBinding(bindingType string) error {
......
...@@ -63,8 +63,7 @@ func InvalidateUserCache(userId int) error { ...@@ -63,8 +63,7 @@ func InvalidateUserCache(userId int) error {
return invalidateUserCache(userId) return invalidateUserCache(userId)
} }
// updateUserCache updates all user cache fields using hash func populateUserCache(user User) error {
func updateUserCache(user User) error {
if !common.RedisEnabled { if !common.RedisEnabled {
return nil return nil
} }
...@@ -76,6 +75,28 @@ func updateUserCache(user User) error { ...@@ -76,6 +75,28 @@ func updateUserCache(user User) error {
) )
} }
// updateUserCache refreshes non-quota user cache fields.
// Quota is maintained by atomic quota delta paths and must not be overwritten
// by stale user snapshots from profile/settings updates.
func updateUserCache(user User) error {
if !common.RedisEnabled {
return nil
}
if err := updateUserGroupCache(user.Id, user.Group); err != nil {
return err
}
if err := updateUserEmailCache(user.Id, user.Email); err != nil {
return err
}
if err := updateUserStatusCache(user.Id, user.Status == common.UserStatusEnabled); err != nil {
return err
}
if err := updateUserNameCache(user.Id, user.Username); err != nil {
return err
}
return updateUserSettingCache(user.Id, user.Setting)
}
// GetUserCache gets complete user cache from hash // GetUserCache gets complete user cache from hash
func GetUserCache(userId int) (userCache *UserBase, err error) { func GetUserCache(userId int) (userCache *UserBase, err error) {
var user *User var user *User
...@@ -84,7 +105,7 @@ func GetUserCache(userId int) (userCache *UserBase, err error) { ...@@ -84,7 +105,7 @@ func GetUserCache(userId int) (userCache *UserBase, err error) {
// Update Redis cache asynchronously on successful DB read // Update Redis cache asynchronously on successful DB read
if shouldUpdateRedis(fromDB, err) && user != nil { if shouldUpdateRedis(fromDB, err) && user != nil {
gopool.Go(func() { gopool.Go(func() {
if err := updateUserCache(*user); err != nil { if err := populateUserCache(*user); err != nil {
common.SysLog("failed to update user status cache: " + err.Error()) common.SysLog("failed to update user status cache: " + err.Error())
} }
}) })
...@@ -214,6 +235,13 @@ func UpdateUserGroupCache(userId int, group string) error { ...@@ -214,6 +235,13 @@ func UpdateUserGroupCache(userId int, group string) error {
return updateUserGroupCache(userId, group) return updateUserGroupCache(userId, group)
} }
func updateUserEmailCache(userId int, email string) error {
if !common.RedisEnabled {
return nil
}
return common.RedisHSetField(getUserCacheKey(userId), "Email", email)
}
func updateUserNameCache(userId int, username string) error { func updateUserNameCache(userId int, username string) error {
if !common.RedisEnabled { if !common.RedisEnabled {
return nil return nil
......
package model
import (
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/dto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func setupUserUpdateTestState(t *testing.T) {
t.Helper()
truncateTables(t)
require.NoError(t, DB.Exec("DELETE FROM users").Error)
oldRedisEnabled := common.RedisEnabled
oldBatchUpdateEnabled := common.BatchUpdateEnabled
common.RedisEnabled = false
common.BatchUpdateEnabled = false
t.Cleanup(func() {
common.RedisEnabled = oldRedisEnabled
common.BatchUpdateEnabled = oldBatchUpdateEnabled
})
}
func TestUserUpdateDoesNotOverwriteAccountingFields(t *testing.T) {
setupUserUpdateTestState(t)
user := User{
Id: 1,
Username: "quota-race-user",
Password: "password",
DisplayName: "before",
Status: common.UserStatusEnabled,
Quota: 1000,
UsedQuota: 20,
RequestCount: 3,
}
require.NoError(t, DB.Create(&user).Error)
staleUser, err := GetUserById(user.Id, true)
require.NoError(t, err)
require.NoError(t, DB.Model(&User{}).Where("id = ?", user.Id).Updates(map[string]interface{}{
"quota": gorm.Expr("quota - ?", 400),
"used_quota": gorm.Expr("used_quota + ?", 400),
"request_count": gorm.Expr("request_count + ?", 1),
}).Error)
staleUser.DisplayName = "after"
require.NoError(t, staleUser.Update(false))
var got User
require.NoError(t, DB.First(&got, user.Id).Error)
assert.Equal(t, "after", got.DisplayName)
assert.Equal(t, 600, got.Quota)
assert.Equal(t, 420, got.UsedQuota)
assert.Equal(t, 4, got.RequestCount)
}
func TestUpdateUserSettingOnlyUpdatesSetting(t *testing.T) {
setupUserUpdateTestState(t)
user := User{
Id: 2,
Username: "setting-user",
Password: "password",
Status: common.UserStatusEnabled,
Quota: 1000,
UsedQuota: 20,
RequestCount: 3,
}
require.NoError(t, DB.Create(&user).Error)
require.NoError(t, DB.Model(&User{}).Where("id = ?", user.Id).Updates(map[string]interface{}{
"quota": gorm.Expr("quota - ?", 250),
"used_quota": gorm.Expr("used_quota + ?", 250),
"request_count": gorm.Expr("request_count + ?", 1),
}).Error)
require.NoError(t, UpdateUserSetting(user.Id, dto.UserSetting{Language: "zh"}))
var got User
require.NoError(t, DB.First(&got, user.Id).Error)
assert.Equal(t, 750, got.Quota)
assert.Equal(t, 270, got.UsedQuota)
assert.Equal(t, 4, got.RequestCount)
assert.Equal(t, "zh", got.GetSetting().Language)
}
...@@ -83,7 +83,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -83,7 +83,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.GET("/self/groups", controller.GetUserGroups) selfRoute.GET("/self/groups", controller.GetUserGroups)
selfRoute.GET("/self", controller.GetSelf) selfRoute.GET("/self", controller.GetSelf)
selfRoute.GET("/models", controller.GetUserModels) selfRoute.GET("/models", controller.GetUserModels)
selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.PUT("/self", middleware.CriticalRateLimit(), controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf) selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/passkey", controller.PasskeyStatus) selfRoute.GET("/passkey", controller.PasskeyStatus)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment