From 78756a849f7bb50ae309d9fdc90f19a2b24211e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Wed, 26 Aug 2026 22:23:21 +0800 Subject: [PATCH] =?UTF-8?q?CHG:=20=E7=A7=BB=E9=99=A4=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E7=AE=A1=E7=90=86=E8=8F=9C=E5=8D=95=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=EF=BC=88008=5Fmenu=5Frbac=5Fv4.sql=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/admin/admin/admin.go | 74 +++++++++ api/admin/system/menu/menu.go | 33 ++++ api/admin/system/menu_manage/menu_manage.go | 78 ++++++++++ api/admin/system/role/role.go | 63 ++++++++ api/open/tools/doc.go | 15 ++ api/open/tools/ip/ip.go | 15 ++ api/open/tools/md5/md5.go | 15 ++ api/open/tools/random/random.go | 16 ++ api/open/tools/time/time.go | 16 ++ api/open/tools/uuid/uuid.go | 15 ++ api/user/auth/auth.go | 33 ++++ internal/service/admin/admin/admin/admin.go | 158 ++++++++++++++++++++ manifest/sql/004b_fill_seed.sql | 43 ++++++ manifest/sql/007_menu_permissions_v3.sql | 36 +++++ manifest/sql/008_menu_rbac_v4.sql | 23 +++ 15 files changed, 633 insertions(+) create mode 100644 api/admin/admin/admin/admin.go create mode 100644 api/admin/system/menu/menu.go create mode 100644 api/admin/system/menu_manage/menu_manage.go create mode 100644 api/admin/system/role/role.go create mode 100644 api/open/tools/doc.go create mode 100644 api/open/tools/ip/ip.go create mode 100644 api/open/tools/md5/md5.go create mode 100644 api/open/tools/random/random.go create mode 100644 api/open/tools/time/time.go create mode 100644 api/open/tools/uuid/uuid.go create mode 100644 api/user/auth/auth.go create mode 100644 internal/service/admin/admin/admin/admin.go create mode 100644 manifest/sql/004b_fill_seed.sql create mode 100644 manifest/sql/007_menu_permissions_v3.sql create mode 100644 manifest/sql/008_menu_rbac_v4.sql diff --git a/api/admin/admin/admin/admin.go b/api/admin/admin/admin/admin.go new file mode 100644 index 0000000..5b088b0 --- /dev/null +++ b/api/admin/admin/admin/admin.go @@ -0,0 +1,74 @@ +// Package admin_admin_admin 定义后台管理接口(管理员账号管理),路由前缀 /admin。 +package admin_admin_admin + +import "github.com/gogf/gf/v2/frame/g" + +// AdminItem 管理员列表中的一行。 +type AdminItem struct { + Id uint64 `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Status int `json:"status"` + RoleIds []uint64 `json:"roleIds"` + RoleNames []string `json:"roleNames"` + CreatedAt string `json:"createdAt"` +} + +// AdminListReq 分页查询管理员。 +type AdminListReq struct { + g.Meta `path:"/admin/list" method:"post" tags:"Admin/Admin" summary:"管理员列表"` + Page int `json:"page" d:"1" v:"min:1"` + Size int `json:"size" d:"10" v:"min:1|max:100"` + Keyword string `json:"keyword"` // 匹配用户名或昵称 +} + +// AdminListRes 是 AdminListReq 的响应。 +type AdminListRes struct { + List []*AdminItem `json:"list"` + Total int `json:"total"` +} + +// AdminCreateReq 创建管理员。 +type AdminCreateReq struct { + g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"` + Username string `json:"username" v:"required"` + Password string `json:"password" v:"required|min-length:6#password required|password too short"` + Nickname string `json:"nickname"` + RoleIds []uint64 `json:"roleIds"` +} + +// AdminCreateRes 是 AdminCreateReq 的响应。 +type AdminCreateRes struct { + Id uint64 `json:"id"` +} + +// AdminUpdateReq 更新管理员的资料、状态与角色。 +type AdminUpdateReq struct { + g.Meta `path:"/admin/update/{id}" method:"post" tags:"Admin/Admin" summary:"更新管理员"` + Id uint64 `json:"id" in:"path" v:"required"` + Nickname string `json:"nickname"` + Status int `json:"status"` + RoleIds []uint64 `json:"roleIds"` +} + +// AdminUpdateRes 是 AdminUpdateReq 的响应。 +type AdminUpdateRes struct{} + +// AdminResetPwdReq 重置管理员密码。 +type AdminResetPwdReq struct { + g.Meta `path:"/admin/resetPwd/{id}" method:"post" tags:"Admin/Admin" summary:"重置管理员密码"` + Id uint64 `json:"id" in:"path" v:"required"` + Password string `json:"password" v:"required|min-length:6#password required|password too short"` +} + +// AdminResetPwdRes 是 AdminResetPwdReq 的响应。 +type AdminResetPwdRes struct{} + +// AdminDeleteReq 删除管理员(软删除)。 +type AdminDeleteReq struct { + g.Meta `path:"/admin/delete/{id}" method:"post" tags:"Admin/Admin" summary:"删除管理员"` + Id uint64 `json:"id" in:"path" v:"required"` +} + +// AdminDeleteRes 是 AdminDeleteReq 的响应。 +type AdminDeleteRes struct{} diff --git a/api/admin/system/menu/menu.go b/api/admin/system/menu/menu.go new file mode 100644 index 0000000..d8f86e0 --- /dev/null +++ b/api/admin/system/menu/menu.go @@ -0,0 +1,33 @@ +// Package admin_system_menu 定义菜单路由接口(当前管理员可见菜单树),路由前缀 /admin。 +package admin_system_menu + +import "github.com/gogf/gf/v2/frame/g" + +// RouteMeta 与 vben 后台动态路由元数据对应。 +type RouteMeta struct { + Title string `json:"title"` + Icon string `json:"icon"` + Order int `json:"order"` + Authority []string `json:"authority,omitempty"` // 角色码 + HideInMenu bool `json:"hideInMenu,omitempty"` + KeepAlive bool `json:"keepAlive,omitempty"` +} + +// RouteItem vben 路由树中的一个节点。 +type RouteItem struct { + Name string `json:"name"` + Path string `json:"path"` + Component string `json:"component,omitempty"` + Meta RouteMeta `json:"meta"` + Children []*RouteItem `json:"children,omitempty"` +} + +// MenuRoutesReq 获取当前管理员的菜单树(后端鉴权模式,登录后即可访问)。 +type MenuRoutesReq struct { + g.Meta `path:"/system/menu/routes" method:"post" tags:"Admin/System/Menu" summary:"当前管理员菜单路由"` +} + +// MenuRoutesRes 是 MenuRoutesReq 的响应。 +type MenuRoutesRes struct { + Routes []*RouteItem `json:"routes"` +} diff --git a/api/admin/system/menu_manage/menu_manage.go b/api/admin/system/menu_manage/menu_manage.go new file mode 100644 index 0000000..56c2ba3 --- /dev/null +++ b/api/admin/system/menu_manage/menu_manage.go @@ -0,0 +1,78 @@ +// Package admin_system_menu_manage 定义菜单管理接口(菜单 + 按钮权限树),路由前缀 /admin。 +package admin_system_menu_manage + +import "github.com/gogf/gf/v2/frame/g" + +// MenuItem 完整菜单树中的一个节点(type 1 菜单 / 2 按钮)。 +type MenuItem struct { + Id uint64 `json:"id"` + ParentId uint64 `json:"parentId"` + Name string `json:"name"` + Icon string `json:"icon"` + Type int `json:"type"` // 1 菜单, 2 按钮/接口 + Path string `json:"path"` + Component string `json:"component"` + Permission string `json:"permission"` + Sort int `json:"sort"` + Status int `json:"status"` + Hidden bool `json:"hidden"` + Children []*MenuItem `json:"children,omitempty"` +} + +// MenuTreeReq 获取完整菜单树(供管理)。 +type MenuTreeReq struct { + g.Meta `path:"/system/menu/tree" method:"post" tags:"Admin/System/Menu" summary:"完整菜单树"` +} + +// MenuTreeRes 是 MenuTreeReq 的响应。 +type MenuTreeRes struct { + Tree []*MenuItem `json:"tree"` +} + +// MenuCreateReq 创建菜单或按钮节点。 +type MenuCreateReq struct { + g.Meta `path:"/system/menu/create" method:"post" tags:"Admin/System/Menu" summary:"创建菜单"` + ParentId uint64 `json:"parentId"` + Name string `json:"name" v:"required"` + Icon string `json:"icon"` + Type int `json:"type" v:"in:1,2#type must be 1 or 2"` + Path string `json:"path"` + Component string `json:"component"` + Permission string `json:"permission" v:"required"` + Sort int `json:"sort"` + Status int `json:"status"` + Hidden bool `json:"hidden"` +} + +// MenuCreateRes 是 MenuCreateReq 的响应。 +type MenuCreateRes struct { + Id uint64 `json:"id"` +} + +// MenuUpdateReq 更新菜单或按钮节点。 +type MenuUpdateReq struct { + g.Meta `path:"/system/menu/update/{id}" method:"post" tags:"Admin/System/Menu" summary:"更新菜单"` + Id uint64 `json:"id" in:"path" v:"required"` + ParentId uint64 `json:"parentId"` + Name string `json:"name" v:"required"` + Icon string `json:"icon"` + Type int `json:"type" v:"in:1,2#type must be 1 or 2"` + Path string `json:"path"` + Component string `json:"component"` + Permission string `json:"permission" v:"required"` + Sort int `json:"sort"` + Status int `json:"status"` + Hidden bool `json:"hidden"` +} + +// MenuUpdateRes 是 MenuUpdateReq 的响应。 +type MenuUpdateRes struct{} + +// MenuDeleteReq 删除菜单节点(软删除)。 +type MenuDeleteReq struct { + g.Meta `path:"/system/menu/delete/{id}" method:"post" tags:"Admin/System/Menu" summary:"删除菜单"` + Id uint64 `json:"id" in:"path" v:"required"` +} + +// MenuDeleteRes 是 MenuDeleteReq 的响应。 +type MenuDeleteRes struct{} diff --git a/api/admin/system/role/role.go b/api/admin/system/role/role.go new file mode 100644 index 0000000..8258f80 --- /dev/null +++ b/api/admin/system/role/role.go @@ -0,0 +1,63 @@ +// Package admin_system_role 定义角色管理接口(角色及其菜单绑定),路由前缀 /admin。 +package admin_system_role + +import "github.com/gogf/gf/v2/frame/g" + +// RoleItem 角色列表中的一行。 +type RoleItem struct { + Id uint64 `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Status int `json:"status"` + MenuIds []uint64 `json:"menuIds"` // 已绑定的菜单 id(编辑用) + CreatedAt string `json:"createdAt"` +} + +// RoleListReq 分页查询角色。 +type RoleListReq struct { + g.Meta `path:"/system/role/list" method:"post" tags:"Admin/System/Role" summary:"角色列表"` + Page int `json:"page" d:"1" v:"min:1"` + Size int `json:"size" d:"10" v:"min:1|max:100"` + Keyword string `json:"keyword"` // 匹配角色码或名称 +} + +// RoleListRes 是 RoleListReq 的响应。 +type RoleListRes struct { + List []*RoleItem `json:"list"` + Total int `json:"total"` +} + +// RoleCreateReq 创建角色并绑定菜单。 +type RoleCreateReq struct { + g.Meta `path:"/system/role/create" method:"post" tags:"Admin/System/Role" summary:"创建角色"` + Code string `json:"code" v:"required"` + Name string `json:"name" v:"required"` + Status int `json:"status"` + MenuIds []uint64 `json:"menuIds"` +} + +// RoleCreateRes 是 RoleCreateReq 的响应。 +type RoleCreateRes struct { + Id uint64 `json:"id"` +} + +// RoleUpdateReq 更新角色并重新绑定菜单。 +type RoleUpdateReq struct { + g.Meta `path:"/system/role/update/{id}" method:"post" tags:"Admin/System/Role" summary:"更新角色"` + Id uint64 `json:"id" in:"path" v:"required"` + Name string `json:"name" v:"required"` + Status int `json:"status"` + MenuIds []uint64 `json:"menuIds"` +} + +// RoleUpdateRes 是 RoleUpdateReq 的响应。 +type RoleUpdateRes struct{} + +// RoleDeleteReq 删除角色(软删除)。 +type RoleDeleteReq struct { + g.Meta `path:"/system/role/delete/{id}" method:"post" tags:"Admin/System/Role" summary:"删除角色"` + Id uint64 `json:"id" in:"path" v:"required"` +} + +// RoleDeleteRes 是 RoleDeleteReq 的响应。 +type RoleDeleteRes struct{} diff --git a/api/open/tools/doc.go b/api/open/tools/doc.go new file mode 100644 index 0000000..636146f --- /dev/null +++ b/api/open/tools/doc.go @@ -0,0 +1,15 @@ +// Package open_tools 承载开放工具接口(GET/POST /tools/*)。 +// +// 命名规则:tools/ 下每个子功能一个独立目录,目录名即包名;Req/Res 契约放在 +// 目录内以功能命名的文件中,例如: +// +// api/open/tools/uuid/uuid.go - POST /tools/uuid +// api/open/tools/md5/md5.go - POST /tools/md5 +// api/open/tools/random/random.go - POST /tools/random +// api/open/tools/time/time.go - POST /tools/time +// api/open/tools/ip/ip.go - POST /tools/ip +// +// 功能增长时,在其目录内新增文件(如 ocr/ 下加 idcard.go、invoice.go), +// 而不是把起始文件写大。新增功能:创建 tools//.go 并在 +// internal/controller/open/.go 中增加对应方法,路由自动绑定。 +package open_tools diff --git a/api/open/tools/ip/ip.go b/api/open/tools/ip/ip.go new file mode 100644 index 0000000..6c73bfa --- /dev/null +++ b/api/open/tools/ip/ip.go @@ -0,0 +1,15 @@ +// Package open_tools_ip 定义 POST /tools/ip 接口契约。 +package open_tools_ip + +import "github.com/gogf/gf/v2/frame/g" + +// IPReq 获取调用方 IP 信息。 +type IPReq struct { + g.Meta `path:"/tools/ip" method:"post" tags:"Open/Tools" summary:"客户端 IP 信息"` +} + +// IPRes 是 IPReq 的响应。 +type IPRes struct { + IP string `json:"ip"` + Internal bool `json:"internal"` // 是否为内网/私网地址 +} diff --git a/api/open/tools/md5/md5.go b/api/open/tools/md5/md5.go new file mode 100644 index 0000000..62b5c1a --- /dev/null +++ b/api/open/tools/md5/md5.go @@ -0,0 +1,15 @@ +// Package open_tools_md5 定义 POST /tools/md5 接口契约。 +package open_tools_md5 + +import "github.com/gogf/gf/v2/frame/g" + +// MD5Req 计算文本的 MD5 摘要。 +type MD5Req struct { + g.Meta `path:"/tools/md5" method:"post" tags:"Open/Tools" summary:"计算 MD5 摘要"` + Text string `json:"text" v:"required#text required"` +} + +// MD5Res 是 MD5Req 的响应。 +type MD5Res struct { + MD5 string `json:"md5"` +} diff --git a/api/open/tools/random/random.go b/api/open/tools/random/random.go new file mode 100644 index 0000000..6845645 --- /dev/null +++ b/api/open/tools/random/random.go @@ -0,0 +1,16 @@ +// Package open_tools_random 定义 POST /tools/random 接口契约。 +package open_tools_random + +import "github.com/gogf/gf/v2/frame/g" + +// RandomReq 生成随机字符串。 +type RandomReq struct { + g.Meta `path:"/tools/random" method:"post" tags:"Open/Tools" summary:"生成随机字符串"` + Length int `json:"length" d:"16" v:"min:1|max:128#length must be 1-128"` + Type string `json:"type" d:"alnum" v:"in:alnum,digits,letters#unsupported type"` +} + +// RandomRes 是 RandomReq 的响应。 +type RandomRes struct { + Value string `json:"value"` +} diff --git a/api/open/tools/time/time.go b/api/open/tools/time/time.go new file mode 100644 index 0000000..a957cb8 --- /dev/null +++ b/api/open/tools/time/time.go @@ -0,0 +1,16 @@ +// Package open_tools_time 定义 POST /tools/time 接口契约。 +package open_tools_time + +import "github.com/gogf/gf/v2/frame/g" + +// TimeReq 获取当前服务器时间。 +type TimeReq struct { + g.Meta `path:"/tools/time" method:"post" tags:"Open/Tools" summary:"当前服务器时间"` +} + +// TimeRes 是 TimeReq 的响应。 +type TimeRes struct { + Timestamp int64 `json:"timestamp"` // Unix 秒 + DateTime string `json:"dateTime"` // 2006-01-02 15:04:05 + Date string `json:"date"` // 2006-01-02 +} diff --git a/api/open/tools/uuid/uuid.go b/api/open/tools/uuid/uuid.go new file mode 100644 index 0000000..6819115 --- /dev/null +++ b/api/open/tools/uuid/uuid.go @@ -0,0 +1,15 @@ +// Package open_tools_uuid 定义 POST /tools/uuid 接口契约。 +package open_tools_uuid + +import "github.com/gogf/gf/v2/frame/g" + +// UUIDReq 生成唯一 ID。 +type UUIDReq struct { + g.Meta `path:"/tools/uuid" method:"post" tags:"Open/Tools" summary:"生成唯一 ID"` + Short bool `json:"short"` // true: 8 位短码;false: 32 位 ID +} + +// UUIDRes 是 UUIDReq 的响应。 +type UUIDRes struct { + UUID string `json:"uuid"` +} diff --git a/api/user/auth/auth.go b/api/user/auth/auth.go new file mode 100644 index 0000000..53001f4 --- /dev/null +++ b/api/user/auth/auth.go @@ -0,0 +1,33 @@ +// Package user_auth 定义用户端认证接口(登录/刷新),路由前缀 /api。 +package user_auth + +import "github.com/gogf/gf/v2/frame/g" + +// LoginReq 用户登录请求(支持微信/手机验证码/账号密码三种方式)。 +type LoginReq struct { + g.Meta `path:"/auth/login" method:"post" tags:"User/Auth" summary:"用户登录"` + LoginType string `json:"loginType" v:"required|in:wechat,mobile,password#login type required|unsupported login type"` + Code string `json:"code"` // 微信授权码 + Mobile string `json:"mobile"` // 手机号(mobile 登录方式) + VerifyCode string `json:"verifyCode"` // 短信验证码 + Account string `json:"account"` // 账号(password 登录方式) + Password string `json:"password"` // 密码 + Terminal string `json:"terminal" v:"required|in:mini,h5,app#terminal required|invalid terminal"` +} + +// LoginRes 用户登录响应。 +type LoginRes struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + ExpiresIn int64 `json:"expiresIn"` + UserID uint64 `json:"userId"` +} + +// RefreshReq 使用刷新令牌轮换用户令牌对。 +type RefreshReq struct { + g.Meta `path:"/auth/refresh" method:"post" tags:"User/Auth" summary:"轮换用户令牌"` + RefreshToken string `json:"refreshToken" v:"required"` +} + +// RefreshRes 是 RefreshReq 的响应。 +type RefreshRes LoginRes diff --git a/internal/service/admin/admin/admin/admin.go b/internal/service/admin/admin/admin/admin.go new file mode 100644 index 0000000..748fc68 --- /dev/null +++ b/internal/service/admin/admin/admin/admin.go @@ -0,0 +1,158 @@ +package admin_admin_admin + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "golang.org/x/crypto/bcrypt" + + "service.xpcool.com/internal/consts" + "service.xpcool.com/internal/dao" + "service.xpcool.com/internal/library/response" + "service.xpcool.com/internal/model/do" + "service.xpcool.com/internal/model/dto" + "service.xpcool.com/internal/model/entity" +) + +// IAdminManage 管理后台管理员账号。 +type IAdminManage interface { + List(context.Context, dto.PageQuery) ([]*dto.AdminItem, int, error) + Create(context.Context, dto.AdminCreateInput) (uint64, error) + Update(context.Context, dto.AdminUpdateInput) error + ResetPassword(context.Context, uint64, string) error + Delete(context.Context, uint64) error +} + +type adminManage struct{} + +var localAdminManage IAdminManage + +func NewAdminManage() IAdminManage { return &adminManage{} } + +func AdminManage() IAdminManage { + if localAdminManage == nil { + panic("AdminManage implementation not registered") + } + return localAdminManage +} +func RegisterAdminManage(i IAdminManage) { localAdminManage = i } + +// ---------------------- Admin manage ---------------------- + +func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminItem, int, error) { + total, err := dao.AdminUser.Ctx(ctx).Count() + if err != nil { + return nil, 0, gerror.Wrap(err, "count admins") + } + if total == 0 { + return nil, 0, nil + } + var list []entity.AdminUser + if err = dao.AdminUser.Ctx(ctx).Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil { + return nil, 0, gerror.Wrap(err, "query admins") + } + items := make([]*dto.AdminItem, 0, len(list)) + for i := range list { + a := &list[i] + roleIds, roleNames, err := s.roles(ctx, a.Id) + if err != nil { + return nil, 0, err + } + items = append(items, &dto.AdminItem{ + Id: a.Id, Username: a.Username, Nickname: a.Nickname, Status: a.Status, + RoleIds: roleIds, RoleNames: roleNames, + CreatedAt: a.CreatedAt.Layout("2006-01-02 15:04:05"), + }) + } + return items, total, nil +} + +func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []string, error) { + var rels []entity.AdminUserRole + if err := dao.AdminUserRole.Ctx(ctx).Where(do.AdminUserRole{AdminUserId: adminID}).Scan(&rels); err != nil { + return nil, nil, gerror.Wrap(err, "query admin roles") + } + roleIds := make([]uint64, 0, len(rels)) + for _, r := range rels { + roleIds = append(roleIds, r.RoleId) + } + if len(roleIds) == 0 { + return roleIds, nil, nil + } + var roles []entity.AdminRole + if err := dao.AdminRole.Ctx(ctx).WhereIn("id", roleIds).Scan(&roles); err != nil { + return nil, nil, gerror.Wrap(err, "query roles") + } + names := make([]string, 0, len(roles)) + for _, r := range roles { + names = append(names, r.Name) + } + return roleIds, names, nil +} + +func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint64, error) { + count, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Count() + if err != nil { + return 0, gerror.Wrap(err, "check username") + } + if count > 0 { + return 0, response.Error(consts.CodeInvalidParam, "username already exists") + } + hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) + if err != nil { + return 0, gerror.Wrap(err, "hash password") + } + id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{ + Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1, + }).InsertAndGetId() + if err != nil { + return 0, gerror.Wrap(err, "insert admin") + } + if err = bindAdminRoles(ctx, uint64(id), in.RoleIds); err != nil { + return 0, err + } + return uint64(id), nil +} + +func (s *adminManage) Update(ctx context.Context, in dto.AdminUpdateInput) error { + data := do.AdminUser{Nickname: in.Nickname, Status: in.Status} + if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: in.Id}).Data(data).Update(); err != nil { + return gerror.Wrap(err, "update admin") + } + return bindAdminRoles(ctx, in.Id, in.RoleIds) +} + +func (s *adminManage) ResetPassword(ctx context.Context, id uint64, password string) error { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return gerror.Wrap(err, "hash password") + } + if _, err = dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Data(do.AdminUser{PasswordHash: string(hash)}).Update(); err != nil { + return gerror.Wrap(err, "reset password") + } + return nil +} + +func (s *adminManage) Delete(ctx context.Context, id uint64) error { + if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete admin") + } + // 关联关系物理删除,避免唯一键残留。 + if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete admin role bindings") + } + return nil +} + +// bindAdminRoles 全量重绑管理员-角色关系。 +func bindAdminRoles(ctx context.Context, adminID uint64, roleIds []uint64) error { + if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: adminID}).Delete(); err != nil { + return gerror.Wrap(err, "clear admin roles") + } + for _, rid := range roleIds { + if _, err := dao.AdminUserRole.Ctx(ctx).Data(do.AdminUserRole{AdminUserId: adminID, RoleId: rid}).Insert(); err != nil { + return gerror.Wrap(err, "bind admin role") + } + } + return nil +} diff --git a/manifest/sql/004b_fill_seed.sql b/manifest/sql/004b_fill_seed.sql new file mode 100644 index 0000000..7c59ab0 --- /dev/null +++ b/manifest/sql/004b_fill_seed.sql @@ -0,0 +1,43 @@ +-- 004b_fill_seed.sql +-- 补齐 004_seed.sql 中断后的缺失数据(admin_menu 菜单树 + super_admin 全绑定 + admin 用户角色绑定)。 +-- 背景:服务器首次导入 004 时因缺 icon 列在 admin_menu 处中断,仅写入了 admin_user/admin_role; +-- 003 补列后再导入 004 会因 admin_user id=1 已存在而中断。本脚本以 INSERT IGNORE 幂等补齐。 +-- 依赖:003_schema_ext.sql 已执行(icon/component/hidden 列存在);007_menu_permissions_v3.sql 已执行(含 id=63)。 + +-- 1) 菜单树(type=1 菜单 + type=2 按钮,与 004_seed.sql 一致) +INSERT IGNORE INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES + -- 系统管理 + (1, 0, '系统管理', 'mdi:settings-outline', 1, '/system', '', 'system', 1, 1, 0, NOW(), NOW()), + -- 管理员管理 + (2, 1, '管理员管理', 'mdi:account-group-outline', 1, 'admin', 'system/admin/index', 'system:admin', 1, 1, 0, NOW(), NOW()), + (21, 2, '查询', '', 2, '', '', 'system:admin:list', 1, 1, 0, NOW(), NOW()), + (22, 2, '新增', '', 2, '', '', 'system:admin:create', 2, 1, 0, NOW(), NOW()), + (23, 2, '编辑', '', 2, '', '', 'system:admin:update', 3, 1, 0, NOW(), NOW()), + (24, 2, '删除', '', 2, '', '', 'system:admin:delete', 4, 1, 0, NOW(), NOW()), + (25, 2, '重置密码', '', 2, '', '', 'system:admin:resetPwd', 5, 1, 0, NOW(), NOW()), + -- 角色管理 + (3, 1, '角色管理', 'mdi:shield-account-outline', 1, 'role', 'system/role/index', 'system:role', 2, 1, 0, NOW(), NOW()), + (31, 3, '查询', '', 2, '', '', 'system:role:list', 1, 1, 0, NOW(), NOW()), + (32, 3, '新增', '', 2, '', '', 'system:role:create', 2, 1, 0, NOW(), NOW()), + (33, 3, '编辑', '', 2, '', '', 'system:role:update', 3, 1, 0, NOW(), NOW()), + (34, 3, '删除', '', 2, '', '', 'system:role:delete', 4, 1, 0, NOW(), NOW()), + (35, 3, '分配菜单', '', 2, '', '', 'system:role:assignMenu', 5, 1, 0, NOW(), NOW()), + -- 菜单管理 + (4, 1, '菜单管理', 'mdi:menu-outline', 1, 'menu', 'system/menu/index', 'system:menu', 3, 1, 0, NOW(), NOW()), + (41, 4, '查询', '', 2, '', '', 'system:menu:list', 1, 1, 0, NOW(), NOW()), + (42, 4, '新增', '', 2, '', '', 'system:menu:create', 2, 1, 0, NOW(), NOW()), + (43, 4, '编辑', '', 2, '', '', 'system:menu:update', 3, 1, 0, NOW(), NOW()), + (44, 4, '删除', '', 2, '', '', 'system:menu:delete', 4, 1, 0, NOW(), NOW()), + -- 系统监控 + (5, 0, '系统监控', 'mdi:monitor-dashboard', 1, '/monitor', '', 'monitor', 2, 1, 0, NOW(), NOW()), + -- 服务器日志 + (6, 5, '服务器日志', 'mdi:file-document-outline', 1, 'log', 'monitor/log/index', 'monitor:log', 1, 1, 0, NOW(), NOW()), + (61, 6, '查看', '', 2, '', '', 'monitor:log:view', 1, 1, 0, NOW(), NOW()), + (62, 6, '实时监控', '', 2, '', '', 'monitor:log:tail', 2, 1, 0, NOW(), NOW()); + +-- 2) super_admin 绑定全部菜单(含 007 新增的 63) +INSERT IGNORE INTO admin_role_menu (role_id, menu_id, created_at, updated_at) +SELECT 1, id, NOW(), NOW() FROM admin_menu WHERE deleted_at IS NULL; + +-- 3) admin 用户绑定 super_admin 角色 +INSERT IGNORE INTO admin_user_role (admin_user_id, role_id, created_at, updated_at) VALUES (1, 1, NOW(), NOW()); diff --git a/manifest/sql/007_menu_permissions_v3.sql b/manifest/sql/007_menu_permissions_v3.sql new file mode 100644 index 0000000..b16b008 --- /dev/null +++ b/manifest/sql/007_menu_permissions_v3.sql @@ -0,0 +1,36 @@ +-- 007_menu_permissions_v3.sql +-- 适配 2026-08-26 统一 API 前缀 /api/service + 全 POST 重构后的权限映射。 +-- 背景:005/006 的 path 仍为旧的 /admin/v1 路径,与最新路由不符; +-- 本脚本将 admin_menu type=2 行的 path 全部更新为「METHOD /api/service/admin/...」格式, +-- 并补充系统日志 system:log:list 权限映射(菜单 id=63,挂服务器日志 id=6 下)。 +-- 依赖:先执行 004_seed.sql(表结构已就绪时仅需 004 + 本脚本)。 +-- 幂等性:UPDATE 幂等;INSERT 63 用 INSERT IGNORE,可重复执行。 + +-- ============ 1) 权限路径映射(type=2 按钮行) ============ +-- 管理员管理(父 2) +UPDATE admin_menu SET path='POST /api/service/admin/admin/list' WHERE id=21; -- system:admin:list +UPDATE admin_menu SET path='POST /api/service/admin/admin/create' WHERE id=22; -- system:admin:create +UPDATE admin_menu SET path='POST /api/service/admin/admin/update/{id}' WHERE id=23; -- system:admin:update +UPDATE admin_menu SET path='POST /api/service/admin/admin/delete/{id}' WHERE id=24; -- system:admin:delete +UPDATE admin_menu SET path='POST /api/service/admin/admin/resetPwd/{id}' WHERE id=25; -- system:admin:resetPwd +-- 角色管理(父 3) +UPDATE admin_menu SET path='POST /api/service/admin/system/role/list' WHERE id=31; -- system:role:list +UPDATE admin_menu SET path='POST /api/service/admin/system/role/create' WHERE id=32; -- system:role:create +UPDATE admin_menu SET path='POST /api/service/admin/system/role/update/{id}' WHERE id=33; -- system:role:update +UPDATE admin_menu SET path='POST /api/service/admin/system/role/delete/{id}' WHERE id=34; -- system:role:delete +UPDATE admin_menu SET path='' WHERE id=35; -- system:role:assignMenu(无独立端点) +-- 菜单管理(父 4,查询走 tree 接口) +UPDATE admin_menu SET path='POST /api/service/admin/system/menu/tree' WHERE id=41; -- system:menu:list +UPDATE admin_menu SET path='POST /api/service/admin/system/menu/create' WHERE id=42; -- system:menu:create +UPDATE admin_menu SET path='POST /api/service/admin/system/menu/update/{id}' WHERE id=43; -- system:menu:update +UPDATE admin_menu SET path='POST /api/service/admin/system/menu/delete/{id}' WHERE id=44; -- system:menu:delete +-- 服务器日志(父 6) +UPDATE admin_menu SET path='POST /api/service/admin/base/log/files' WHERE id=61; -- monitor:log:view +UPDATE admin_menu SET path='POST /api/service/admin/base/log/tail' WHERE id=62; -- monitor:log:tail + +-- ============ 2) 系统日志权限(id=63,父 6,system:log:list) ============ +INSERT IGNORE INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) +VALUES (63, 6, '操作日志', '', 2, 'POST /api/service/admin/system/log', '', 'system:log:list', 3, 1, 0, NOW(), NOW()); + +-- super_admin 绑定新权限 +INSERT IGNORE INTO admin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (1, 63, NOW(), NOW()); diff --git a/manifest/sql/008_menu_rbac_v4.sql b/manifest/sql/008_menu_rbac_v4.sql new file mode 100644 index 0000000..7ec8102 --- /dev/null +++ b/manifest/sql/008_menu_rbac_v4.sql @@ -0,0 +1,23 @@ +-- 008_menu_rbac_v4.sql +-- 2026-08-26 RBAC 界面调整: +-- 1) 移除"管理员管理"菜单(id=2 及按钮行 21-25,软删 + 清理角色绑定); +-- 后端 /admin/* 接口与权限码 system:admin:* 保留,仅前端不再出现该菜单。 +-- 2) 系统管理下新增"日志管理"菜单(id=7,type=1,组件 system/log/index); +-- 原按钮行 63(system:log:list,POST /api/service/admin/system/log)改挂到 id=7 下。 +-- 依赖:先执行 004_seed.sql + 007_menu_permissions_v3.sql。 +-- 幂等性:UPDATE/INSERT IGNORE 可重复执行。 + +-- ============ 1) 移除管理员管理菜单 ============ +UPDATE admin_menu SET deleted_at=NOW() WHERE id IN (2,21,22,23,24,25) AND deleted_at IS NULL; +DELETE FROM admin_role_menu WHERE menu_id IN (2,21,22,23,24,25); + +-- ============ 2) 新增日志管理菜单 ============ +INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) +VALUES (7, 1, '日志管理', 'mdi:clipboard-text-outline', 1, 'log', 'system/log/index', 'system:log', 4, 1, 0, NOW(), NOW()) +ON DUPLICATE KEY UPDATE deleted_at=NULL, parent_id=1, name='日志管理', icon='mdi:clipboard-text-outline', path='log', component='system/log/index', permission='system:log', sort=4, status=1, hidden=0; + +-- 原按钮行 63 挂到日志管理下 +UPDATE admin_menu SET parent_id=7, name='操作日志', sort=1, deleted_at=NULL WHERE id=63; + +-- 超管绑定新菜单与按钮 +INSERT IGNORE INTO admin_role_menu (role_id, menu_id, created_at, updated_at) VALUES (1, 7, NOW(), NOW()), (1, 63, NOW(), NOW());