Files
go-admin/cmd/migrate/migration/version/1786700001000_demo_menu.go
zhangwenjian 2628ab8e3e fix🐛: a seeded menu overflowed its column and stopped the migration run
sort is gorm:"size:4", which MySQL builds as a tinyint holding -128..127.
The demo menu seeded Sort: 900, so on MySQL the run stopped at
1786700001000 with Error 1264, and every migration after it - including
the soft-delete conversion - never ran.

deleted_at therefore stayed NULL while the code queries deleted_at = 0,
and the login returned 'incorrect Username or Password' on a database
whose password hash was correct all along.

sqlite ignores the declared width, so a fresh install there passed and
the fault only appeared on MySQL.
2026-08-27 12:12:00 +08:00

139 lines
5.9 KiB
Go

package version
import (
"runtime"
"strconv"
"gorm.io/gorm"
"go-admin/cmd/migrate/migration"
"go-admin/cmd/migrate/migration/models"
common "go-admin/common/models"
)
// 为 app/demo 模块写入菜单、接口与权限种子数据。
//
// 一个业务模块要在界面上可用,需要四类数据:
//
// 1. sys_api —— 后端路由的登记,Casbin 据此判定权限
// 2. sys_menu —— 侧边栏菜单(目录 M / 菜单 C / 按钮 F)
// 3. sys_menu_api_rule —— 菜单与接口的多对多关联,角色保存时据此生成策略
// 4. casbin_rule —— 实际生效的权限策略
//
// 注意 casbin_rule 才是 adapter 实际使用的表;models.CasbinRule 对应的
// sys_casbin_rule 是历史遗留,其 7 列 size:512 唯一索引在 MySQL 下会超出
// 索引长度限制,不要将其纳入 AutoMigrate。
func init() {
_, fileName, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(fileName), _1786700001000DemoMenu)
}
const (
demoMenuId = 9000 // 目录:示例模块
demoProductId = 9001 // 菜单:商品管理
demoApiBaseId = 9000 // sys_api 起始 id
adminRoleKey = "admin"
)
func _1786700001000DemoMenu(db *gorm.DB, version string) error {
return db.Transaction(func(tx *gorm.DB) error {
// ---- 1. 接口登记 ----
apis := []models.SysApi{
{Id: demoApiBaseId + 1, Handle: "go-admin/common/actions.IndexAction.func1", Title: "示例商品列表", Path: "/api/v1/demo-product", Type: "SYS", Action: "GET"},
{Id: demoApiBaseId + 2, Handle: "go-admin/common/actions.ViewAction.func1", Title: "示例商品详情", Path: "/api/v1/demo-product/:id", Type: "SYS", Action: "GET"},
{Id: demoApiBaseId + 3, Handle: "go-admin/common/actions.CreateAction.func1", Title: "示例商品新增", Path: "/api/v1/demo-product", Type: "SYS", Action: "POST"},
{Id: demoApiBaseId + 4, Handle: "go-admin/common/actions.UpdateAction.func1", Title: "示例商品修改", Path: "/api/v1/demo-product/:id", Type: "SYS", Action: "PUT"},
{Id: demoApiBaseId + 5, Handle: "go-admin/common/actions.DeleteAction.func1", Title: "示例商品删除", Path: "/api/v1/demo-product", Type: "SYS", Action: "DELETE"},
}
for i := range apis {
if err := upsert(tx, &models.SysApi{}, "id = ?", apis[i].Id, &apis[i]); err != nil {
return err
}
}
// ---- 2. 菜单 ----
// 目录本身不对应页面,component 固定为 Layout
dir := models.SysMenu{
MenuId: demoMenuId, MenuName: "Demo", Title: "示例模块", Icon: "example",
Path: "/demo", Paths: "/0/9000", MenuType: "M", ParentId: 0,
// sort is `gorm:"size:4"`, which MySQL builds as a tinyint - anything
// over 127 is rejected outright. The seeded menus run to 100, so 110
// still puts this last.
Component: "Layout", Sort: 110, Visible: "0", IsFrame: "1",
}
if err := upsert(tx, &models.SysMenu{}, "menu_id = ?", dir.MenuId, &dir); err != nil {
return err
}
// 菜单的 menu_name 需与前端组件 name 一致,否则 keep-alive 缓存无法命中
page := models.SysMenu{
MenuId: demoProductId, MenuName: "DemoProduct", Title: "商品管理", Icon: "documentation",
Path: "/demo/product", Paths: "/0/9000/9001", MenuType: "C", ParentId: demoMenuId,
Component: "/demo/product/index", Sort: 1, Visible: "0", IsFrame: "1",
SysApi: apis,
}
if err := upsert(tx, &models.SysMenu{}, "menu_id = ?", page.MenuId, &page); err != nil {
return err
}
// 按钮级权限,permission 需与前端 v-permisaction 中的标识一致
buttons := []models.SysMenu{
{MenuId: 9002, MenuName: "DemoProductAdd", Title: "新增", MenuType: "F", ParentId: demoProductId, Permission: "demo:product:add", Sort: 1, Visible: "0", IsFrame: "1"},
{MenuId: 9003, MenuName: "DemoProductEdit", Title: "修改", MenuType: "F", ParentId: demoProductId, Permission: "demo:product:edit", Sort: 2, Visible: "0", IsFrame: "1"},
{MenuId: 9004, MenuName: "DemoProductDel", Title: "删除", MenuType: "F", ParentId: demoProductId, Permission: "demo:product:delete", Sort: 3, Visible: "0", IsFrame: "1"},
}
for i := range buttons {
buttons[i].Paths = "/0/9000/9001/" + strconv.Itoa(buttons[i].MenuId)
if err := upsert(tx, &models.SysMenu{}, "menu_id = ?", buttons[i].MenuId, &buttons[i]); err != nil {
return err
}
}
// ---- 3. 授权给 admin 角色 ----
var role models.SysRole
err := tx.Where("role_key = ?", adminRoleKey).First(&role).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 尚未初始化角色数据时跳过授权,不阻断迁移
return tx.Create(&common.Migration{Version: version}).Error
}
return err
}
for _, id := range []int{demoMenuId, demoProductId, 9002, 9003, 9004} {
if err = tx.Exec(
"INSERT INTO sys_role_menu (role_id, menu_id) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = ? AND menu_id = ?)",
role.RoleId, id, role.RoleId, id,
).Error; err != nil {
return err
}
}
// ---- 4. Casbin 策略 ----
// admin 角色在中间件中直接放行,此处仍写入策略以便复制该角色配置时可继承
for _, a := range apis {
if err = tx.Exec(
"INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) SELECT 'p', ?, ?, ?, '', '', '' WHERE NOT EXISTS (SELECT 1 FROM casbin_rule WHERE ptype='p' AND v0=? AND v1=? AND v2=?)",
role.RoleKey, a.Path, a.Action, role.RoleKey, a.Path, a.Action,
).Error; err != nil {
return err
}
}
return tx.Create(&common.Migration{Version: version}).Error
})
}
// upsert 存在则更新、不存在则插入。迁移可能在已有数据的库上运行,
// 直接 Create 会因主键冲突失败。
func upsert(tx *gorm.DB, model interface{}, where string, id int, value interface{}) error {
var count int64
if err := tx.Model(model).Where(where, id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return tx.Model(model).Where(where, id).Updates(value).Error
}
return tx.Create(value).Error
}