You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

84 KiB

Menu Management + Role Management + Organization Management Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add dynamic menu management (assigned by role), custom role management, and tree-structured organization management with org-scoped roles to the BASE admin panel.

Architecture: Five new database tables (menus, roles, role_menus, organizations, user_organizations) with GORM entities/models. go-zero API definitions generate handlers; business logic follows existing Handler→Logic→Model pattern. JWT Claims extended with currentOrgId. Frontend sidebar becomes dynamic, driven by GET /menus/current. AuthContext gains org switching and menu state.

Tech Stack: Go + go-zero + GORM + Casbin (backend), React 19 + TypeScript + Tailwind CSS (frontend), MySQL


Task 1: Menu Entity + Model

Files:

  • Create: backend/model/menu_entity.go
  • Create: backend/model/menu_model.go

Step 1: Create Menu entity

Create backend/model/menu_entity.go:

package model

import "time"

// Menu 菜单模型
type Menu struct {
	Id        int64     `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
	ParentId  int64     `gorm:"column:parent_id;default:0;index" json:"parentId"`
	Name      string    `gorm:"column:name;type:varchar(50);not null" json:"name"`
	Path      string    `gorm:"column:path;type:varchar(200);default:''" json:"path"`
	Icon      string    `gorm:"column:icon;type:varchar(50);default:''" json:"icon"`
	Component string    `gorm:"column:component;type:varchar(200);default:''" json:"component"`
	Type      string    `gorm:"column:type;type:varchar(20);default:'config'" json:"type"` // default or config
	SortOrder int       `gorm:"column:sort_order;default:0" json:"sortOrder"`
	Visible   bool      `gorm:"column:visible;default:true" json:"visible"`
	Status    int       `gorm:"column:status;default:1" json:"status"`
	CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
	UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}

func (Menu) TableName() string {
	return "menu"
}

Step 2: Create Menu model (data access)

Create backend/model/menu_model.go:

package model

import (
	"context"
	"errors"

	"gorm.io/gorm"
)

// MenuInsert 插入菜单
func MenuInsert(ctx context.Context, db *gorm.DB, menu *Menu) (int64, error) {
	result := db.WithContext(ctx).Create(menu)
	if result.Error != nil {
		return 0, result.Error
	}
	return menu.Id, nil
}

// MenuFindOne 根据ID查询菜单
func MenuFindOne(ctx context.Context, db *gorm.DB, id int64) (*Menu, error) {
	var menu Menu
	result := db.WithContext(ctx).First(&menu, id)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, ErrNotFound
		}
		return nil, result.Error
	}
	return &menu, nil
}

// MenuFindAll 查询所有启用的菜单(排序)
func MenuFindAll(ctx context.Context, db *gorm.DB) ([]Menu, error) {
	var menus []Menu
	err := db.WithContext(ctx).Where("status = 1").Order("sort_order ASC, id ASC").Find(&menus).Error
	return menus, err
}

// MenuFindByIds 根据ID列表查询菜单
func MenuFindByIds(ctx context.Context, db *gorm.DB, ids []int64) ([]Menu, error) {
	var menus []Menu
	if len(ids) == 0 {
		return menus, nil
	}
	err := db.WithContext(ctx).Where("id IN ? AND status = 1", ids).Order("sort_order ASC, id ASC").Find(&menus).Error
	return menus, err
}

// MenuUpdate 更新菜单
func MenuUpdate(ctx context.Context, db *gorm.DB, menu *Menu) error {
	return db.WithContext(ctx).Save(menu).Error
}

// MenuDelete 删除菜单
func MenuDelete(ctx context.Context, db *gorm.DB, id int64) error {
	return db.WithContext(ctx).Delete(&Menu{}, id).Error
}

// MenuHasChildren 检查菜单是否有子菜单
func MenuHasChildren(ctx context.Context, db *gorm.DB, parentId int64) (bool, error) {
	var count int64
	err := db.WithContext(ctx).Model(&Menu{}).Where("parent_id = ?", parentId).Count(&count).Error
	return count > 0, err
}

Step 3: Verify build

Run: cd D:\APPS\base\backend && go build ./model/... Expected: BUILD SUCCESS

Step 4: Commit

git add backend/model/menu_entity.go backend/model/menu_model.go
git commit -m "feat: add Menu entity and model"

Task 2: Role Entity + Model

Files:

  • Create: backend/model/role_entity.go
  • Create: backend/model/role_model.go

Step 1: Create Role entity

Create backend/model/role_entity.go:

package model

import "time"

// Role 角色模型
type Role struct {
	Id          int64     `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
	Name        string    `gorm:"column:name;type:varchar(50);not null" json:"name"`
	Code        string    `gorm:"column:code;type:varchar(50);uniqueIndex;not null" json:"code"`
	Description string    `gorm:"column:description;type:varchar(255);default:''" json:"description"`
	IsSystem    bool      `gorm:"column:is_system;default:false" json:"isSystem"`
	SortOrder   int       `gorm:"column:sort_order;default:0" json:"sortOrder"`
	Status      int       `gorm:"column:status;default:1" json:"status"`
	CreatedAt   time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
	UpdatedAt   time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}

func (Role) TableName() string {
	return "role"
}

Step 2: Create Role model

Create backend/model/role_model.go:

package model

import (
	"context"
	"errors"

	"gorm.io/gorm"
)

// RoleInsert 插入角色
func RoleInsert(ctx context.Context, db *gorm.DB, role *Role) (int64, error) {
	result := db.WithContext(ctx).Create(role)
	if result.Error != nil {
		return 0, result.Error
	}
	return role.Id, nil
}

// RoleFindOne 根据ID查询角色
func RoleFindOne(ctx context.Context, db *gorm.DB, id int64) (*Role, error) {
	var role Role
	result := db.WithContext(ctx).First(&role, id)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, ErrNotFound
		}
		return nil, result.Error
	}
	return &role, nil
}

// RoleFindOneByCode 根据编码查询角色
func RoleFindOneByCode(ctx context.Context, db *gorm.DB, code string) (*Role, error) {
	var role Role
	result := db.WithContext(ctx).Where("code = ?", code).First(&role)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, ErrNotFound
		}
		return nil, result.Error
	}
	return &role, nil
}

// RoleFindAll 查询所有启用的角色
func RoleFindAll(ctx context.Context, db *gorm.DB) ([]Role, error) {
	var roles []Role
	err := db.WithContext(ctx).Where("status = 1").Order("sort_order ASC, id ASC").Find(&roles).Error
	return roles, err
}

// RoleUpdate 更新角色
func RoleUpdate(ctx context.Context, db *gorm.DB, role *Role) error {
	return db.WithContext(ctx).Save(role).Error
}

// RoleDelete 删除角色
func RoleDelete(ctx context.Context, db *gorm.DB, id int64) error {
	return db.WithContext(ctx).Delete(&Role{}, id).Error
}

Step 3: Verify build

Run: cd D:\APPS\base\backend && go build ./model/... Expected: BUILD SUCCESS

Step 4: Commit

git add backend/model/role_entity.go backend/model/role_model.go
git commit -m "feat: add Role entity and model"

Task 3: RoleMenu + Organization + UserOrganization Models

Files:

  • Create: backend/model/role_menu_model.go
  • Create: backend/model/organization_entity.go
  • Create: backend/model/organization_model.go
  • Create: backend/model/user_organization_entity.go
  • Create: backend/model/user_organization_model.go

Step 1: Create RoleMenu join table model

Create backend/model/role_menu_model.go:

package model

import (
	"context"

	"gorm.io/gorm"
)

// RoleMenu 角色-菜单关联
type RoleMenu struct {
	Id     int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
	RoleId int64 `gorm:"column:role_id;index" json:"roleId"`
	MenuId int64 `gorm:"column:menu_id;index" json:"menuId"`
}

func (RoleMenu) TableName() string {
	return "role_menu"
}

// RoleMenuFindByRoleId 获取角色的菜单ID列表
func RoleMenuFindByRoleId(ctx context.Context, db *gorm.DB, roleId int64) ([]int64, error) {
	var menuIds []int64
	err := db.WithContext(ctx).Model(&RoleMenu{}).Where("role_id = ?", roleId).Pluck("menu_id", &menuIds).Error
	return menuIds, err
}

// RoleMenuFindByRoleIds 获取多个角色的菜单ID列表(去重)
func RoleMenuFindByRoleIds(ctx context.Context, db *gorm.DB, roleIds []int64) ([]int64, error) {
	var menuIds []int64
	if len(roleIds) == 0 {
		return menuIds, nil
	}
	err := db.WithContext(ctx).Model(&RoleMenu{}).Where("role_id IN ?", roleIds).Distinct("menu_id").Pluck("menu_id", &menuIds).Error
	return menuIds, err
}

// RoleMenuSetForRole 全量设置角色的菜单(先删后插)
func RoleMenuSetForRole(ctx context.Context, db *gorm.DB, roleId int64, menuIds []int64) error {
	return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// 删除旧关联
		if err := tx.Where("role_id = ?", roleId).Delete(&RoleMenu{}).Error; err != nil {
			return err
		}
		// 插入新关联
		if len(menuIds) == 0 {
			return nil
		}
		records := make([]RoleMenu, 0, len(menuIds))
		for _, menuId := range menuIds {
			records = append(records, RoleMenu{RoleId: roleId, MenuId: menuId})
		}
		return tx.Create(&records).Error
	})
}

// RoleMenuDeleteByRoleId 删除角色的所有菜单关联
func RoleMenuDeleteByRoleId(ctx context.Context, db *gorm.DB, roleId int64) error {
	return db.WithContext(ctx).Where("role_id = ?", roleId).Delete(&RoleMenu{}).Error
}

Step 2: Create Organization entity

Create backend/model/organization_entity.go:

package model

import "time"

