# 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`: ```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`: ```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** ```bash 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`: ```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`: ```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** ```bash 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`: ```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`: ```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`: ```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`: ```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`: ```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** ```bash 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: ```go 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: ```go 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)`: ```go 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`: ```go token, err := jwt.GenerateToken(user.Id, user.Username, user.Role, user.CurrentOrgId) ``` 2. `backend/internal/logic/auth/registerlogic.go` — register doesn't generate token (it returns UserInfo), so **no change needed**. 3. `backend/internal/logic/auth/refreshtokenlogic.go:55`: ```go newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Role, user.CurrentOrgId) ``` 4. `backend/internal/logic/auth/ssologic.go:177` (the HandleCallback method): ```go 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** ```bash 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`: ```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`: ```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`: ```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"`): ```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): ```api // ========== 菜单管理(当前用户菜单,需登录) ========== @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** ```bash 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** ```bash 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: ```go 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`: ```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: ```go seedRoles(db) seedMenus(db) seedRoleMenus(db) ``` **Step 4: Update seedCasbinPolicies with new API routes** Add these policies to the existing `policies` slice in `seedCasbinPolicies`: ```go // 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** ```bash 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. ```go 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): ```go 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)** ```go 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** ```go 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** ```go 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** ```go 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** ```bash 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** ```go 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** ```go 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** ```go 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** ```go 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** ```go 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** ```go 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 ./...` ```bash 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** ```go 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** ```go 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** ```go 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** ```go 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** ```go 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** ```go 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** ```go 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** ```go 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 ./...` ```bash 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** ```go 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** ```go 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 ./...` ```bash 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** ```bash # 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** ```bash 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: ```typescript // 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: ```typescript // 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 { return this.request('/menu', { method: 'POST', body: JSON.stringify(data) }) } async updateMenu(id: number, data: UpdateMenuRequest): Promise { return this.request(`/menu/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } async deleteMenu(id: number): Promise> { return this.request>(`/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 { return this.request('/role', { method: 'POST', body: JSON.stringify(data) }) } async updateRole(id: number, data: UpdateRoleRequest): Promise { return this.request(`/role/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } async deleteRole(id: number): Promise> { return this.request>(`/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> { return this.request>(`/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 { return this.request('/organization', { method: 'POST', body: JSON.stringify(data) }) } async updateOrganization(id: number, data: UpdateOrgRequest): Promise { return this.request(`/organization/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } async deleteOrganization(id: number): Promise> { return this.request>(`/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> { return this.request>(`/organization/${orgId}/member`, { method: 'POST', body: JSON.stringify({ userId, roleId }), }) } async updateOrgMember(orgId: number, userId: number, roleId: number): Promise> { return this.request>(`/organization/${orgId}/member/${userId}`, { method: 'PUT', body: JSON.stringify({ roleId }), }) } async removeOrgMember(orgId: number, userId: number): Promise> { return this.request>(`/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** ```bash 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: ```typescript interface AuthContextType { // ... existing fields ... currentOrg: { id: number; name: string } | null userOrgs: UserOrgInfo[] userMenus: MenuItem[] switchOrg: (orgId: number) => Promise refreshMenus: () => Promise } ``` **Step 2: Add state and methods** In `AuthProvider`, add after existing state: ```typescript const [currentOrg, setCurrentOrg] = useState<{ id: number; name: string } | null>(null) const [userOrgs, setUserOrgs] = useState([]) const [userMenus, setUserMenus] = useState([]) ``` Add `refreshMenus` function: ```typescript 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): ```typescript 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: ```typescript 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: ```typescript // 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: ```typescript 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: ```typescript useEffect(() => { if (token && !isLoading) { loadUserContext() } }, [token, isLoading]) ``` **Step 4: Update the value object** ```typescript 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** ```bash 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: ```typescript 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 = { 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(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 ( ) } 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** ```bash 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: ```typescript 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 (
{/* User Info Card */} 个人信息

用户名

{user?.username || '-'}

邮箱

{user?.email || '-'}

当前角色

{user?.role || '-'}

当前机构

{currentOrg?.name || '无'}

{/* Organizations Card */} 我的机构 {userOrgs.length === 0 ? (

暂未加入任何机构

) : (
{userOrgs.map((org) => (

{org.orgName}

{org.roleName}
{currentOrg?.id === org.orgId && ( 当前 )}
))}
)}
) } ``` **Step 2: Commit** ```bash 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** ```bash 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** ```bash 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** ```bash 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: ```typescript import { MyPage } from './pages/MyPage' import { MenuManagementPage } from './pages/MenuManagementPage' import { RoleManagementPage } from './pages/RoleManagementPage' import { OrganizationManagementPage } from './pages/OrganizationManagementPage' ``` Add routes inside the `` block: ```typescript } /> } /> } /> } /> ``` **Step 2: Update MainLayout pageTitles** Add to the `pageTitles` object: ```typescript '/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** ```bash 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** ```bash git add -A git commit -m "feat: complete menu/role/org management with E2E verification" ```