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:
backend/internal/logic/auth/loginlogic.go:55:
token, err := jwt.GenerateToken(user.Id, user.Username, user.Role, user.CurrentOrgId)
-
backend/internal/logic/auth/registerlogic.go— register doesn't generate token (it returns UserInfo), so no change needed. -
backend/internal/logic/auth/refreshtokenlogic.go:55:
newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Role, user.CurrentOrgId)
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.gowith new type definitions - Regenerate
internal/handler/routes.gowith 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 createdorUpdated 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
- Login with admin/admin123
- Verify sidebar shows all 8 menus dynamically loaded from API
- Navigate to each new page: 我的, 菜单管理, 角色管理, 机构管理
- Test CRUD on each page
- Create a test organization
- Add admin user as member with a role
- Switch org and verify menus reload
Step 4: Test permission control
- Register a new regular user
- Login as regular user → should only see "我的" and "设置" menus
- 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"