// Organization 机构模型(树形)
type Organization struct {
	Id        int64     `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
	ParentId  int64     `gorm:"column:parent_id;default:0;index" json:"parentId"`
	Name      string    `gorm:"column:name;type:varchar(100);not null" json:"name"`
	Code      string    `gorm:"column:code;type:varchar(50);uniqueIndex" json:"code"`
	Leader    string    `gorm:"column:leader;type:varchar(50);default:''" json:"leader"`
	Phone     string    `gorm:"column:phone;type:varchar(20);default:''" json:"phone"`
	Email     string    `gorm:"column:email;type:varchar(100);default:''" json:"email"`
	SortOrder int       `gorm:"column:sort_order;default:0" json:"sortOrder"`
	Status    int       `gorm:"column:status;default:1" json:"status"`
	CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
	UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}

func (Organization) TableName() string {
	return "organization"
}

Step 3: Create Organization model

Create backend/model/organization_model.go:

package model

import (
	"context"
	"errors"

	"gorm.io/gorm"
)

// OrgInsert 插入机构
func OrgInsert(ctx context.Context, db *gorm.DB, org *Organization) (int64, error) {
	result := db.WithContext(ctx).Create(org)
	if result.Error != nil {
		return 0, result.Error
	}
	return org.Id, nil
}

// OrgFindOne 根据ID查询机构
func OrgFindOne(ctx context.Context, db *gorm.DB, id int64) (*Organization, error) {
	var org Organization
	result := db.WithContext(ctx).First(&org, id)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, ErrNotFound
		}
		return nil, result.Error
	}
	return &org, nil
}

// OrgFindAll 查询所有启用的机构
func OrgFindAll(ctx context.Context, db *gorm.DB) ([]Organization, error) {
	var orgs []Organization
	err := db.WithContext(ctx).Where("status = 1").Order("sort_order ASC, id ASC").Find(&orgs).Error
	return orgs, err
}

// OrgUpdate 更新机构
func OrgUpdate(ctx context.Context, db *gorm.DB, org *Organization) error {
	return db.WithContext(ctx).Save(org).Error
}

// OrgDelete 删除机构
func OrgDelete(ctx context.Context, db *gorm.DB, id int64) error {
	return db.WithContext(ctx).Delete(&Organization{}, id).Error
}

// OrgHasChildren 检查是否有子机构
func OrgHasChildren(ctx context.Context, db *gorm.DB, parentId int64) (bool, error) {
	var count int64
	err := db.WithContext(ctx).Model(&Organization{}).Where("parent_id = ? AND status = 1", parentId).Count(&count).Error
	return count > 0, err
}

// OrgFindOneByCode 根据编码查询机构
func OrgFindOneByCode(ctx context.Context, db *gorm.DB, code string) (*Organization, error) {
	var org Organization
	result := db.WithContext(ctx).Where("code = ?", code).First(&org)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, ErrNotFound
		}
		return nil, result.Error
	}
	return &org, nil
}

Step 4: Create UserOrganization entity

Create backend/model/user_organization_entity.go:

package model

import "time"

// UserOrganization 用户-机构-角色关联
type UserOrganization struct {
	Id        int64     `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
	UserId    int64     `gorm:"column:user_id;index" json:"userId"`
	OrgId     int64     `gorm:"column:org_id;index" json:"orgId"`
	RoleId    int64     `gorm:"column:role_id" json:"roleId"`
	CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
}

func (UserOrganization) TableName() string {
	return "user_organization"
}

Step 5: Create UserOrganization model

Create backend/model/user_organization_model.go:

package model

import (
	"context"
	"errors"

	"gorm.io/gorm"
)

// UserOrgInsert 添加用户-机构关联
func UserOrgInsert(ctx context.Context, db *gorm.DB, uo *UserOrganization) (int64, error) {
	result := db.WithContext(ctx).Create(uo)
	if result.Error != nil {
		return 0, result.Error
	}
	return uo.Id, nil
}

// UserOrgFindOne 查询单条关联
func UserOrgFindOne(ctx context.Context, db *gorm.DB, userId, orgId int64) (*UserOrganization, error) {
	var uo UserOrganization
	result := db.WithContext(ctx).Where("user_id = ? AND org_id = ?", userId, orgId).First(&uo)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, ErrNotFound
		}
		return nil, result.Error
	}
	return &uo, nil
}

// UserOrgFindByUserId 获取用户的所有机构关联
func UserOrgFindByUserId(ctx context.Context, db *gorm.DB, userId int64) ([]UserOrganization, error) {
	var list []UserOrganization
	err := db.WithContext(ctx).Where("user_id = ?", userId).Find(&list).Error
	return list, err
}

// UserOrgFindByOrgId 获取机构的所有成员关联
func UserOrgFindByOrgId(ctx context.Context, db *gorm.DB, orgId int64) ([]UserOrganization, error) {
	var list []UserOrganization
	err := db.WithContext(ctx).Where("org_id = ?", orgId).Find(&list).Error
	return list, err
}

// UserOrgUpdate 更新关联(如改角色)
func UserOrgUpdate(ctx context.Context, db *gorm.DB, uo *UserOrganization) error {
	return db.WithContext(ctx).Save(uo).Error
}

// UserOrgDelete 删除关联
func UserOrgDelete(ctx context.Context, db *gorm.DB, userId, orgId int64) error {
	return db.WithContext(ctx).Where("user_id = ? AND org_id = ?", userId, orgId).Delete(&UserOrganization{}).Error
}

// UserOrgDeleteByOrgId 删除机构的所有成员关联
func UserOrgDeleteByOrgId(ctx context.Context, db *gorm.DB, orgId int64) error {
	return db.WithContext(ctx).Where("org_id = ?", orgId).Delete(&UserOrganization{}).Error
}

// UserOrgCountByOrgId 统计机构成员数
func UserOrgCountByOrgId(ctx context.Context, db *gorm.DB, orgId int64) (int64, error) {
	var count int64
	err := db.WithContext(ctx).Model(&UserOrganization{}).Where("org_id = ?", orgId).Count(&count).Error
	return count, err
}

Step 6: Verify build

Run: cd D:\APPS\base\backend && go build ./model/... Expected: BUILD SUCCESS

Step 7: Commit

git add backend/model/role_menu_model.go backend/model/organization_entity.go backend/model/organization_model.go backend/model/user_organization_entity.go backend/model/user_organization_model.go
git commit -m "feat: add RoleMenu, Organization, UserOrganization models"

Task 4: User Entity + JWT Claims Modifications

Files:

  • Modify: backend/model/user_entity.go
  • Modify: backend/internal/util/jwt/jwt.go
  • Modify: backend/internal/middleware/authmiddleware.go

Step 1: Add CurrentOrgId to User entity

In backend/model/user_entity.go, add after the Remark field:

CurrentOrgId int64  `gorm:"column:current_org_id;default:0" json:"currentOrgId"`

Step 2: Add CurrentOrgId to JWT Claims

In backend/internal/util/jwt/jwt.go, update the Claims struct and GenerateToken:

type Claims struct {
	UserID       int64  `json:"userId"`
	Username     string `json:"username"`
	Role         string `json:"role"`
	CurrentOrgId int64  `json:"currentOrgId"`
	jwt.RegisteredClaims
}

// GenerateToken 生成 JWT Token
func GenerateToken(userId int64, username, role string, currentOrgId int64) (string, error) {
	claims := Claims{
		UserID:       userId,
		Username:     username,
		Role:         role,
		CurrentOrgId: currentOrgId,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpireTime)),
			Issuer:    "base-api",
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(SigningKey)
}

Step 3: Update Auth middleware to inject currentOrgId

In backend/internal/middleware/authmiddleware.go, add after ctx = context.WithValue(ctx, "role", claims.Role):

ctx = context.WithValue(ctx, "currentOrgId", claims.CurrentOrgId)

Step 4: Update all GenerateToken call sites

There are 4 call sites that need the new currentOrgId parameter:

  1. backend/internal/logic/auth/loginlogic.go:55:
token, err := jwt.GenerateToken(user.Id, user.Username, user.Role, user.CurrentOrgId)
  1. backend/internal/logic/auth/registerlogic.go — register doesn't generate token (it returns UserInfo), so no change needed.

  2. backend/internal/logic/auth/refreshtokenlogic.go:55:

newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Role, user.CurrentOrgId)
  1. backend/internal/logic/auth/ssologic.go:177 (the HandleCallback method):
token, err := jwt.GenerateToken(localUser.Id, localUser.Username, localUser.Role, localUser.CurrentOrgId)

Step 5: Verify build

Run: cd D:\APPS\base\backend && go build ./... Expected: BUILD SUCCESS

Step 6: Commit

git add backend/model/user_entity.go backend/internal/util/jwt/jwt.go backend/internal/middleware/authmiddleware.go backend/internal/logic/auth/loginlogic.go backend/internal/logic/auth/refreshtokenlogic.go backend/internal/logic/auth/ssologic.go
git commit -m "feat: add currentOrgId to User entity, JWT Claims, and auth middleware"

Task 5: API Definitions (menu.api + role.api + organization.api)

Files:

  • Create: backend/api/menu.api
  • Create: backend/api/role.api
  • Create: backend/api/organization.api
  • Modify: backend/base.api

Step 1: Create menu.api

Create backend/api/menu.api:

syntax = "v1"

// ========== 菜单管理类型定义 ==========
type (
	// 菜单信息
	MenuItem {
		Id        int64      `json:"id"`
		ParentId  int64      `json:"parentId"`
		Name      string     `json:"name"`
		Path      string     `json:"path"`
		Icon      string     `json:"icon"`
		Component string     `json:"component"`
		Type      string     `json:"type"`
		SortOrder int        `json:"sortOrder"`
		Visible   bool       `json:"visible"`
		Status    int        `json:"status"`
		Children  []MenuItem `json:"children"`
		CreatedAt string     `json:"createdAt"`
		UpdatedAt string     `json:"updatedAt"`
	}

	// 菜单列表响应
	MenuListResponse {
		List []MenuItem `json:"list"`
	}

	// 创建菜单请求
	CreateMenuRequest {
		ParentId  int64  `json:"parentId,optional"`
		Name      string `json:"name" validate:"required"`
		Path      string `json:"path,optional"`
		Icon      string `json:"icon,optional"`
		Component string `json:"component,optional"`
		Type      string `json:"type,optional"`
		SortOrder int    `json:"sortOrder,optional"`
		Visible   *bool  `json:"visible,optional"`
	}

	// 更新菜单请求
	UpdateMenuRequest {
		Id        int64  `path:"id"`
		ParentId  *int64 `json:"parentId,optional"`
		Name      string `json:"name,optional"`
		Path      string `json:"path,optional"`
		Icon      string `json:"icon,optional"`
		Component string `json:"component,optional"`
		Type      string `json:"type,optional"`
		SortOrder *int   `json:"sortOrder,optional"`
		Visible   *bool  `json:"visible,optional"`
		Status    *int   `json:"status,optional"`
	}

	// 删除菜单请求
	DeleteMenuRequest {
		Id int64 `path:"id"`
	}
)

Step 2: Create role.api

Create backend/api/role.api:

syntax = "v1"

// ========== 角色管理类型定义 ==========
type (
	// 角色信息
	RoleInfo {
		Id          int64  `json:"id"`
		Name        string `json:"name"`
		Code        string `json:"code"`
		Description string `json:"description"`
		IsSystem    bool   `json:"isSystem"`
		SortOrder   int    `json:"sortOrder"`
		Status      int    `json:"status"`
		CreatedAt   string `json:"createdAt"`
		UpdatedAt   string `json:"updatedAt"`
	}

	// 角色列表响应
	RoleListResponse {
		List []RoleInfo `json:"list"`
	}

	// 创建角色请求
	CreateRoleRequest {
		Name        string `json:"name" validate:"required"`
		Code        string `json:"code" validate:"required"`
		Description string `json:"description,optional"`
		SortOrder   int    `json:"sortOrder,optional"`
	}

	// 更新角色请求
	UpdateRoleRequest {
		Id          int64  `path:"id"`
		Name        string `json:"name,optional"`
		Description string `json:"description,optional"`
		SortOrder   *int   `json:"sortOrder,optional"`
		Status      *int   `json:"status,optional"`
	}

	// 删除角色请求
	DeleteRoleRequest {
		Id int64 `path:"id"`
	}

	// 获取角色菜单请求
	GetRoleMenusRequest {
		Id int64 `path:"id"`
	}

	// 角色菜单响应
	RoleMenusResponse {
		MenuIds []int64 `json:"menuIds"`
	}

	// 设置角色菜单请求
	SetRoleMenusRequest {
		Id      int64   `path:"id"`
		MenuIds []int64 `json:"menuIds"`
	}
)

