From 88bab510569a1927aa2e64087d448399aea66e33 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 22 Aug 2026 11:11:03 +0800 Subject: [PATCH] =?UTF-8?q?fix=F0=9F=90=9B:=20stop=20hand-writing=20the=20?= =?UTF-8?q?soft-delete=20condition,=20and=20check=20the=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things in front of the unique-index work, both safe on their own. getSysMenuByRoleName carried "deleted_at is null" in its where clause. GORM adds that condition itself for a model with a DeletedAt field, so it was a duplicate — and one phrased as a column being null, which stops being true the moment the column stops being nullable. A schema that moves to a non-null delete marker would have turned this query into one that matches nothing, silently, for admin users only. SysDictType.Insert dropped the error from its duplicate check: a query that failed left the count at zero and the insert went ahead as though the name were free. The test pins what the removed clause was there for. Its counter-proof is Unscoped rather than deleting the field — taking ModelTime off the model fails to compile, which proves nothing. --- app/admin/service/sys_dict_type.go | 7 ++- app/admin/service/sys_menu.go | 6 ++- app/admin/service/sys_menu_softdelete_test.go | 49 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 app/admin/service/sys_menu_softdelete_test.go diff --git a/app/admin/service/sys_dict_type.go b/app/admin/service/sys_dict_type.go index bc45fe24..9899cc89 100644 --- a/app/admin/service/sys_dict_type.go +++ b/app/admin/service/sys_dict_type.go @@ -59,7 +59,12 @@ func (e *SysDictType) Insert(c *dto.SysDictTypeInsertReq) error { var data models.SysDictType c.Generate(&data) var count int64 - e.Orm.Model(&data).Where("dict_type = ?", data.DictType).Count(&count) + // The error was dropped, so a query that failed left count at zero and the + // insert went ahead as though the name were free. + if err = e.Orm.Model(&data).Where("dict_type = ?", data.DictType).Count(&count).Error; err != nil { + e.Log.Errorf("db error: %s", err) + return err + } if count > 0 { return fmt.Errorf("当前字典类型[%s]已经存在!", data.DictType) } diff --git a/app/admin/service/sys_menu.go b/app/admin/service/sys_menu.go index 7f2b48fa..9581a42b 100644 --- a/app/admin/service/sys_menu.go +++ b/app/admin/service/sys_menu.go @@ -395,7 +395,11 @@ func (e *SysMenu) getByRoleName(roleName string) ([]models.SysMenu, error) { data := make([]models.SysMenu, 0) if roleName == "admin" { - err = e.Orm.Where(" menu_type in ('M','C') and deleted_at is null"). + // The soft-delete condition is GORM's to add: it appends one for the + // model's DeletedAt field on every query. Writing it by hand duplicates + // that and hard-codes what "deleted" looks like — a column that stops + // being nullable turns this clause into one that matches nothing. + err = e.Orm.Where("menu_type in ('M','C')"). Order("sort"). Find(&data). Error diff --git a/app/admin/service/sys_menu_softdelete_test.go b/app/admin/service/sys_menu_softdelete_test.go new file mode 100644 index 00000000..d4a7cd6f --- /dev/null +++ b/app/admin/service/sys_menu_softdelete_test.go @@ -0,0 +1,49 @@ +package service + +import ( + "testing" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" + + "go-admin/app/admin/models" +) + +// The admin branch of getSysMenuByRoleName carried "deleted_at is null" in its +// where clause. GORM adds that condition itself for a model with a DeletedAt +// field, so the clause was a duplicate — and one written in terms of a column +// being null, which stops being true the moment the column stops being +// nullable. This pins the behaviour the clause was there for. +func TestSoftDeletedMenusAreNotReturned(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open: %v", err) + } + if err := db.AutoMigrate(&models.SysMenu{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + live := models.SysMenu{MenuName: "live", MenuType: "M"} + gone := models.SysMenu{MenuName: "gone", MenuType: "M"} + if err := db.Create(&live).Error; err != nil { + t.Fatalf("create: %v", err) + } + if err := db.Create(&gone).Error; err != nil { + t.Fatalf("create: %v", err) + } + if err := db.Delete(&gone).Error; err != nil { + t.Fatalf("delete: %v", err) + } + + var got []models.SysMenu + if err := db.Where("menu_type in ('M','C')").Order("sort").Find(&got).Error; err != nil { + t.Fatalf("find: %v", err) + } + + if len(got) != 1 { + t.Fatalf("got %d rows, want 1", len(got)) + } + if got[0].MenuName != "live" { + t.Errorf("got %q, want the row that was not deleted", got[0].MenuName) + } +}