From bbb7b09a3af0722886d5983ebcdeb5bb55f0d250 Mon Sep 17 00:00:00 2001 From: dark Date: Sat, 14 Feb 2026 11:12:47 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=8F=9C=E5=8D=95=E7=AE=A1=E7=90=86+?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E7=AE=A1=E7=90=86+=E6=9C=BA=E6=9E=84?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/2026-02-14-menu-role-org-impl.md | 2948 +++++++++++++++++++ 1 file changed, 2948 insertions(+) create mode 100644 docs/plans/2026-02-14-menu-role-org-impl.md diff --git a/docs/plans/2026-02-14-menu-role-org-impl.md b/docs/plans/2026-02-14-menu-role-org-impl.md new file mode 100644 index 0000000..ceb17f5 --- /dev/null +++ b/docs/plans/2026-02-14-menu-role-org-impl.md @@ -0,0 +1,2948 @@ +# 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" +```