Step 3: Create organization.api

Create backend/api/organization.api:

syntax = "v1"

// ========== 机构管理类型定义 ==========
type (
	// 机构信息
	OrgInfo {
		Id          int64     `json:"id"`
		ParentId    int64     `json:"parentId"`
		Name        string    `json:"name"`
		Code        string    `json:"code"`
		Leader      string    `json:"leader"`
		Phone       string    `json:"phone"`
		Email       string    `json:"email"`
		SortOrder   int       `json:"sortOrder"`
		Status      int       `json:"status"`
		MemberCount int64     `json:"memberCount"`
		Children    []OrgInfo `json:"children"`
		CreatedAt   string    `json:"createdAt"`
		UpdatedAt   string    `json:"updatedAt"`
	}

	// 机构列表响应
	OrgListResponse {
		List []OrgInfo `json:"list"`
	}

	// 创建机构请求
	CreateOrgRequest {
		ParentId  int64  `json:"parentId,optional"`
		Name      string `json:"name" validate:"required"`
		Code      string `json:"code" validate:"required"`
		Leader    string `json:"leader,optional"`
		Phone     string `json:"phone,optional"`
		Email     string `json:"email,optional"`
		SortOrder int    `json:"sortOrder,optional"`
	}

	// 更新机构请求
	UpdateOrgRequest {
		Id        int64  `path:"id"`
		ParentId  *int64 `json:"parentId,optional"`
		Name      string `json:"name,optional"`
		Code      string `json:"code,optional"`
		Leader    string `json:"leader,optional"`
		Phone     string `json:"phone,optional"`
		Email     string `json:"email,optional"`
		SortOrder *int   `json:"sortOrder,optional"`
		Status    *int   `json:"status,optional"`
	}

	// 删除机构请求
	DeleteOrgRequest {
		Id int64 `path:"id"`
	}

	// 机构成员信息
	OrgMember {
		UserId    int64  `json:"userId"`
		Username  string `json:"username"`
		Email     string `json:"email"`
		Phone     string `json:"phone"`
		RoleId    int64  `json:"roleId"`
		RoleName  string `json:"roleName"`
		RoleCode  string `json:"roleCode"`
		CreatedAt string `json:"createdAt"`
	}

	// 获取机构成员请求
	GetOrgMembersRequest {
		Id int64 `path:"id"`
	}

	// 机构成员列表响应
	OrgMembersResponse {
		List []OrgMember `json:"list"`
	}

	// 添加机构成员请求
	AddOrgMemberRequest {
		Id     int64 `path:"id"`
		UserId int64 `json:"userId" validate:"required"`
		RoleId int64 `json:"roleId" validate:"required"`
	}

	// 更新机构成员角色请求
	UpdateOrgMemberRequest {
		Id     int64 `path:"id"`
		UserId int64 `path:"userId"`
		RoleId int64 `json:"roleId" validate:"required"`
	}

	// 移除机构成员请求
	RemoveOrgMemberRequest {
		Id     int64 `path:"id"`
		UserId int64 `path:"userId"`
	}

	// 用户机构信息
	UserOrgInfo {
		OrgId    int64  `json:"orgId"`
		OrgName  string `json:"orgName"`
		RoleId   int64  `json:"roleId"`
		RoleName string `json:"roleName"`
		RoleCode string `json:"roleCode"`
	}

	// 用户机构列表响应
	UserOrgsResponse {
		List []UserOrgInfo `json:"list"`
	}

	// 切换当前机构请求
	SwitchOrgRequest {
		OrgId int64 `json:"orgId" validate:"required"`
	}

	// 切换机构响应(返回新 token)
	SwitchOrgResponse {
		Token string `json:"token"`
	}
)

Step 4: Update base.api — add imports and route definitions

In backend/base.api, add at the imports section (after import "api/file.api"):

import "api/menu.api"
import "api/role.api"
import "api/organization.api"

Then add these new service blocks at the end of the file (before the closing):

// ========== 菜单管理(当前用户菜单,需登录) ==========
@server (
	prefix:     /api/v1
	group:      menu
	middleware: Cors,Log,Auth
)
service base-api {
	@doc "获取当前用户可见菜单"
	@handler getCurrentMenus
	get /menus/current returns (MenuListResponse)
}

// ========== 菜单管理(管理端,需授权) ==========
@server (
	prefix:     /api/v1
	group:      menu
	middleware: Cors,Log,Auth,Authz
)
service base-api {
	@doc "获取全部菜单列表"
	@handler getMenuList
	get /menus returns (MenuListResponse)

	@doc "创建菜单"
	@handler createMenu
	post /menu (CreateMenuRequest) returns (MenuItem)

	@doc "更新菜单"
	@handler updateMenu
	put /menu/:id (UpdateMenuRequest) returns (MenuItem)

	@doc "删除菜单"
	@handler deleteMenu
	delete /menu/:id (DeleteMenuRequest) returns (Response)
}

// ========== 角色管理 ==========
@server (
	prefix:     /api/v1
	group:      role
	middleware: Cors,Log,Auth,Authz
)
service base-api {
	@doc "获取角色列表"
	@handler getRoleList
	get /roles returns (RoleListResponse)

	@doc "创建角色"
	@handler createRole
	post /role (CreateRoleRequest) returns (RoleInfo)

	@doc "更新角色"
	@handler updateRole
	put /role/:id (UpdateRoleRequest) returns (RoleInfo)

	@doc "删除角色"
	@handler deleteRole
	delete /role/:id (DeleteRoleRequest) returns (Response)

	@doc "获取角色菜单"
	@handler getRoleMenus
	get /role/:id/menus (GetRoleMenusRequest) returns (RoleMenusResponse)

	@doc "设置角色菜单"
	@handler setRoleMenus
	put /role/:id/menus (SetRoleMenusRequest) returns (Response)
}

// ========== 机构管理 ==========
@server (
	prefix:     /api/v1
	group:      organization
	middleware: Cors,Log,Auth,Authz
)
service base-api {
	@doc "获取机构列表"
	@handler getOrganizationList
	get /organizations returns (OrgListResponse)

	@doc "创建机构"
	@handler createOrganization
	post /organization (CreateOrgRequest) returns (OrgInfo)

	@doc "更新机构"
	@handler updateOrganization
	put /organization/:id (UpdateOrgRequest) returns (OrgInfo)

	@doc "删除机构"
	@handler deleteOrganization
	delete /organization/:id (DeleteOrgRequest) returns (Response)

	@doc "获取机构成员"
	@handler getOrgMembers
	get /organization/:id/members (GetOrgMembersRequest) returns (OrgMembersResponse)

	@doc "添加机构成员"
	@handler addOrgMember
	post /organization/:id/member (AddOrgMemberRequest) returns (Response)

	@doc "更新机构成员角色"
	@handler updateOrgMember
	put /organization/:id/member/:userId (UpdateOrgMemberRequest) returns (Response)

	@doc "移除机构成员"
	@handler removeOrgMember
	delete /organization/:id/member/:userId (RemoveOrgMemberRequest) returns (Response)
}

// ========== 用户机构上下文(需登录,Authz放行) ==========
@server (
	prefix:     /api/v1
	group:      profile
	middleware: Cors,Log,Auth,Authz
)
service base-api {
	@doc "获取我的机构列表"
	@handler getUserOrgs
	get /profile/orgs returns (UserOrgsResponse)

	@doc "切换当前机构"
	@handler switchOrg
	put /profile/current-org (SwitchOrgRequest) returns (SwitchOrgResponse)
}

Step 5: Commit

git add backend/api/menu.api backend/api/role.api backend/api/organization.api backend/base.api
git commit -m "feat: add API definitions for menu, role, and organization management"

Task 6: Run goctl Code Generation

Step 1: Run goctl to regenerate types and routes

Run: cd D:\APPS\base\backend && goctl api go -api base.api -dir .

This will:

  • Regenerate internal/types/types.go with new type definitions
  • Regenerate internal/handler/routes.go with new routes
  • Generate new handler files under internal/handler/menu/, internal/handler/role/, internal/handler/organization/
  • Generate new logic stubs under internal/logic/menu/, internal/logic/role/, internal/logic/organization/

Step 2: Fix any import paths in generated files

The generated files will use the module path from go.mod. Verify by checking: Run: cd D:\APPS\base\backend && go build ./...

If there are compilation errors related to duplicate profile handlers (since we added new profile endpoints), we may need to adjust. The getUserOrgs and switchOrg handlers will be generated into internal/handler/profile/ alongside existing profile handlers.

Step 3: Commit generated code

git add backend/internal/types/types.go backend/internal/handler/ backend/internal/logic/
git commit -m "feat: goctl generated handlers, types, and logic stubs for menu/role/org"

Task 7: ServiceContext — AutoMigrate + Seed Data

Files:

  • Modify: backend/internal/svc/servicecontext.go

Step 1: Add new models to AutoMigrate

In the NewServiceContext function, update the AutoMigrate call:

err = db.AutoMigrate(&model.User{}, &model.Profile{}, &model.File{}, &model.Menu{}, &model.Role{}, &model.RoleMenu{}, &model.Organization{}, &model.UserOrganization{})

Step 2: Add seed functions for roles and menus

Add these functions to servicecontext.go:

// seedRoles 种子角色数据(幂等)
func seedRoles(db *gorm.DB) {
	ctx := context.Background()
	systemRoles := []model.Role{
		{Name: "超级管理员", Code: model.RoleSuperAdmin, Description: "拥有所有权限", IsSystem: true, SortOrder: 1, Status: 1},
		{Name: "管理员", Code: model.RoleAdmin, Description: "管理用户和内容", IsSystem: true, SortOrder: 2, Status: 1},
		{Name: "普通用户", Code: model.RoleUser, Description: "基础功能访问", IsSystem: true, SortOrder: 3, Status: 1},
		{Name: "访客", Code: model.RoleGuest, Description: "只读访问", IsSystem: true, SortOrder: 4, Status: 1},
	}
	for _, role := range systemRoles {
		existing, err := model.RoleFindOneByCode(ctx, db, role.Code)
		if err == model.ErrNotFound {
			model.RoleInsert(ctx, db, &role)
			log.Printf("[Seed] Role created: %s", role.Code)
		} else if err == nil && !existing.IsSystem {
			existing.IsSystem = true
			model.RoleUpdate(ctx, db, existing)
		}
	}
}

// seedMenus 种子菜单数据(幂等)
func seedMenus(db *gorm.DB) {
	ctx := context.Background()
	menus := []model.Menu{
		{Name: "我的", Path: "/my", Icon: "User", Type: "default", SortOrder: 1, Visible: true, Status: 1},
		{Name: "仪表盘", Path: "/dashboard", Icon: "LayoutDashboard", Type: "config", SortOrder: 2, Visible: true, Status: 1},
		{Name: "用户管理", Path: "/users", Icon: "Users", Type: "config", SortOrder: 3, Visible: true, Status: 1},
		{Name: "文件管理", Path: "/files", Icon: "FolderOpen", Type: "config", SortOrder: 4, Visible: true, Status: 1},
		{Name: "角色管理", Path: "/roles", Icon: "Shield", Type: "config", SortOrder: 5, Visible: true, Status: 1},
		{Name: "菜单管理", Path: "/menus", Icon: "Menu", Type: "config", SortOrder: 6, Visible: true, Status: 1},
		{Name: "机构管理", Path: "/organizations", Icon: "Building2", Type: "config", SortOrder: 7, Visible: true, Status: 1},
		{Name: "设置", Path: "/settings", Icon: "Settings", Type: "default", SortOrder: 8, Visible: true, Status: 1},
	}

	for i := range menus {
		var existing model.Menu
		err := db.WithContext(ctx).Where("path = ?", menus[i].Path).First(&existing).Error
		if err != nil {
			model.MenuInsert(ctx, db, &menus[i])
			log.Printf("[Seed] Menu created: %s", menus[i].Name)
		}
	}
}

// seedRoleMenus 种子角色-菜单关联(幂等)
func seedRoleMenus(db *gorm.DB) {
	ctx := context.Background()

	// 获取所有菜单
	allMenus, _ := model.MenuFindAll(ctx, db)
	if len(allMenus) == 0 {
		return
	}

	// 获取 super_admin 角色
	superAdmin, err := model.RoleFindOneByCode(ctx, db, model.RoleSuperAdmin)
	if err != nil {
		return
	}

	// super_admin 绑定所有菜单
	existingMenuIds, _ := model.RoleMenuFindByRoleId(ctx, db, superAdmin.Id)
	if len(existingMenuIds) == 0 {
		allMenuIds := make([]int64, 0, len(allMenus))
		for _, m := range allMenus {
			allMenuIds = append(allMenuIds, m.Id)
		}
		model.RoleMenuSetForRole(ctx, db, superAdmin.Id, allMenuIds)
		log.Println("[Seed] super_admin bound to all menus")
	}

	// user 角色只绑定 default 类型菜单
	userRole, err := model.RoleFindOneByCode(ctx, db, model.RoleUser)
	if err != nil {
		return
	}
	existingMenuIds, _ = model.RoleMenuFindByRoleId(ctx, db, userRole.Id)
	if len(existingMenuIds) == 0 {
		defaultMenuIds := make([]int64, 0)
		for _, m := range allMenus {
			if m.Type == "default" {
				defaultMenuIds = append(defaultMenuIds, m.Id)
			}
		}
		model.RoleMenuSetForRole(ctx, db, userRole.Id, defaultMenuIds)
		log.Println("[Seed] user role bound to default menus")
	}

	// admin 角色绑定所有菜单(和 super_admin 一样,但不能管理菜单和角色)
	adminRole, err := model.RoleFindOneByCode(ctx, db, model.RoleAdmin)
	if err != nil {
		return
	}
	existingMenuIds, _ = model.RoleMenuFindByRoleId(ctx, db, adminRole.Id)
	if len(existingMenuIds) == 0 {
		adminMenuIds := make([]int64, 0)
		for _, m := range allMenus {
			adminMenuIds = append(adminMenuIds, m.Id)
		}
		model.RoleMenuSetForRole(ctx, db, adminRole.Id, adminMenuIds)
		log.Println("[Seed] admin role bound to all menus")
	}

	// guest 角色只绑定 default 类型菜单
	guestRole, err := model.RoleFindOneByCode(ctx, db, model.RoleGuest)
	if err != nil {
		return
	}
	existingMenuIds, _ = model.RoleMenuFindByRoleId(ctx, db, guestRole.Id)
	if len(existingMenuIds) == 0 {
		defaultMenuIds := make([]int64, 0)
		for _, m := range allMenus {
			if m.Type == "default" {
				defaultMenuIds = append(defaultMenuIds, m.Id)
			}
		}
		model.RoleMenuSetForRole(ctx, db, guestRole.Id, defaultMenuIds)
		log.Println("[Seed] guest role bound to default menus")
	}
}

Step 3: Call seed functions in NewServiceContext

After the existing seedSuperAdmin(db) and seedCasbinPolicies(enforcer) calls, add:

seedRoles(db)
seedMenus(db)
seedRoleMenus(db)

Step 4: Update seedCasbinPolicies with new API routes

Add these policies to the existing policies slice in seedCasbinPolicies:

// user: 获取当前用户菜单(不需要 Authz,但 Auth 保护)
// Note: /menus/current 不经过 Authz middleware,所以无需 Casbin 策略

// user: 个人机构相关
{"user", "/api/v1/profile/orgs", "GET"},
{"user", "/api/v1/profile/current-org", "PUT"},

// admin: 菜单管理(读取)
{"admin", "/api/v1/menus", "GET"},

// super_admin: 菜单管理(增删改)
{"super_admin", "/api/v1/menu", "POST"},
{"super_admin", "/api/v1/menu/:id", "PUT"},
{"super_admin", "/api/v1/menu/:id", "DELETE"},

// admin: 角色管理(读取)
{"admin", "/api/v1/roles", "GET"},
{"admin", "/api/v1/role/:id/menus", "GET"},

// super_admin: 角色管理(增删改)
{"super_admin", "/api/v1/role", "POST"},
{"super_admin", "/api/v1/role/:id", "PUT"},
{"super_admin", "/api/v1/role/:id", "DELETE"},
{"super_admin", "/api/v1/role/:id/menus", "PUT"},

// admin: 机构管理
{"admin", "/api/v1/organizations", "GET"},
{"admin", "/api/v1/organization", "POST"},
{"admin", "/api/v1/organization/:id", "PUT"},
{"admin", "/api/v1/organization/:id/members", "GET"},
{"admin", "/api/v1/organization/:id/member", "POST"},
{"admin", "/api/v1/organization/:id/member/:userId", "PUT"},
{"admin", "/api/v1/organization/:id/member/:userId", "DELETE"},

// super_admin: 机构删除
{"super_admin", "/api/v1/organization/:id", "DELETE"},

Step 5: Verify build

Run: cd D:\APPS\base\backend && go build ./... Expected: BUILD SUCCESS

Step 6: Commit

git add backend/internal/svc/servicecontext.go
git commit -m "feat: add AutoMigrate, seed roles/menus/role_menus, update Casbin policies"

Task 8: Menu Logic Implementation

Files:

  • Modify: backend/internal/logic/menu/getcurrentmenuslogic.go
  • Modify: backend/internal/logic/menu/getmenulistlogic.go
  • Modify: backend/internal/logic/menu/createmenulogic.go
  • Modify: backend/internal/logic/menu/updatemenulogic.go
  • Modify: backend/internal/logic/menu/deletemenulogic.go

These files are generated stubs by goctl. We need to fill in the business logic.

Step 1: Implement GetCurrentMenus logic

This is the core logic: get the user's role (from JWT), find the role's menu IDs, fetch those menus, and build a tree.

func (l *GetCurrentMenusLogic) GetCurrentMenus() (resp *types.MenuListResponse, err error) {
	// Get user's role from context
	role, _ := l.ctx.Value("role").(string)
	if role == "" {
		role = model.RoleGuest
	}

	// Find role record
	roleRecord, err := model.RoleFindOneByCode(l.ctx, l.svcCtx.DB, role)
	if err != nil {
		return nil, fmt.Errorf("查询角色失败: %v", err)
	}

	// Get menu IDs for this role
	menuIds, err := model.RoleMenuFindByRoleId(l.ctx, l.svcCtx.DB, roleRecord.Id)
	if err != nil {
		return nil, fmt.Errorf("查询角色菜单失败: %v", err)
	}

	// Also include all "default" type menus
	allMenus, err := model.MenuFindAll(l.ctx, l.svcCtx.DB)
	if err != nil {
		return nil, fmt.Errorf("查询菜单失败: %v", err)
	}

	// Build set of allowed menu IDs
	menuIdSet := make(map[int64]bool)
	for _, id := range menuIds {
		menuIdSet[id] = true
	}

	// Filter: include menus that are in role_menus OR are default type
	var filteredMenus []model.Menu
	for _, m := range allMenus {
		if menuIdSet[m.Id] || m.Type == "default" {
			if m.Visible {
				filteredMenus = append(filteredMenus, m)
			}
		}
	}

	// Build tree
	tree := buildMenuTree(filteredMenus, 0)

	resp = &types.MenuListResponse{
		List: tree,
	}
	return resp, nil
}

Add the buildMenuTree helper function (as a package-level function in the same file or a shared helper):

func buildMenuTree(menus []model.Menu, parentId int64) []types.MenuItem {
	var tree []types.MenuItem
	for _, m := range menus {
		if m.ParentId == parentId {
			item := types.MenuItem{
				Id:        m.Id,
				ParentId:  m.ParentId,
				Name:      m.Name,
				Path:      m.Path,
				Icon:      m.Icon,
				Component: m.Component,
				Type:      m.Type,
				SortOrder: m.SortOrder,
				Visible:   m.Visible,
				Status:    m.Status,
				Children:  buildMenuTree(menus, m.Id),
				CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
				UpdatedAt: m.UpdatedAt.Format("2006-01-02 15:04:05"),
			}
			tree = append(tree, item)
		}
	}
	if tree == nil {
		tree = []types.MenuItem{}
	}
	return tree
}

Step 2: Implement GetMenuList logic (admin — returns all menus as tree)

func (l *GetMenuListLogic) GetMenuList() (resp *types.MenuListResponse, err error) {
	menus, err := model.MenuFindAll(l.ctx, l.svcCtx.DB)
	if err != nil {
		return nil, fmt.Errorf("查询菜单列表失败: %v", err)
	}

	tree := buildMenuTree(menus, 0)
	resp = &types.MenuListResponse{List: tree}
	return resp, nil
}

(Reuse the same buildMenuTree helper — it can be defined as a package-level function.)

Step 3: Implement CreateMenu logic

func (l *CreateMenuLogic) CreateMenu(req *types.CreateMenuRequest) (resp *types.MenuItem, err error) {
	visible := true
	if req.Visible != nil {
		visible = *req.Visible
	}
	menuType := "config"
	if req.Type != "" {
		menuType = req.Type
	}

	menu := &model.Menu{
		ParentId:  req.ParentId,
		Name:      req.Name,
		Path:      req.Path,
		Icon:      req.Icon,
		Component: req.Component,
		Type:      menuType,
		SortOrder: req.SortOrder,
		Visible:   visible,
		Status:    1,
	}

	id, err := model.MenuInsert(l.ctx, l.svcCtx.DB, menu)
	if err != nil {
		return nil, fmt.Errorf("创建菜单失败: %v", err)
	}

	menu, err = model.MenuFindOne(l.ctx, l.svcCtx.DB, id)
	if err != nil {
		return nil, fmt.Errorf("查询菜单失败: %v", err)
	}

	resp = &types.MenuItem{
		Id:        menu.Id,
		ParentId:  menu.ParentId,
		Name:      menu.Name,
		Path:      menu.Path,
		Icon:      menu.Icon,
		Component: menu.Component,
		Type:      menu.Type,
		SortOrder: menu.SortOrder,
		Visible:   menu.Visible,
		Status:    menu.Status,
		Children:  []types.MenuItem{},
		CreatedAt: menu.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: menu.UpdatedAt.Format("2006-01-02 15:04:05"),
	}
	return resp, nil
}

Step 4: Implement UpdateMenu logic

func (l *UpdateMenuLogic) UpdateMenu(req *types.UpdateMenuRequest) (resp *types.MenuItem, err error) {
	menu, err := model.MenuFindOne(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("菜单不存在")
	}

	if req.Name != "" {
		menu.Name = req.Name
	}
	if req.Path != "" {
		menu.Path = req.Path
	}
	if req.Icon != "" {
		menu.Icon = req.Icon
	}
	if req.Component != "" {
		menu.Component = req.Component
	}
	if req.Type != "" {
		menu.Type = req.Type
	}
	if req.ParentId != nil {
		menu.ParentId = *req.ParentId
	}
	if req.SortOrder != nil {
		menu.SortOrder = *req.SortOrder
	}
	if req.Visible != nil {
		menu.Visible = *req.Visible
	}
	if req.Status != nil {
		menu.Status = *req.Status
	}

	if err := model.MenuUpdate(l.ctx, l.svcCtx.DB, menu); err != nil {
		return nil, fmt.Errorf("更新菜单失败: %v", err)
	}

	resp = &types.MenuItem{
		Id: menu.Id, ParentId: menu.ParentId, Name: menu.Name, Path: menu.Path,
		Icon: menu.Icon, Component: menu.Component, Type: menu.Type,
		SortOrder: menu.SortOrder, Visible: menu.Visible, Status: menu.Status,
		Children: []types.MenuItem{},
		CreatedAt: menu.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: menu.UpdatedAt.Format("2006-01-02 15:04:05"),
	}
	return resp, nil
}

Step 5: Implement DeleteMenu logic

func (l *DeleteMenuLogic) DeleteMenu(req *types.DeleteMenuRequest) (resp *types.Response, err error) {
	// Check if menu has children
	hasChildren, err := model.MenuHasChildren(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("检查子菜单失败: %v", err)
	}
	if hasChildren {
		return nil, fmt.Errorf("该菜单下有子菜单,无法删除")
	}

	if err := model.MenuDelete(l.ctx, l.svcCtx.DB, req.Id); err != nil {
		return nil, fmt.Errorf("删除菜单失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "删除成功", Success: true}, nil
}

Step 6: Verify build

Run: cd D:\APPS\base\backend && go build ./... Expected: BUILD SUCCESS

Step 7: Commit

git add backend/internal/logic/menu/
git commit -m "feat: implement menu CRUD logic"

Task 9: Role Logic Implementation

Files:

  • Modify: backend/internal/logic/role/getrolelistlogic.go
  • Modify: backend/internal/logic/role/createrolelogic.go
  • Modify: backend/internal/logic/role/updaterolelogic.go
  • Modify: backend/internal/logic/role/deleterolelogic.go
  • Modify: backend/internal/logic/role/getrolemenuslogic.go
  • Modify: backend/internal/logic/role/setrolemenuslogic.go

Step 1: Implement GetRoleList

func (l *GetRoleListLogic) GetRoleList() (resp *types.RoleListResponse, err error) {
	roles, err := model.RoleFindAll(l.ctx, l.svcCtx.DB)
	if err != nil {
		return nil, fmt.Errorf("查询角色列表失败: %v", err)
	}

	list := make([]types.RoleInfo, 0, len(roles))
	for _, r := range roles {
		list = append(list, types.RoleInfo{
			Id: r.Id, Name: r.Name, Code: r.Code, Description: r.Description,
			IsSystem: r.IsSystem, SortOrder: r.SortOrder, Status: r.Status,
			CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
			UpdatedAt: r.UpdatedAt.Format("2006-01-02 15:04:05"),
		})
	}

	return &types.RoleListResponse{List: list}, nil
}

Step 2: Implement CreateRole

func (l *CreateRoleLogic) CreateRole(req *types.CreateRoleRequest) (resp *types.RoleInfo, err error) {
	// Check code uniqueness
	_, err = model.RoleFindOneByCode(l.ctx, l.svcCtx.DB, req.Code)
	if err == nil {
		return nil, fmt.Errorf("角色编码已存在")
	}

	role := &model.Role{
		Name: req.Name, Code: req.Code, Description: req.Description,
		IsSystem: false, SortOrder: req.SortOrder, Status: 1,
	}

	id, err := model.RoleInsert(l.ctx, l.svcCtx.DB, role)
	if err != nil {
		return nil, fmt.Errorf("创建角色失败: %v", err)
	}

	role, _ = model.RoleFindOne(l.ctx, l.svcCtx.DB, id)
	return &types.RoleInfo{
		Id: role.Id, Name: role.Name, Code: role.Code, Description: role.Description,
		IsSystem: role.IsSystem, SortOrder: role.SortOrder, Status: role.Status,
		CreatedAt: role.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: role.UpdatedAt.Format("2006-01-02 15:04:05"),
	}, nil
}

Step 3: Implement UpdateRole

func (l *UpdateRoleLogic) UpdateRole(req *types.UpdateRoleRequest) (resp *types.RoleInfo, err error) {
	role, err := model.RoleFindOne(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("角色不存在")
	}

	if req.Name != "" {
		role.Name = req.Name
	}
	if req.Description != "" {
		role.Description = req.Description
	}
	if req.SortOrder != nil {
		role.SortOrder = *req.SortOrder
	}
	if req.Status != nil {
		role.Status = *req.Status
	}

	if err := model.RoleUpdate(l.ctx, l.svcCtx.DB, role); err != nil {
		return nil, fmt.Errorf("更新角色失败: %v", err)
	}

	return &types.RoleInfo{
		Id: role.Id, Name: role.Name, Code: role.Code, Description: role.Description,
		IsSystem: role.IsSystem, SortOrder: role.SortOrder, Status: role.Status,
		CreatedAt: role.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: role.UpdatedAt.Format("2006-01-02 15:04:05"),
	}, nil
}

Step 4: Implement DeleteRole

func (l *DeleteRoleLogic) DeleteRole(req *types.DeleteRoleRequest) (resp *types.Response, err error) {
	role, err := model.RoleFindOne(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("角色不存在")
	}
	if role.IsSystem {
		return nil, fmt.Errorf("系统内置角色不可删除")
	}

	// Delete role-menu associations
	model.RoleMenuDeleteByRoleId(l.ctx, l.svcCtx.DB, req.Id)

	if err := model.RoleDelete(l.ctx, l.svcCtx.DB, req.Id); err != nil {
		return nil, fmt.Errorf("删除角色失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "删除成功", Success: true}, nil
}

Step 5: Implement GetRoleMenus

func (l *GetRoleMenusLogic) GetRoleMenus(req *types.GetRoleMenusRequest) (resp *types.RoleMenusResponse, err error) {
	menuIds, err := model.RoleMenuFindByRoleId(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("查询角色菜单失败: %v", err)
	}
	if menuIds == nil {
		menuIds = []int64{}
	}
	return &types.RoleMenusResponse{MenuIds: menuIds}, nil
}

Step 6: Implement SetRoleMenus

func (l *SetRoleMenusLogic) SetRoleMenus(req *types.SetRoleMenusRequest) (resp *types.Response, err error) {
	// Verify role exists
	_, err = model.RoleFindOne(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("角色不存在")
	}

	if err := model.RoleMenuSetForRole(l.ctx, l.svcCtx.DB, req.Id, req.MenuIds); err != nil {
		return nil, fmt.Errorf("设置角色菜单失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "设置成功", Success: true}, nil
}

Step 7: Verify build and commit

Run: cd D:\APPS\base\backend && go build ./...

git add backend/internal/logic/role/
git commit -m "feat: implement role CRUD and menu assignment logic"

Task 10: Organization Logic Implementation

Files:

  • Modify: backend/internal/logic/organization/getorganizationlistlogic.go
  • Modify: backend/internal/logic/organization/createorganizationlogic.go
  • Modify: backend/internal/logic/organization/updateorganizationlogic.go
  • Modify: backend/internal/logic/organization/deleteorganizationlogic.go
  • Modify: backend/internal/logic/organization/getorgmemberslogic.go
  • Modify: backend/internal/logic/organization/addorgmemberlogic.go
  • Modify: backend/internal/logic/organization/updateorgmemberlogic.go
  • Modify: backend/internal/logic/organization/removeorgmemberlogic.go

Step 1: Implement GetOrganizationList

func (l *GetOrganizationListLogic) GetOrganizationList() (resp *types.OrgListResponse, err error) {
	orgs, err := model.OrgFindAll(l.ctx, l.svcCtx.DB)
	if err != nil {
		return nil, fmt.Errorf("查询机构列表失败: %v", err)
	}

	tree := buildOrgTree(l.ctx, l.svcCtx.DB, orgs, 0)
	return &types.OrgListResponse{List: tree}, nil
}

func buildOrgTree(ctx context.Context, db *gorm.DB, orgs []model.Organization, parentId int64) []types.OrgInfo {
	var tree []types.OrgInfo
	for _, o := range orgs {
		if o.ParentId == parentId {
			memberCount, _ := model.UserOrgCountByOrgId(ctx, db, o.Id)
			item := types.OrgInfo{
				Id: o.Id, ParentId: o.ParentId, Name: o.Name, Code: o.Code,
				Leader: o.Leader, Phone: o.Phone, Email: o.Email,
				SortOrder: o.SortOrder, Status: o.Status, MemberCount: memberCount,
				Children: buildOrgTree(ctx, db, orgs, o.Id),
				CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
				UpdatedAt: o.UpdatedAt.Format("2006-01-02 15:04:05"),
			}
			tree = append(tree, item)
		}
	}
	if tree == nil {
		tree = []types.OrgInfo{}
	}
	return tree
}

Step 2: Implement CreateOrganization

func (l *CreateOrganizationLogic) CreateOrganization(req *types.CreateOrgRequest) (resp *types.OrgInfo, err error) {
	// Check code uniqueness
	_, err = model.OrgFindOneByCode(l.ctx, l.svcCtx.DB, req.Code)
	if err == nil {
		return nil, fmt.Errorf("机构编码已存在")
	}

	org := &model.Organization{
		ParentId: req.ParentId, Name: req.Name, Code: req.Code,
		Leader: req.Leader, Phone: req.Phone, Email: req.Email,
		SortOrder: req.SortOrder, Status: 1,
	}

	id, err := model.OrgInsert(l.ctx, l.svcCtx.DB, org)
	if err != nil {
		return nil, fmt.Errorf("创建机构失败: %v", err)
	}

	org, _ = model.OrgFindOne(l.ctx, l.svcCtx.DB, id)
	return &types.OrgInfo{
		Id: org.Id, ParentId: org.ParentId, Name: org.Name, Code: org.Code,
		Leader: org.Leader, Phone: org.Phone, Email: org.Email,
		SortOrder: org.SortOrder, Status: org.Status, MemberCount: 0,
		Children: []types.OrgInfo{},
		CreatedAt: org.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: org.UpdatedAt.Format("2006-01-02 15:04:05"),
	}, nil
}

Step 3: Implement UpdateOrganization

func (l *UpdateOrganizationLogic) UpdateOrganization(req *types.UpdateOrgRequest) (resp *types.OrgInfo, err error) {
	org, err := model.OrgFindOne(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("机构不存在")
	}

	if req.Name != "" { org.Name = req.Name }
	if req.Code != "" { org.Code = req.Code }
	if req.Leader != "" { org.Leader = req.Leader }
	if req.Phone != "" { org.Phone = req.Phone }
	if req.Email != "" { org.Email = req.Email }
	if req.ParentId != nil { org.ParentId = *req.ParentId }
	if req.SortOrder != nil { org.SortOrder = *req.SortOrder }
	if req.Status != nil { org.Status = *req.Status }

	if err := model.OrgUpdate(l.ctx, l.svcCtx.DB, org); err != nil {
		return nil, fmt.Errorf("更新机构失败: %v", err)
	}

	memberCount, _ := model.UserOrgCountByOrgId(l.ctx, l.svcCtx.DB, org.Id)
	return &types.OrgInfo{
		Id: org.Id, ParentId: org.ParentId, Name: org.Name, Code: org.Code,
		Leader: org.Leader, Phone: org.Phone, Email: org.Email,
		SortOrder: org.SortOrder, Status: org.Status, MemberCount: memberCount,
		Children: []types.OrgInfo{},
		CreatedAt: org.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: org.UpdatedAt.Format("2006-01-02 15:04:05"),
	}, nil
}

Step 4: Implement DeleteOrganization

func (l *DeleteOrganizationLogic) DeleteOrganization(req *types.DeleteOrgRequest) (resp *types.Response, err error) {
	hasChildren, err := model.OrgHasChildren(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("检查子机构失败: %v", err)
	}
	if hasChildren {
		return nil, fmt.Errorf("该机构下有子机构,无法删除")
	}

	memberCount, _ := model.UserOrgCountByOrgId(l.ctx, l.svcCtx.DB, req.Id)
	if memberCount > 0 {
		return nil, fmt.Errorf("该机构下有成员,请先移除成员")
	}

	if err := model.OrgDelete(l.ctx, l.svcCtx.DB, req.Id); err != nil {
		return nil, fmt.Errorf("删除机构失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "删除成功", Success: true}, nil
}

Step 5: Implement GetOrgMembers

func (l *GetOrgMembersLogic) GetOrgMembers(req *types.GetOrgMembersRequest) (resp *types.OrgMembersResponse, err error) {
	userOrgs, err := model.UserOrgFindByOrgId(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("查询机构成员失败: %v", err)
	}

	list := make([]types.OrgMember, 0, len(userOrgs))
	for _, uo := range userOrgs {
		user, err := model.FindOne(l.ctx, l.svcCtx.DB, uo.UserId)
		if err != nil {
			continue
		}
		role, err := model.RoleFindOne(l.ctx, l.svcCtx.DB, uo.RoleId)
		roleName, roleCode := "", ""
		if err == nil {
			roleName = role.Name
			roleCode = role.Code
		}

		list = append(list, types.OrgMember{
			UserId: user.Id, Username: user.Username, Email: user.Email, Phone: user.Phone,
			RoleId: uo.RoleId, RoleName: roleName, RoleCode: roleCode,
			CreatedAt: uo.CreatedAt.Format("2006-01-02 15:04:05"),
		})
	}

	return &types.OrgMembersResponse{List: list}, nil
}

Step 6: Implement AddOrgMember

func (l *AddOrgMemberLogic) AddOrgMember(req *types.AddOrgMemberRequest) (resp *types.Response, err error) {
	// Verify org exists
	_, err = model.OrgFindOne(l.ctx, l.svcCtx.DB, req.Id)
	if err != nil {
		return nil, fmt.Errorf("机构不存在")
	}

	// Verify user exists
	_, err = model.FindOne(l.ctx, l.svcCtx.DB, req.UserId)
	if err != nil {
		return nil, fmt.Errorf("用户不存在")
	}

	// Check if already a member
	_, err = model.UserOrgFindOne(l.ctx, l.svcCtx.DB, req.UserId, req.Id)
	if err == nil {
		return nil, fmt.Errorf("该用户已是该机构成员")
	}

	uo := &model.UserOrganization{
		UserId: req.UserId, OrgId: req.Id, RoleId: req.RoleId,
	}
	if _, err := model.UserOrgInsert(l.ctx, l.svcCtx.DB, uo); err != nil {
		return nil, fmt.Errorf("添加成员失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "添加成功", Success: true}, nil
}

Step 7: Implement UpdateOrgMember

func (l *UpdateOrgMemberLogic) UpdateOrgMember(req *types.UpdateOrgMemberRequest) (resp *types.Response, err error) {
	uo, err := model.UserOrgFindOne(l.ctx, l.svcCtx.DB, req.UserId, req.Id)
	if err != nil {
		return nil, fmt.Errorf("成员关系不存在")
	}

	uo.RoleId = req.RoleId
	if err := model.UserOrgUpdate(l.ctx, l.svcCtx.DB, uo); err != nil {
		return nil, fmt.Errorf("更新成员角色失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "更新成功", Success: true}, nil
}

Step 8: Implement RemoveOrgMember

func (l *RemoveOrgMemberLogic) RemoveOrgMember(req *types.RemoveOrgMemberRequest) (resp *types.Response, err error) {
	if err := model.UserOrgDelete(l.ctx, l.svcCtx.DB, req.UserId, req.Id); err != nil {
		return nil, fmt.Errorf("移除成员失败: %v", err)
	}

	return &types.Response{Code: 200, Message: "移除成功", Success: true}, nil
}

Step 9: Verify build and commit

Run: cd D:\APPS\base\backend && go build ./...

git add backend/internal/logic/organization/
git commit -m "feat: implement organization CRUD and member management logic"

Task 11: Profile Extensions (GetUserOrgs + SwitchOrg)

Files:

  • Modify: backend/internal/logic/profile/getuserorgslogic.go
  • Modify: backend/internal/logic/profile/switchorglogic.go

Step 1: Implement GetUserOrgs

func (l *GetUserOrgsLogic) GetUserOrgs() (resp *types.UserOrgsResponse, err error) {
	userId, _ := l.ctx.Value("userId").(int64)

	userOrgs, err := model.UserOrgFindByUserId(l.ctx, l.svcCtx.DB, userId)
	if err != nil {
		return nil, fmt.Errorf("查询用户机构失败: %v", err)
	}

	list := make([]types.UserOrgInfo, 0, len(userOrgs))
	for _, uo := range userOrgs {
		org, err := model.OrgFindOne(l.ctx, l.svcCtx.DB, uo.OrgId)
		if err != nil {
			continue
		}
		role, err := model.RoleFindOne(l.ctx, l.svcCtx.DB, uo.RoleId)
		roleName, roleCode := "", ""
		if err == nil {
			roleName = role.Name
			roleCode = role.Code
		}

		list = append(list, types.UserOrgInfo{
			OrgId: org.Id, OrgName: org.Name,
			RoleId: uo.RoleId, RoleName: roleName, RoleCode: roleCode,
		})
	}

	return &types.UserOrgsResponse{List: list}, nil
}

Step 2: Implement SwitchOrg

func (l *SwitchOrgLogic) SwitchOrg(req *types.SwitchOrgRequest) (resp *types.SwitchOrgResponse, err error) {
	userId, _ := l.ctx.Value("userId").(int64)
	username, _ := l.ctx.Value("username").(string)

	// Verify user belongs to this org
	uo, err := model.UserOrgFindOne(l.ctx, l.svcCtx.DB, userId, req.OrgId)
	if err != nil {
		return nil, fmt.Errorf("您不属于该机构")
	}

	// Get role code for the org
	role, err := model.RoleFindOne(l.ctx, l.svcCtx.DB, uo.RoleId)
	if err != nil {
		return nil, fmt.Errorf("角色不存在")
	}

	// Update user's current_org_id
	user, err := model.FindOne(l.ctx, l.svcCtx.DB, userId)
	if err != nil {
		return nil, fmt.Errorf("用户不存在")
	}
	user.CurrentOrgId = req.OrgId
	model.Update(l.ctx, l.svcCtx.DB, user)

	// Generate new token with org role
	token, err := jwt.GenerateToken(userId, username, role.Code, req.OrgId)
	if err != nil {
		return nil, fmt.Errorf("生成Token失败: %v", err)
	}

	return &types.SwitchOrgResponse{Token: token}, nil
}

Step 3: Verify build and commit

Run: cd D:\APPS\base\backend && go build ./...

git add backend/internal/logic/profile/
git commit -m "feat: implement getUserOrgs and switchOrg logic"

Task 12: Backend Build Verification + Run Test

Step 1: Full build

Run: cd D:\APPS\base\backend && go build ./... Expected: BUILD SUCCESS (0 errors)

Step 2: Start server and test seed data

Run: cd D:\APPS\base\backend && go run base.go -f etc/base-api.yaml Expected logs:

  • [Casbin] Enforcer initialized successfully
  • [Seed] Super admin created or Updated admin to super_admin role
  • [Seed] Role created: super_admin (etc.)
  • [Seed] Menu created: 我的 (etc.)
  • [Seed] super_admin bound to all menus

Step 3: Test menu API with curl

# Login as admin
TOKEN=$(curl -s -X POST http://localhost:8888/api/v1/login -H "Content-Type: application/json" -d '{"account":"admin","password":"admin123"}' | jq -r '.token')

# Get current user menus
curl -s http://localhost:8888/api/v1/menus/current -H "Authorization: Bearer $TOKEN" | jq .

# Get all menus
curl -s http://localhost:8888/api/v1/menus -H "Authorization: Bearer $TOKEN" | jq .

# Get roles
curl -s http://localhost:8888/api/v1/roles -H "Authorization: Bearer $TOKEN" | jq .

Step 4: Commit if any fixes needed

git add -A
git commit -m "fix: backend build and seed data verification"

Task 13: Frontend Types + API Client

Files:

  • Modify: frontend/react-shadcn/pc/src/types/index.ts
  • Modify: frontend/react-shadcn/pc/src/services/api.ts

Step 1: Add new types to types/index.ts

Add at the end of the file:

// Menu Types
export interface MenuItem {
  id: number
  parentId: number
  name: string
  path: string
  icon: string
  component: string
  type: 'default' | 'config'
  sortOrder: number
  visible: boolean
  status: number
  children: MenuItem[]
  createdAt: string
  updatedAt: string
}

export interface CreateMenuRequest {
  parentId?: number
  name: string
  path?: string
  icon?: string
  component?: string
  type?: string
  sortOrder?: number
  visible?: boolean
}

export interface UpdateMenuRequest {
  parentId?: number
  name?: string
  path?: string
  icon?: string
  component?: string
  type?: string
  sortOrder?: number
  visible?: boolean
  status?: number
}

// Role Types
export interface RoleInfo {
  id: number
  name: string
  code: string
  description: string
  isSystem: boolean
  sortOrder: number
  status: number
  createdAt: string
  updatedAt: string
}

export interface CreateRoleRequest {
  name: string
  code: string
  description?: string
  sortOrder?: number
}

export interface UpdateRoleRequest {
  name?: string
  description?: string
  sortOrder?: number
  status?: number
}

// Organization Types
export interface OrgInfo {
  id: number
  parentId: number
  name: string
  code: string
  leader: string
  phone: string
  email: string
  sortOrder: number
  status: number
  memberCount: number
  children: OrgInfo[]
  createdAt: string
  updatedAt: string
}

export interface CreateOrgRequest {
  parentId?: number
  name: string
  code: string
  leader?: string
  phone?: string
  email?: string
  sortOrder?: number
}

export interface UpdateOrgRequest {
  parentId?: number
  name?: string
  code?: string
  leader?: string
  phone?: string
  email?: string
  sortOrder?: number
  status?: number
}

export interface OrgMember {
  userId: number
  username: string
  email: string
  phone: string
  roleId: number
  roleName: string
  roleCode: string
  createdAt: string
}

export interface UserOrgInfo {
  orgId: number
  orgName: string
  roleId: number
  roleName: string
  roleCode: string
}

Step 2: Add API methods to api.ts

Add these methods to the ApiClient class:

// Menu Management
async getCurrentMenus(): Promise<{ list: MenuItem[] }> {
  const rawData = await this.request<{ list: MenuItem[] }>('/menus/current')
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async getMenuList(): Promise<{ list: MenuItem[] }> {
  const rawData = await this.request<{ list: MenuItem[] }>('/menus')
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async createMenu(data: CreateMenuRequest): Promise<MenuItem> {
  return this.request<MenuItem>('/menu', { method: 'POST', body: JSON.stringify(data) })
}

async updateMenu(id: number, data: UpdateMenuRequest): Promise<MenuItem> {
  return this.request<MenuItem>(`/menu/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}

async deleteMenu(id: number): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/menu/${id}`, { method: 'DELETE' })
}

// Role Management
async getRoles(): Promise<{ list: RoleInfo[] }> {
  const rawData = await this.request<{ list: RoleInfo[] }>('/roles')
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async createRole(data: CreateRoleRequest): Promise<RoleInfo> {
  return this.request<RoleInfo>('/role', { method: 'POST', body: JSON.stringify(data) })
}

async updateRole(id: number, data: UpdateRoleRequest): Promise<RoleInfo> {
  return this.request<RoleInfo>(`/role/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}

async deleteRole(id: number): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/role/${id}`, { method: 'DELETE' })
}

async getRoleMenus(roleId: number): Promise<{ menuIds: number[] }> {
  const rawData = await this.request<{ menuIds: number[] }>(`/role/${roleId}/menus`)
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async setRoleMenus(roleId: number, menuIds: number[]): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/role/${roleId}/menus`, {
    method: 'PUT', body: JSON.stringify({ menuIds }),
  })
}

// Organization Management
async getOrganizations(): Promise<{ list: OrgInfo[] }> {
  const rawData = await this.request<{ list: OrgInfo[] }>('/organizations')
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async createOrganization(data: CreateOrgRequest): Promise<OrgInfo> {
  return this.request<OrgInfo>('/organization', { method: 'POST', body: JSON.stringify(data) })
}

async updateOrganization(id: number, data: UpdateOrgRequest): Promise<OrgInfo> {
  return this.request<OrgInfo>(`/organization/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}

async deleteOrganization(id: number): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/organization/${id}`, { method: 'DELETE' })
}

async getOrgMembers(orgId: number): Promise<{ list: OrgMember[] }> {
  const rawData = await this.request<{ list: OrgMember[] }>(`/organization/${orgId}/members`)
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async addOrgMember(orgId: number, userId: number, roleId: number): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/organization/${orgId}/member`, {
    method: 'POST', body: JSON.stringify({ userId, roleId }),
  })
}

async updateOrgMember(orgId: number, userId: number, roleId: number): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/organization/${orgId}/member/${userId}`, {
    method: 'PUT', body: JSON.stringify({ roleId }),
  })
}

async removeOrgMember(orgId: number, userId: number): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>(`/organization/${orgId}/member/${userId}`, {
    method: 'DELETE',
  })
}

// User Org Context
async getUserOrgs(): Promise<{ list: UserOrgInfo[] }> {
  const rawData = await this.request<{ list: UserOrgInfo[] }>('/profile/orgs')
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

async switchOrg(orgId: number): Promise<{ token: string }> {
  const rawData = await this.request<{ token: string }>('/profile/current-org', {
    method: 'PUT', body: JSON.stringify({ orgId }),
  })
  if ('success' in rawData) return (rawData as any).data || rawData
  return rawData
}

Also add the new type imports at the top of api.ts.

Step 3: Verify build

Run: cd D:\APPS\base\frontend\react-shadcn\pc && npm run build Expected: BUILD SUCCESS

Step 4: Commit

git add frontend/react-shadcn/pc/src/types/index.ts frontend/react-shadcn/pc/src/services/api.ts
git commit -m "feat: add frontend types and API client methods for menu/role/org"

Task 14: Frontend AuthContext Extension

Files:

  • Modify: frontend/react-shadcn/pc/src/contexts/AuthContext.tsx

Step 1: Extend AuthContextType interface

Add to the interface:

interface AuthContextType {
  // ... existing fields ...
  currentOrg: { id: number; name: string } | null
  userOrgs: UserOrgInfo[]
  userMenus: MenuItem[]
  switchOrg: (orgId: number) => Promise<void>
  refreshMenus: () => Promise<void>
}

Step 2: Add state and methods

In AuthProvider, add after existing state:

const [currentOrg, setCurrentOrg] = useState<{ id: number; name: string } | null>(null)
const [userOrgs, setUserOrgs] = useState<UserOrgInfo[]>([])
const [userMenus, setUserMenus] = useState<MenuItem[]>([])

Add refreshMenus function:

const refreshMenus = async () => {
  try {
    const data = await apiClient.getCurrentMenus()
    setUserMenus(data.list || [])
  } catch (e) {
    console.error('Failed to fetch menus:', e)
  }
}

Add loadUserContext function (called after login):

const loadUserContext = async () => {
  try {
    // Load user orgs
    const orgsData = await apiClient.getUserOrgs()
    const orgs = orgsData.list || []
    setUserOrgs(orgs)

    // If user has orgs, select the first one (or the saved one from JWT)
    if (orgs.length > 0) {
      const storedOrg = localStorage.getItem('currentOrg')
      let selectedOrg = orgs[0]
      if (storedOrg) {
        try {
          const parsed = JSON.parse(storedOrg)
          const found = orgs.find(o => o.orgId === parsed.id)
          if (found) selectedOrg = found
        } catch {}
      }
      setCurrentOrg({ id: selectedOrg.orgId, name: selectedOrg.orgName })
      localStorage.setItem('currentOrg', JSON.stringify({ id: selectedOrg.orgId, name: selectedOrg.orgName }))
    }

    // Load menus
    await refreshMenus()
  } catch (e) {
    console.error('Failed to load user context:', e)
    // Still load menus even if orgs fail
    await refreshMenus()
  }
}

Add switchOrg function:

const switchOrg = async (orgId: number) => {
  try {
    const data = await apiClient.switchOrg(orgId)
    if (data.token) {
      localStorage.setItem('token', data.token)
      setToken(data.token)

      // Update user from new token
      try {
        const payload = JSON.parse(atob(data.token.split('.')[1]))
        const userData: User = {
          id: payload.userId || 0,
          username: payload.username || '',
          email: '',
          role: payload.role || 'user',
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString(),
        }
        setUser(userData)
        localStorage.setItem('user', JSON.stringify(userData))
      } catch {}

      // Update current org
      const org = userOrgs.find(o => o.orgId === orgId)
      if (org) {
        setCurrentOrg({ id: org.orgId, name: org.orgName })
        localStorage.setItem('currentOrg', JSON.stringify({ id: org.orgId, name: org.orgName }))
      }

      // Refresh menus for new role
      await refreshMenus()
    }
  } catch (e) {
    console.error('Failed to switch org:', e)
    throw e
  }
}

Step 3: Call loadUserContext after login

In the login function, after setting user state, add:

// Load org and menu context
setTimeout(() => loadUserContext(), 100)

In the loginWithToken function, after setting user state, add the same.

Also in the initial useEffect that restores from localStorage, if token exists:

if (storedToken) {
  setToken(storedToken)
  // Restore saved org
  const savedOrg = localStorage.getItem('currentOrg')
  if (savedOrg) {
    try { setCurrentOrg(JSON.parse(savedOrg)) } catch {}
  }
}

And add a separate useEffect to load menus/orgs after authentication:

useEffect(() => {
  if (token && !isLoading) {
    loadUserContext()
  }
}, [token, isLoading])

Step 4: Update the value object

const value: AuthContextType = {
  user, token, isAuthenticated: !!token, isLoading,
  login, loginWithToken, logout,
  currentOrg, userOrgs, userMenus,
  switchOrg, refreshMenus,
}

Step 5: Verify build

Run: cd D:\APPS\base\frontend\react-shadcn\pc && npm run build

Step 6: Commit

git add frontend/react-shadcn/pc/src/contexts/AuthContext.tsx
git commit -m "feat: extend AuthContext with org switching and dynamic menus"

Task 15: Frontend Dynamic Sidebar + OrgSwitcher

Files:

  • Modify: frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx

Step 1: Replace hardcoded navItems with dynamic menus from context

Replace the entire Sidebar component to use userMenus from AuthContext and add org switcher:

import { NavLink } from 'react-router-dom'
import {
  LayoutDashboard, Users, LogOut, Settings, FolderOpen,
  Shield, Menu as MenuIcon, Building2, User, ChevronDown, LucideIcon
} from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
import { useState, useRef, useEffect } from 'react'
import type { MenuItem } from '@/types'

// Icon mapping from string name to lucide-react component
const iconMap: Record<string, LucideIcon> = {
  User, LayoutDashboard, Users, FolderOpen, Shield,
  Menu: MenuIcon, Building2, Settings,
}

function getIcon(iconName: string): LucideIcon {
  return iconMap[iconName] || LayoutDashboard
}

export function Sidebar() {
  const { user, logout, userMenus, currentOrg, userOrgs, switchOrg } = useAuth()
  const [orgDropdownOpen, setOrgDropdownOpen] = useState(false)
  const dropdownRef = useRef<HTMLDivElement>(null)

  // Close dropdown on outside click
  useEffect(() => {
    function handleClickOutside(e: MouseEvent) {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
        setOrgDropdownOpen(false)
      }
    }
    document.addEventListener('mousedown', handleClickOutside)
    return () => document.removeEventListener('mousedown', handleClickOutside)
  }, [])

  const handleSwitchOrg = async (orgId: number) => {
    await switchOrg(orgId)
    setOrgDropdownOpen(false)
  }

  // Use dynamic menus from context, fallback to defaults if empty
  const menuItems: MenuItem[] = userMenus.length > 0 ? userMenus : []

  return (
    <aside className="w-64 h-screen bg-gray-900/80 backdrop-blur-xl border-r border-gray-800 flex flex-col fixed left-0 top-0 z-40">
      {/* Logo */}
      <div className="p-6 border-b border-gray-800">
        <div className="flex items-center gap-3">
          <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-sky-500 to-blue-600 flex items-center justify-center glow-primary">
            <span className="text-xl font-bold text-white font-display">B</span>
          </div>
          <div>
            <h1 className="text-lg font-bold text-white font-display">BASE</h1>
            <p className="text-xs text-gray-500 font-body">管理面板</p>
          </div>
        </div>
      </div>

      {/* Org Switcher */}
      {userOrgs.length > 0 && (
        <div className="px-4 pt-4" ref={dropdownRef}>
          <button
            onClick={() => setOrgDropdownOpen(!orgDropdownOpen)}
            className="w-full flex items-center justify-between px-3 py-2 rounded-lg bg-gray-800/50 border border-gray-700 text-sm text-gray-300 hover:bg-gray-800 transition-colors"
          >
            <span className="truncate">{currentOrg?.name || '选择机构'}</span>
            <ChevronDown className={cn('h-4 w-4 transition-transform', orgDropdownOpen && 'rotate-180')} />
          </button>
          {orgDropdownOpen && (
            <div className="mt-1 py-1 rounded-lg bg-gray-800 border border-gray-700 shadow-xl">
              {userOrgs.map((org) => (
                <button
                  key={org.orgId}
                  onClick={() => handleSwitchOrg(org.orgId)}
                  className={cn(
                    'w-full text-left px-3 py-2 text-sm transition-colors',
                    currentOrg?.id === org.orgId
                      ? 'text-sky-400 bg-sky-500/10'
                      : 'text-gray-400 hover:text-gray-200 hover:bg-gray-700/50'
                  )}
                >
                  <div className="truncate">{org.orgName}</div>
                  <div className="text-xs text-gray-500">{org.roleName}</div>
                </button>
              ))}
            </div>
          )}
        </div>
      )}

      {/* Navigation */}
      <nav className="flex-1 p-4 space-y-1 overflow-y-auto">
        {menuItems.map((item) => {
          const Icon = getIcon(item.icon)
          return (
            <NavLink
              key={item.path}
              to={item.path}
              className={({ isActive }) =>
                cn(
                  'flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200',
                  'font-body text-sm font-medium',
                  isActive
                    ? 'bg-gradient-to-r from-sky-500/20 to-blue-600/20 text-sky-400 border border-sky-500/30'
                    : 'text-gray-400 hover:bg-gray-800/50 hover:text-gray-200'
                )
              }
            >
              <Icon className="h-5 w-5" />
              {item.name}
            </NavLink>
          )
        })}
      </nav>

      {/* User Info */}
      <div className="p-4 border-t border-gray-800">
        <div className="flex items-center gap-3 p-3 rounded-lg bg-gray-800/50">
          <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
            <span className="text-lg font-bold text-white font-display">
              {user?.username?.[0]?.toUpperCase() || 'U'}
            </span>
          </div>
          <div className="flex-1 min-w-0">
            <p className="text-sm font-medium text-white font-body truncate">
              {user?.username || 'User'}
            </p>
            <p className="text-xs text-gray-500 font-body truncate">
              {user?.role || ''}
            </p>
          </div>
          <button
            onClick={logout}
            className="p-2 text-gray-500 hover:text-red-400 transition-colors"
            title="退出登录"
          >
            <LogOut className="h-4 w-4" />
          </button>
        </div>
      </div>
    </aside>
  )
}

function cn(...classes: (string | undefined | false | null)[]) {
  return classes.filter(Boolean).join(' ')
}

Step 2: Verify build

Run: cd D:\APPS\base\frontend\react-shadcn\pc && npm run build

Step 3: Commit

git add frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx
git commit -m "feat: dynamic sidebar with org switcher, menu from API"

Task 16: Frontend MyPage

Files:

  • Create: frontend/react-shadcn/pc/src/pages/MyPage.tsx

Step 1: Create MyPage

Create a personal information page showing user info, role, and organizations:

import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
import { useAuth } from '@/contexts/AuthContext'
import { User, Building2, Shield } from 'lucide-react'

export function MyPage() {
  const { user, currentOrg, userOrgs } = useAuth()

  return (
    <div className="space-y-6">
      {/* User Info Card */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <User className="h-5 w-5" />
            个人信息
          </CardTitle>
        </CardHeader>
        <CardContent>
          <div className="grid grid-cols-2 gap-4">
            <div>
              <p className="text-sm text-gray-500">用户名</p>
              <p className="text-white font-medium">{user?.username || '-'}</p>
            </div>
            <div>
              <p className="text-sm text-gray-500">邮箱</p>
              <p className="text-white font-medium">{user?.email || '-'}</p>
            </div>
            <div>
              <p className="text-sm text-gray-500">当前角色</p>
              <p className="text-white font-medium">{user?.role || '-'}</p>
            </div>
            <div>
              <p className="text-sm text-gray-500">当前机构</p>
              <p className="text-white font-medium">{currentOrg?.name || '无'}</p>
            </div>
          </div>
        </CardContent>
      </Card>

      {/* Organizations Card */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <Building2 className="h-5 w-5" />
            我的机构
          </CardTitle>
        </CardHeader>
        <CardContent>
          {userOrgs.length === 0 ? (
            <p className="text-gray-500 text-sm">暂未加入任何机构</p>
          ) : (
            <div className="space-y-3">
              {userOrgs.map((org) => (
                <div key={org.orgId} className="flex items-center justify-between p-3 rounded-lg bg-gray-800/50">
                  <div>
                    <p className="text-white font-medium">{org.orgName}</p>
                    <div className="flex items-center gap-2 mt-1">
                      <Shield className="h-3 w-3 text-gray-400" />
                      <span className="text-xs text-gray-400">{org.roleName}</span>
                    </div>
                  </div>
                  {currentOrg?.id === org.orgId && (
                    <span className="text-xs px-2 py-1 rounded bg-sky-500/20 text-sky-400 border border-sky-500/30">
                      当前
                    </span>
                  )}
                </div>
              ))}
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  )
}

Step 2: Commit

git add frontend/react-shadcn/pc/src/pages/MyPage.tsx
git commit -m "feat: add MyPage component"

Task 17: Frontend MenuManagementPage

Files:

  • Create: frontend/react-shadcn/pc/src/pages/MenuManagementPage.tsx

Use the @frontend-design skill to build this page matching the existing dark theme (gray-900 bg, sky-500 accents). The page needs:

  • Tree table showing menus with parent-child hierarchy (indentation)
  • Columns: Name (with icon), Path, Type (badge), Sort Order, Visible (toggle), Status, Actions
  • Create/Edit modal with fields: parent, name, path, icon, type, sortOrder, visible
  • Delete confirmation dialog
  • Admin-only: CRUD operations

Follow the pattern established in UserManagementPage.tsx and FileManagementPage.tsx for modal patterns, table structure, and API integration.

Step 1: Create the page (implement using established patterns)

Step 2: Verify build and commit

git add frontend/react-shadcn/pc/src/pages/MenuManagementPage.tsx
git commit -m "feat: add MenuManagementPage component"

Task 18: Frontend RoleManagementPage

Files:

  • Create: frontend/react-shadcn/pc/src/pages/RoleManagementPage.tsx

Use the @frontend-design skill. The page needs:

  • Role list table: Name, Code, Description, IsSystem (badge), Status, Actions
  • Create/Edit modal with fields: name, code, description
  • Delete button (disabled for system roles)
  • Menu assignment dialog: Click "分配菜单" → opens modal with tree checkbox list of all menus → save selected menuIds
  • Follow existing page patterns

Step 1: Create the page

Step 2: Verify build and commit

git add frontend/react-shadcn/pc/src/pages/RoleManagementPage.tsx
git commit -m "feat: add RoleManagementPage component"

Task 19: Frontend OrganizationManagementPage

Files:

  • Create: frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx

Use the @frontend-design skill. The page needs:

  • Tree table for organizations (like menu management but for orgs)
  • Columns: Name, Code, Leader, Phone, Member Count, Status, Actions
  • Create/Edit modal
  • Delete (disabled if has children or members)
  • Member management drawer/modal: Click "成员管理" → shows member list with role, add/remove/change role buttons
  • Follow existing page patterns

Step 1: Create the page

Step 2: Verify build and commit

git add frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx
git commit -m "feat: add OrganizationManagementPage component"

Task 20: Frontend Route Registration + MainLayout Update

Files:

  • Modify: frontend/react-shadcn/pc/src/App.tsx
  • Modify: frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx

Step 1: Add routes in App.tsx

Add imports:

import { MyPage } from './pages/MyPage'
import { MenuManagementPage } from './pages/MenuManagementPage'
import { RoleManagementPage } from './pages/RoleManagementPage'
import { OrganizationManagementPage } from './pages/OrganizationManagementPage'

Add routes inside the <Route path="/" ...> block:

<Route path="my" element={<MyPage />} />
<Route path="menus" element={<MenuManagementPage />} />
<Route path="roles" element={<RoleManagementPage />} />
<Route path="organizations" element={<OrganizationManagementPage />} />

Step 2: Update MainLayout pageTitles

Add to the pageTitles object:

'/my': { title: '我的', subtitle: '个人信息和机构' },
'/menus': { title: '菜单管理', subtitle: '管理系统菜单配置' },
'/roles': { title: '角色管理', subtitle: '管理角色和权限' },
'/organizations': { title: '机构管理', subtitle: '管理组织架构' },

Step 3: Verify build

Run: cd D:\APPS\base\frontend\react-shadcn\pc && npm run build Expected: BUILD SUCCESS

Step 4: Commit

git add frontend/react-shadcn/pc/src/App.tsx frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx
git commit -m "feat: register new routes and update page titles for menu/role/org pages"

Task 21: End-to-End Verification

Step 1: Start backend

Run: cd D:\APPS\base\backend && go run base.go -f etc/base-api.yaml Verify seed logs appear for roles, menus, and role-menu associations.

Step 2: Start frontend

Run: cd D:\APPS\base\frontend\react-shadcn\pc && npm run dev

Step 3: Login as admin and verify

  1. Login with admin/admin123
  2. Verify sidebar shows all 8 menus dynamically loaded from API
  3. Navigate to each new page: 我的, 菜单管理, 角色管理, 机构管理
  4. Test CRUD on each page
  5. Create a test organization
  6. Add admin user as member with a role
  7. Switch org and verify menus reload

Step 4: Test permission control

  1. Register a new regular user
  2. Login as regular user → should only see "我的" and "设置" menus
  3. Verify other pages return 403

Step 5: Final commit if any fixes needed

git add -A
git commit -m "feat: complete menu/role/org management with E2E verification"