From 6e3ef6770916026f1ed4131b5fac2c74458952cb 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 15:40:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(service):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=B1=82=E7=BB=93=E6=9E=84=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 admin 相关服务移动到 internal/service/admin 目录下 - 更新控制器中的服务导入路径引用 - 移除已合并的服务文件 - 添加统一工作约定文档 - 更新 API 接口定义的包路径 --- AGENTS.md | 8 + api/admin/v1/admin/{ => admin}/admin.go | 2 +- api/admin/v1/base/log.go | 36 -- api/admin/v1/system/{ => auth}/auth.go | 17 +- api/admin/v1/system/{ => menu}/menu.go | 2 +- .../system/{ => menu_manage}/menu_manage.go | 2 +- api/admin/v1/system/{ => role}/role.go | 2 +- api/user/v1/{ => auth}/auth.go | 2 +- internal/cmd/cmd.go | 31 +- internal/controller/admin/admin.go | 14 +- internal/controller/admin/auth.go | 25 +- internal/controller/admin/log.go | 20 +- internal/controller/admin/menu.go | 34 +- internal/controller/admin/profile.go | 32 +- internal/controller/admin/role.go | 32 +- internal/controller/user/auth.go | 16 +- internal/service/admin/admin/admin.go | 158 ++++++++ .../system/audit/audit.go} | 24 +- .../system/auth/auth.go} | 59 ++- .../system/menu/menu.go} | 2 +- .../admin/system/menu_manage/menu_manage.go | 131 ++++++ internal/service/admin/system/role/role.go | 128 ++++++ internal/service/admin_rbac.go | 383 ------------------ internal/service/auth.go | 56 --- internal/service/log_manage.go | 167 -------- .../{user_auth.go => user/auth/auth.go} | 18 +- 26 files changed, 656 insertions(+), 745 deletions(-) rename api/admin/v1/admin/{ => admin}/admin.go (98%) delete mode 100644 api/admin/v1/base/log.go rename api/admin/v1/system/{ => auth}/auth.go (66%) rename api/admin/v1/system/{ => menu}/menu.go (98%) rename api/admin/v1/system/{ => menu_manage}/menu_manage.go (99%) rename api/admin/v1/system/{ => role}/role.go (99%) rename api/user/v1/{ => auth}/auth.go (98%) create mode 100644 internal/service/admin/admin/admin.go rename internal/service/{admin_audit.go => admin/system/audit/audit.go} (54%) rename internal/service/{admin_auth.go => admin/system/auth/auth.go} (66%) rename internal/service/{admin_menu.go => admin/system/menu/menu.go} (99%) create mode 100644 internal/service/admin/system/menu_manage/menu_manage.go create mode 100644 internal/service/admin/system/role/role.go delete mode 100644 internal/service/admin_rbac.go delete mode 100644 internal/service/auth.go delete mode 100644 internal/service/log_manage.go rename internal/service/{user_auth.go => user/auth/auth.go} (91%) diff --git a/AGENTS.md b/AGENTS.md index e27541c..11a7072 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,3 +116,11 @@ go test ./... - 配置文件按 `GF_GCFG_FILE` 切换;`manifest/config/config.yaml` 不入库。 - 生产环境强密码:`JWT_SECRET`、数据库口令。 - 变更涉及 API 时同步更新 `api/` 下的 Swagger 元数据注释。 + +## 统一工作约定(xpcool.com 中央规则) + +> 与中央 `xpcool.com/.workbuddy/memory/MEMORY.md` 保持一致;本项目变更流水在 `.workbuddy/memory/CHANGELOG.md`。 + +1. **中文注释**:写/改代码时,在应有处(函数、复杂逻辑、配置项、非显然分支)加中文注释。 +2. **做记录**:每次请求/变更/修复/文档动作,都在本项目 `.workbuddy/memory/CHANGELOG.md` 顶部追加一条,格式 `YYYY-MM-DD | 类型 | 一句话摘要`(类型:REQ/CHG/FIX/DOC/CFG/DEP)。 +3. **中文优先**:与用户的思考、输出、交流,能中文尽量中文。 diff --git a/api/admin/v1/admin/admin.go b/api/admin/v1/admin/admin/admin.go similarity index 98% rename from api/admin/v1/admin/admin.go rename to api/admin/v1/admin/admin/admin.go index 55a17e9..3de38ea 100644 --- a/api/admin/v1/admin/admin.go +++ b/api/admin/v1/admin/admin/admin.go @@ -1,4 +1,4 @@ -// Package admin 定义后台管理接口(管理员账号管理),路由前缀 /admin/v1/admin。 +// Package admin 定义后台管理接口(管理员账号管理),路由前缀 /admin/v1/admin/admin。 package admin import "github.com/gogf/gf/v2/frame/g" diff --git a/api/admin/v1/base/log.go b/api/admin/v1/base/log.go deleted file mode 100644 index f7e5bff..0000000 --- a/api/admin/v1/base/log.go +++ /dev/null @@ -1,36 +0,0 @@ -// Package base 定义基础常规接口(服务器日志监控等),路由前缀 /admin/v1/base。 -package base - -import "github.com/gogf/gf/v2/frame/g" - -// LogFile describes one server log file. -type LogFile struct { - Name string `json:"name"` // file name - Path string `json:"path"` // path relative to the log dir - Size int64 `json:"size"` // bytes - ModTime string `json:"modTime"` // 2006-01-02 15:04:05 -} - -// LogFilesReq lists the server log files. -type LogFilesReq struct { - g.Meta `path:"/base/log/files" method:"get" tags:"Admin/Base/Log" summary:"List server log files"` -} - -// LogFilesRes is the response of LogFilesReq. -type LogFilesRes struct { - Dir string `json:"dir"` - Files []*LogFile `json:"files"` -} - -// LogTailReq reads the tail of a log file with optional keyword filter. -type LogTailReq struct { - g.Meta `path:"/base/log/tail" method:"get" tags:"Admin/Base/Log" summary:"Tail a server log file"` - File string `json:"file" v:"required#file required"` // relative to the log dir; path traversal is rejected - Lines int `json:"lines" d:"200" v:"min:1|max:2000"` - Keyword string `json:"keyword"` // substring filter -} - -// LogTailRes is the response of LogTailReq. -type LogTailRes struct { - Lines []string `json:"lines"` -} diff --git a/api/admin/v1/system/auth.go b/api/admin/v1/system/auth/auth.go similarity index 66% rename from api/admin/v1/system/auth.go rename to api/admin/v1/system/auth/auth.go index b3fdaa1..8607372 100644 --- a/api/admin/v1/system/auth.go +++ b/api/admin/v1/system/auth/auth.go @@ -1,5 +1,5 @@ -// Package system 定义系统管理接口(认证、菜单、角色),路由前缀 /admin/v1/system。 -package system +// Package auth 定义管理端认证接口(登录/资料/权限码/刷新/登出),路由前缀 /admin/v1/system/auth。 +package auth import "github.com/gogf/gf/v2/frame/g" @@ -38,3 +38,16 @@ type CodesReq struct { type CodesRes struct { Codes []string `json:"codes"` } + +// RefreshReq rotates the administrator token pair using a refresh token. +type RefreshReq struct { + g.Meta `path:"/system/auth/refresh" method:"post" tags:"Admin/System/Auth" summary:"Rotate admin token"` + RefreshToken string `json:"refreshToken" v:"required"` +} +type RefreshRes LoginRes + +// LogoutReq ends the administrator session. +type LogoutReq struct { + g.Meta `path:"/system/auth/logout" method:"post" tags:"Admin/System/Auth" summary:"Admin logout"` +} +type LogoutRes struct{} diff --git a/api/admin/v1/system/menu.go b/api/admin/v1/system/menu/menu.go similarity index 98% rename from api/admin/v1/system/menu.go rename to api/admin/v1/system/menu/menu.go index 33acdc6..1c1313e 100644 --- a/api/admin/v1/system/menu.go +++ b/api/admin/v1/system/menu/menu.go @@ -1,4 +1,4 @@ -package system +package menu import "github.com/gogf/gf/v2/frame/g" diff --git a/api/admin/v1/system/menu_manage.go b/api/admin/v1/system/menu_manage/menu_manage.go similarity index 99% rename from api/admin/v1/system/menu_manage.go rename to api/admin/v1/system/menu_manage/menu_manage.go index 3f62650..68721d1 100644 --- a/api/admin/v1/system/menu_manage.go +++ b/api/admin/v1/system/menu_manage/menu_manage.go @@ -1,4 +1,4 @@ -package system +package menu_manage import "github.com/gogf/gf/v2/frame/g" diff --git a/api/admin/v1/system/role.go b/api/admin/v1/system/role/role.go similarity index 99% rename from api/admin/v1/system/role.go rename to api/admin/v1/system/role/role.go index 44d4092..8dad972 100644 --- a/api/admin/v1/system/role.go +++ b/api/admin/v1/system/role/role.go @@ -1,4 +1,4 @@ -package system +package role import "github.com/gogf/gf/v2/frame/g" diff --git a/api/user/v1/auth.go b/api/user/v1/auth/auth.go similarity index 98% rename from api/user/v1/auth.go rename to api/user/v1/auth/auth.go index 04064f6..4a1bbe8 100644 --- a/api/user/v1/auth.go +++ b/api/user/v1/auth/auth.go @@ -1,4 +1,4 @@ -package v1 +package auth import "github.com/gogf/gf/v2/frame/g" diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 2830924..ab86234 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -5,8 +5,8 @@ import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" - "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gcfg" + "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/genv" adminctl "service.xpcool.com/internal/controller/admin" @@ -15,7 +15,14 @@ import ( userctl "service.xpcool.com/internal/controller/user" "service.xpcool.com/internal/library/jwt" "service.xpcool.com/internal/middleware" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/admin/admin" + "service.xpcool.com/internal/service/admin/base/log" + adminaudit "service.xpcool.com/internal/service/admin/system/audit" + adminauth "service.xpcool.com/internal/service/admin/system/auth" + adminmenu "service.xpcool.com/internal/service/admin/system/menu" + "service.xpcool.com/internal/service/admin/system/menu_manage" + "service.xpcool.com/internal/service/admin/system/role" + userauth "service.xpcool.com/internal/service/user/auth" ) // injectEnv 手动把关键环境变量写入配置系统。 @@ -43,14 +50,14 @@ var ( injectEnv(ctx) s := g.Server() tokens := jwt.New(ctx) - service.RegisterUserAuth(service.NewUserAuth(tokens, nil, nil)) - service.RegisterAdminAuth(service.NewAdminAuth(tokens)) - service.RegisterAdminMenu(service.NewAdminMenu()) - service.RegisterAdminManage(service.NewAdminManage()) - service.RegisterRoleManage(service.NewRoleManage()) - service.RegisterMenuManage(service.NewMenuManage()) - service.RegisterLogManage(service.NewLogManage()) - service.RegisterAdminAudit(service.NewAdminAudit()) + userauth.RegisterUserAuth(userauth.NewUserAuth(tokens, nil, nil)) + adminauth.RegisterAdminAuth(adminauth.NewAdminAuth(tokens)) + adminmenu.RegisterAdminMenu(adminmenu.NewAdminMenu()) + admin.RegisterAdminManage(admin.NewAdminManage()) + role.RegisterRoleManage(role.NewRoleManage()) + menu_manage.RegisterMenuManage(menu_manage.NewMenuManage()) + log.RegisterLogManage(log.NewLogManage()) + adminaudit.RegisterAdminAudit(adminaudit.NewAdminAudit()) s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(middleware.Recover, middleware.CORS) group.Middleware(ghttp.MiddlewareHandlerResponse) @@ -78,8 +85,8 @@ var ( group.Group("/", func(protected *ghttp.RouterGroup) { // Permission-protected endpoints: RBAC management, logs, ... // 权限由后端按「方法+路径」自动匹配,无需前端传 X-Permission。 - protected.Middleware(middleware.AdminAuth(tokens, service.AdminAuth().PermissionForPath, service.AdminAuth().HasPermission, func(ctx context.Context, id uint64, permission, method, path, ip, param string, duration, status int) { - service.AdminAudit().Record(ctx, service.AuditEvent{AdminID: id, Permission: permission, Method: method, Path: path, IP: ip, Param: param, DurationMS: duration, StatusCode: status}) + protected.Middleware(middleware.AdminAuth(tokens, adminauth.AdminAuth().PermissionForPath, adminauth.AdminAuth().HasPermission, func(ctx context.Context, id uint64, permission, method, path, ip, param string, duration, status int) { + adminaudit.AdminAudit().Record(ctx, adminaudit.AuditEvent{AdminID: id, Permission: permission, Method: method, Path: path, IP: ip, Param: param, DurationMS: duration, StatusCode: status}) })) protected.Bind(adminctl.New()) }) diff --git a/internal/controller/admin/admin.go b/internal/controller/admin/admin.go index 52c889a..3db16f4 100644 --- a/internal/controller/admin/admin.go +++ b/internal/controller/admin/admin.go @@ -3,14 +3,14 @@ package admin import ( "context" - adminv1 "service.xpcool.com/api/admin/v1/admin" + adminv1 "service.xpcool.com/api/admin/v1/admin/admin" "service.xpcool.com/internal/model/dto" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/admin/admin" ) // AdminList pages the administrators. func (c *Controller) AdminList(ctx context.Context, req *adminv1.AdminListReq) (res *adminv1.AdminListRes, err error) { - items, total, err := service.AdminManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword}) + items, total, err := admin.AdminManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword}) if err != nil { return nil, err } @@ -26,7 +26,7 @@ func (c *Controller) AdminList(ctx context.Context, req *adminv1.AdminListReq) ( // AdminCreate creates an administrator. func (c *Controller) AdminCreate(ctx context.Context, req *adminv1.AdminCreateReq) (res *adminv1.AdminCreateRes, err error) { - id, err := service.AdminManage().Create(ctx, dto.AdminCreateInput{ + id, err := admin.AdminManage().Create(ctx, dto.AdminCreateInput{ Username: req.Username, Password: req.Password, Nickname: req.Nickname, RoleIds: req.RoleIds, }) if err != nil { @@ -37,7 +37,7 @@ func (c *Controller) AdminCreate(ctx context.Context, req *adminv1.AdminCreateRe // AdminUpdate updates an administrator. func (c *Controller) AdminUpdate(ctx context.Context, req *adminv1.AdminUpdateReq) (res *adminv1.AdminUpdateRes, err error) { - if err = service.AdminManage().Update(ctx, dto.AdminUpdateInput{Id: req.Id, Nickname: req.Nickname, Status: req.Status, RoleIds: req.RoleIds}); err != nil { + if err = admin.AdminManage().Update(ctx, dto.AdminUpdateInput{Id: req.Id, Nickname: req.Nickname, Status: req.Status, RoleIds: req.RoleIds}); err != nil { return nil, err } return &adminv1.AdminUpdateRes{}, nil @@ -45,7 +45,7 @@ func (c *Controller) AdminUpdate(ctx context.Context, req *adminv1.AdminUpdateRe // AdminResetPwd resets an administrator's password. func (c *Controller) AdminResetPwd(ctx context.Context, req *adminv1.AdminResetPwdReq) (res *adminv1.AdminResetPwdRes, err error) { - if err = service.AdminManage().ResetPassword(ctx, req.Id, req.Password); err != nil { + if err = admin.AdminManage().ResetPassword(ctx, req.Id, req.Password); err != nil { return nil, err } return &adminv1.AdminResetPwdRes{}, nil @@ -53,7 +53,7 @@ func (c *Controller) AdminResetPwd(ctx context.Context, req *adminv1.AdminResetP // AdminDelete deletes an administrator. func (c *Controller) AdminDelete(ctx context.Context, req *adminv1.AdminDeleteReq) (res *adminv1.AdminDeleteRes, err error) { - if err = service.AdminManage().Delete(ctx, req.Id); err != nil { + if err = admin.AdminManage().Delete(ctx, req.Id); err != nil { return nil, err } return &adminv1.AdminDeleteRes{}, nil diff --git a/internal/controller/admin/auth.go b/internal/controller/admin/auth.go index 7861a49..213a0a9 100644 --- a/internal/controller/admin/auth.go +++ b/internal/controller/admin/auth.go @@ -3,9 +3,9 @@ package admin import ( "context" - systemv1 "service.xpcool.com/api/admin/v1/system" + authv1 "service.xpcool.com/api/admin/v1/system/auth" "service.xpcool.com/internal/model/dto" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/admin/system/auth" ) // AuthController exposes only the public login endpoint. @@ -15,10 +15,25 @@ type AuthController struct{} func NewAuth() *AuthController { return &AuthController{} } // Login authenticates an administrator and issues a token pair. -func (c *AuthController) Login(ctx context.Context, req *systemv1.LoginReq) (res *systemv1.LoginRes, err error) { - p, id, err := service.AdminAuth().Login(ctx, dto.AdminLoginInput{Username: req.Username, Password: req.Password}) +func (c *AuthController) Login(ctx context.Context, req *authv1.LoginReq) (res *authv1.LoginRes, err error) { + p, id, err := auth.AdminAuth().Login(ctx, dto.AdminLoginInput{Username: req.Username, Password: req.Password}) if err != nil { return nil, err } - return &systemv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil + return &authv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil +} + +// Refresh rotates the administrator token pair using a valid refresh token. +func (c *AuthController) Refresh(ctx context.Context, req *authv1.RefreshReq) (res *authv1.RefreshRes, err error) { + p, id, err := auth.AdminAuth().Refresh(ctx, req.RefreshToken) + if err != nil { + return nil, err + } + return &authv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil +} + +// Logout ends the administrator session. Stateless JWT logout relies on the +// client discarding tokens; the endpoint always succeeds for an authenticated admin. +func (c *AuthController) Logout(ctx context.Context, req *authv1.LogoutReq) (res *authv1.LogoutRes, err error) { + return &authv1.LogoutRes{}, nil } diff --git a/internal/controller/admin/log.go b/internal/controller/admin/log.go index 4330581..11a7835 100644 --- a/internal/controller/admin/log.go +++ b/internal/controller/admin/log.go @@ -3,28 +3,28 @@ package admin import ( "context" - basev1 "service.xpcool.com/api/admin/v1/base" - "service.xpcool.com/internal/service" + logv1 "service.xpcool.com/api/admin/v1/base/log" + "service.xpcool.com/internal/service/admin/base/log" ) // LogFiles lists the server log files. -func (c *Controller) LogFiles(ctx context.Context, req *basev1.LogFilesReq) (res *basev1.LogFilesRes, err error) { - dir, files, err := service.LogManage().Files(ctx) +func (c *Controller) LogFiles(ctx context.Context, req *logv1.LogFilesReq) (res *logv1.LogFilesRes, err error) { + dir, files, err := log.LogManage().Files(ctx) if err != nil { return nil, err } - list := make([]*basev1.LogFile, 0, len(files)) + list := make([]*logv1.LogFile, 0, len(files)) for _, f := range files { - list = append(list, &basev1.LogFile{Name: f.Name, Path: f.Path, Size: f.Size, ModTime: f.ModTime}) + list = append(list, &logv1.LogFile{Name: f.Name, Path: f.Path, Size: f.Size, ModTime: f.ModTime}) } - return &basev1.LogFilesRes{Dir: dir, Files: list}, nil + return &logv1.LogFilesRes{Dir: dir, Files: list}, nil } // LogTail reads the tail of a log file with optional keyword filter. -func (c *Controller) LogTail(ctx context.Context, req *basev1.LogTailReq) (res *basev1.LogTailRes, err error) { - lines, err := service.LogManage().Tail(ctx, req.File, req.Lines, req.Keyword) +func (c *Controller) LogTail(ctx context.Context, req *logv1.LogTailReq) (res *logv1.LogTailRes, err error) { + lines, err := log.LogManage().Tail(ctx, req.File, req.Lines, req.Keyword) if err != nil { return nil, err } - return &basev1.LogTailRes{Lines: lines}, nil + return &logv1.LogTailRes{Lines: lines}, nil } diff --git a/internal/controller/admin/menu.go b/internal/controller/admin/menu.go index 6e5135c..6d97b0b 100644 --- a/internal/controller/admin/menu.go +++ b/internal/controller/admin/menu.go @@ -3,57 +3,57 @@ package admin import ( "context" - systemv1 "service.xpcool.com/api/admin/v1/system" + menuv1 "service.xpcool.com/api/admin/v1/system/menu_manage" "service.xpcool.com/internal/model/dto" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/admin/system/menu_manage" ) // MenuTree returns the full menu tree (menus + button permissions). -func (c *Controller) MenuTree(ctx context.Context, req *systemv1.MenuTreeReq) (res *systemv1.MenuTreeRes, err error) { - tree, err := service.MenuManage().Tree(ctx) +func (c *Controller) MenuTree(ctx context.Context, req *menuv1.MenuTreeReq) (res *menuv1.MenuTreeRes, err error) { + tree, err := menu_manage.MenuManage().Tree(ctx) if err != nil { return nil, err } - out := make([]*systemv1.MenuItem, 0, len(tree)) + out := make([]*menuv1.MenuItem, 0, len(tree)) for _, n := range tree { out = append(out, menuNodeToV1(n)) } - return &systemv1.MenuTreeRes{Tree: out}, nil + return &menuv1.MenuTreeRes{Tree: out}, nil } // MenuCreate creates a menu or button node. -func (c *Controller) MenuCreate(ctx context.Context, req *systemv1.MenuCreateReq) (res *systemv1.MenuCreateRes, err error) { - id, err := service.MenuManage().Create(ctx, dto.MenuCreateInput{ +func (c *Controller) MenuCreate(ctx context.Context, req *menuv1.MenuCreateReq) (res *menuv1.MenuCreateRes, err error) { + id, err := menu_manage.MenuManage().Create(ctx, dto.MenuCreateInput{ ParentId: req.ParentId, Name: req.Name, Icon: req.Icon, Type: req.Type, Path: req.Path, Component: req.Component, Permission: req.Permission, Sort: req.Sort, Status: req.Status, Hidden: req.Hidden, }) if err != nil { return nil, err } - return &systemv1.MenuCreateRes{Id: id}, nil + return &menuv1.MenuCreateRes{Id: id}, nil } // MenuUpdate updates a menu or button node. -func (c *Controller) MenuUpdate(ctx context.Context, req *systemv1.MenuUpdateReq) (res *systemv1.MenuUpdateRes, err error) { - if err = service.MenuManage().Update(ctx, dto.MenuUpdateInput{ +func (c *Controller) MenuUpdate(ctx context.Context, req *menuv1.MenuUpdateReq) (res *menuv1.MenuUpdateRes, err error) { + if err = menu_manage.MenuManage().Update(ctx, dto.MenuUpdateInput{ Id: req.Id, ParentId: req.ParentId, Name: req.Name, Icon: req.Icon, Type: req.Type, Path: req.Path, Component: req.Component, Permission: req.Permission, Sort: req.Sort, Status: req.Status, Hidden: req.Hidden, }); err != nil { return nil, err } - return &systemv1.MenuUpdateRes{}, nil + return &menuv1.MenuUpdateRes{}, nil } // MenuDelete deletes a menu node. -func (c *Controller) MenuDelete(ctx context.Context, req *systemv1.MenuDeleteReq) (res *systemv1.MenuDeleteRes, err error) { - if err = service.MenuManage().Delete(ctx, req.Id); err != nil { +func (c *Controller) MenuDelete(ctx context.Context, req *menuv1.MenuDeleteReq) (res *menuv1.MenuDeleteRes, err error) { + if err = menu_manage.MenuManage().Delete(ctx, req.Id); err != nil { return nil, err } - return &systemv1.MenuDeleteRes{}, nil + return &menuv1.MenuDeleteRes{}, nil } -func menuNodeToV1(n *dto.MenuNode) *systemv1.MenuItem { - item := &systemv1.MenuItem{ +func menuNodeToV1(n *dto.MenuNode) *menuv1.MenuItem { + item := &menuv1.MenuItem{ Id: n.Id, ParentId: n.ParentId, Name: n.Name, Icon: n.Icon, Type: n.Type, Path: n.Path, Component: n.Component, Permission: n.Permission, Sort: n.Sort, Status: n.Status, Hidden: n.Hidden, } diff --git a/internal/controller/admin/profile.go b/internal/controller/admin/profile.go index 9128d0f..e660dfa 100644 --- a/internal/controller/admin/profile.go +++ b/internal/controller/admin/profile.go @@ -3,9 +3,11 @@ package admin import ( "context" - systemv1 "service.xpcool.com/api/admin/v1/system" + authv1 "service.xpcool.com/api/admin/v1/system/auth" + menuv1 "service.xpcool.com/api/admin/v1/system/menu" "service.xpcool.com/internal/model/dto" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/admin/system/auth" + "service.xpcool.com/internal/service/admin/system/menu" ) // ProfileController exposes the logged-in admin's own profile endpoints @@ -16,43 +18,43 @@ type ProfileController struct{} func NewProfile() *ProfileController { return &ProfileController{} } // Info returns the current administrator profile. -func (c *ProfileController) Info(ctx context.Context, req *systemv1.InfoReq) (res *systemv1.InfoRes, err error) { - info, err := service.AdminAuth().Info(ctx, adminID(ctx)) +func (c *ProfileController) Info(ctx context.Context, req *authv1.InfoReq) (res *authv1.InfoRes, err error) { + info, err := auth.AdminAuth().Info(ctx, adminID(ctx)) if err != nil { return nil, err } - return &systemv1.InfoRes{AdminID: info.AdminID, Username: info.Username, Nickname: info.Nickname, Roles: info.Roles}, nil + return &authv1.InfoRes{AdminID: info.AdminID, Username: info.Username, Nickname: info.Nickname, Roles: info.Roles}, nil } // Codes returns the button-level permission codes of the current administrator. -func (c *ProfileController) Codes(ctx context.Context, req *systemv1.CodesReq) (res *systemv1.CodesRes, err error) { - codes, err := service.AdminAuth().Codes(ctx, adminID(ctx)) +func (c *ProfileController) Codes(ctx context.Context, req *authv1.CodesReq) (res *authv1.CodesRes, err error) { + codes, err := auth.AdminAuth().Codes(ctx, adminID(ctx)) if err != nil { return nil, err } - return &systemv1.CodesRes{Codes: codes}, nil + return &authv1.CodesRes{Codes: codes}, nil } // Routes returns the current administrator's visible menu tree as vben routes. -func (c *ProfileController) Routes(ctx context.Context, req *systemv1.MenuRoutesReq) (res *systemv1.MenuRoutesRes, err error) { - routes, err := service.AdminMenu().Routes(ctx, adminID(ctx)) +func (c *ProfileController) Routes(ctx context.Context, req *menuv1.MenuRoutesReq) (res *menuv1.MenuRoutesRes, err error) { + routes, err := menu.AdminMenu().Routes(ctx, adminID(ctx)) if err != nil { return nil, err } - out := make([]*systemv1.RouteItem, 0, len(routes)) + out := make([]*menuv1.RouteItem, 0, len(routes)) for _, r := range routes { out = append(out, toV1Route(r)) } - return &systemv1.MenuRoutesRes{Routes: out}, nil + return &menuv1.MenuRoutesRes{Routes: out}, nil } // toV1Route converts a dto route tree into the v1 API shape. -func toV1Route(r *dto.RouteItem) *systemv1.RouteItem { - item := &systemv1.RouteItem{ +func toV1Route(r *dto.RouteItem) *menuv1.RouteItem { + item := &menuv1.RouteItem{ Name: r.Name, Path: r.Path, Component: r.Component, - Meta: systemv1.RouteMeta{ + Meta: menuv1.RouteMeta{ Title: r.Meta.Title, Icon: r.Meta.Icon, Order: r.Meta.Order, diff --git a/internal/controller/admin/role.go b/internal/controller/admin/role.go index 56aed02..4327970 100644 --- a/internal/controller/admin/role.go +++ b/internal/controller/admin/role.go @@ -3,47 +3,47 @@ package admin import ( "context" - systemv1 "service.xpcool.com/api/admin/v1/system" + rolev1 "service.xpcool.com/api/admin/v1/system/role" "service.xpcool.com/internal/model/dto" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/admin/system/role" ) // RoleList pages the roles. -func (c *Controller) RoleList(ctx context.Context, req *systemv1.RoleListReq) (res *systemv1.RoleListRes, err error) { - items, total, err := service.RoleManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword}) +func (c *Controller) RoleList(ctx context.Context, req *rolev1.RoleListReq) (res *rolev1.RoleListRes, err error) { + items, total, err := role.RoleManage().List(ctx, dto.PageQuery{Page: req.Page, Size: req.Size, Keyword: req.Keyword}) if err != nil { return nil, err } - list := make([]*systemv1.RoleItem, 0, len(items)) + list := make([]*rolev1.RoleItem, 0, len(items)) for _, it := range items { - list = append(list, &systemv1.RoleItem{ + list = append(list, &rolev1.RoleItem{ Id: it.Id, Code: it.Code, Name: it.Name, Status: it.Status, MenuIds: it.MenuIds, CreatedAt: it.CreatedAt, }) } - return &systemv1.RoleListRes{List: list, Total: total}, nil + return &rolev1.RoleListRes{List: list, Total: total}, nil } // RoleCreate creates a role. -func (c *Controller) RoleCreate(ctx context.Context, req *systemv1.RoleCreateReq) (res *systemv1.RoleCreateRes, err error) { - id, err := service.RoleManage().Create(ctx, dto.RoleCreateInput{Code: req.Code, Name: req.Name, Status: req.Status, MenuIds: req.MenuIds}) +func (c *Controller) RoleCreate(ctx context.Context, req *rolev1.RoleCreateReq) (res *rolev1.RoleCreateRes, err error) { + id, err := role.RoleManage().Create(ctx, dto.RoleCreateInput{Code: req.Code, Name: req.Name, Status: req.Status, MenuIds: req.MenuIds}) if err != nil { return nil, err } - return &systemv1.RoleCreateRes{Id: id}, nil + return &rolev1.RoleCreateRes{Id: id}, nil } // RoleUpdate updates a role. -func (c *Controller) RoleUpdate(ctx context.Context, req *systemv1.RoleUpdateReq) (res *systemv1.RoleUpdateRes, err error) { - if err = service.RoleManage().Update(ctx, dto.RoleUpdateInput{Id: req.Id, Name: req.Name, Status: req.Status, MenuIds: req.MenuIds}); err != nil { +func (c *Controller) RoleUpdate(ctx context.Context, req *rolev1.RoleUpdateReq) (res *rolev1.RoleUpdateRes, err error) { + if err = role.RoleManage().Update(ctx, dto.RoleUpdateInput{Id: req.Id, Name: req.Name, Status: req.Status, MenuIds: req.MenuIds}); err != nil { return nil, err } - return &systemv1.RoleUpdateRes{}, nil + return &rolev1.RoleUpdateRes{}, nil } // RoleDelete deletes a role. -func (c *Controller) RoleDelete(ctx context.Context, req *systemv1.RoleDeleteReq) (res *systemv1.RoleDeleteRes, err error) { - if err = service.RoleManage().Delete(ctx, req.Id); err != nil { +func (c *Controller) RoleDelete(ctx context.Context, req *rolev1.RoleDeleteReq) (res *rolev1.RoleDeleteRes, err error) { + if err = role.RoleManage().Delete(ctx, req.Id); err != nil { return nil, err } - return &systemv1.RoleDeleteRes{}, nil + return &rolev1.RoleDeleteRes{}, nil } diff --git a/internal/controller/user/auth.go b/internal/controller/user/auth.go index 1ee92d4..6e059fa 100644 --- a/internal/controller/user/auth.go +++ b/internal/controller/user/auth.go @@ -2,25 +2,25 @@ package user import ( "context" - userv1 "service.xpcool.com/api/user/v1" + authv1 "service.xpcool.com/api/user/v1/auth" "service.xpcool.com/internal/model/dto" - "service.xpcool.com/internal/service" + "service.xpcool.com/internal/service/user/auth" ) type Controller struct{} func New() *Controller { return &Controller{} } -func (c *Controller) Login(ctx context.Context, req *userv1.LoginReq) (res *userv1.LoginRes, err error) { - p, id, err := service.UserAuth().Login(ctx, dto.UserLoginInput{LoginType: req.LoginType, Code: req.Code, Mobile: req.Mobile, VerifyCode: req.VerifyCode, Account: req.Account, Password: req.Password, Terminal: req.Terminal}) +func (c *Controller) Login(ctx context.Context, req *authv1.LoginReq) (res *authv1.LoginRes, err error) { + p, id, err := auth.UserAuth().Login(ctx, dto.UserLoginInput{LoginType: req.LoginType, Code: req.Code, Mobile: req.Mobile, VerifyCode: req.VerifyCode, Account: req.Account, Password: req.Password, Terminal: req.Terminal}) if err != nil { return nil, err } - return &userv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil + return &authv1.LoginRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil } -func (c *Controller) Refresh(ctx context.Context, req *userv1.RefreshReq) (res *userv1.RefreshRes, err error) { - p, id, err := service.UserAuth().Refresh(ctx, req.RefreshToken) +func (c *Controller) Refresh(ctx context.Context, req *authv1.RefreshReq) (res *authv1.RefreshRes, err error) { + p, id, err := auth.UserAuth().Refresh(ctx, req.RefreshToken) if err != nil { return nil, err } - return &userv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil + return &authv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, UserID: id}, nil } diff --git a/internal/service/admin/admin/admin.go b/internal/service/admin/admin/admin.go new file mode 100644 index 0000000..2ce5c8e --- /dev/null +++ b/internal/service/admin/admin/admin.go @@ -0,0 +1,158 @@ +package 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/internal/service/admin_audit.go b/internal/service/admin/system/audit/audit.go similarity index 54% rename from internal/service/admin_audit.go rename to internal/service/admin/system/audit/audit.go index 2fd7866..6c20b20 100644 --- a/internal/service/admin_audit.go +++ b/internal/service/admin/system/audit/audit.go @@ -1,4 +1,4 @@ -package service +package audit import ( "context" @@ -7,6 +7,28 @@ import ( "service.xpcool.com/internal/model/do" ) +// AuditEvent 管理端操作审计事件。 +type AuditEvent struct { + AdminID uint64 + Permission, Method, Path, IP, Param string + DurationMS, StatusCode int +} + +// IAdminAudit 管理端操作审计服务接口。 +type IAdminAudit interface { + Record(context.Context, AuditEvent) +} + +var localAdminAudit IAdminAudit + +func AdminAudit() IAdminAudit { + if localAdminAudit == nil { + panic("AdminAudit implementation not registered") + } + return localAdminAudit +} +func RegisterAdminAudit(i IAdminAudit) { localAdminAudit = i } + type adminAudit struct{} func NewAdminAudit() IAdminAudit { return &adminAudit{} } diff --git a/internal/service/admin_auth.go b/internal/service/admin/system/auth/auth.go similarity index 66% rename from internal/service/admin_auth.go rename to internal/service/admin/system/auth/auth.go index 9897757..07c21d0 100644 --- a/internal/service/admin_auth.go +++ b/internal/service/admin/system/auth/auth.go @@ -1,4 +1,4 @@ -package service +package auth import ( "context" @@ -6,6 +6,7 @@ import ( "strings" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" "golang.org/x/crypto/bcrypt" "service.xpcool.com/internal/consts" "service.xpcool.com/internal/dao" @@ -16,6 +17,28 @@ import ( "service.xpcool.com/internal/model/entity" ) +// IAdminAuth 管理端认证服务接口。 +type IAdminAuth interface { + Login(context.Context, dto.AdminLoginInput) (*dto.TokenPair, uint64, error) + Refresh(context.Context, string) (*dto.TokenPair, uint64, error) + HasPermission(context.Context, uint64, string) (bool, error) + Info(context.Context, uint64) (*dto.AdminInfo, error) + Codes(context.Context, uint64) ([]string, error) + // PermissionForPath resolves the permission code required by an endpoint + // from admin_menu (type=2 rows) by matching " ". + PermissionForPath(context.Context, string, string) (string, error) +} + +var localAdminAuth IAdminAuth + +func AdminAuth() IAdminAuth { + if localAdminAuth == nil { + panic("AdminAuth implementation not registered") + } + return localAdminAuth +} +func RegisterAdminAuth(i IAdminAuth) { localAdminAuth = i } + type adminAuth struct{ tokens *jwt.Service } func NewAdminAuth(tokens *jwt.Service) IAdminAuth { return &adminAuth{tokens} } @@ -31,12 +54,42 @@ func (s *adminAuth) Login(ctx context.Context, in dto.AdminLoginInput) (*dto.Tok if a.Status != 1 || bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(in.Password)) != nil { return nil, 0, response.Error(consts.CodeAdminPasswordWrong, "username or password incorrect") } - access, refresh, exp, err := s.tokens.Issue(a.Id, "admin", "") + // 通过 issue 签发令牌对并把刷新令牌 JTI 落库,保证后续可刷新、可撤销。 + return s.issue(ctx, a.Id, "") +} + +// Refresh 用有效的刷新令牌轮换管理员令牌对:校验 → 撤销旧会话 → 签发新对并落库。 +func (s *adminAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair, uint64, error) { + c, err := s.tokens.Parse(refresh, "refresh", "admin") + if err != nil { + return nil, 0, response.Error(consts.CodeUnauthorized, "invalid refresh token") + } + // 单次使用:撤销旧刷新会话,防止令牌被重复使用。 + if _, err = dao.AuthRefreshSession.Ctx(ctx).Where(do.AuthRefreshSession{Jti: c.JTI}).WhereNull("revoked_at").Data(do.AuthRefreshSession{RevokedAt: gtime.Now()}).Update(); err != nil { + return nil, 0, gerror.Wrap(err, "撤销旧刷新令牌失败") + } + return s.issue(ctx, c.Subject, c.Terminal) +} + +// issue 签发令牌对并把刷新令牌的 JTI 落库,支持撤销与设备会话追踪。 +func (s *adminAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) { + access, refresh, exp, err := s.tokens.Issue(id, "admin", terminal) if err != nil { return nil, 0, gerror.Wrap(err, "issue token") } - return &dto.TokenPair{AccessToken: access, RefreshToken: refresh, ExpiresIn: exp}, a.Id, nil + claims, err := s.tokens.Parse(refresh, "refresh", "admin") + if err != nil { + return nil, 0, gerror.Wrap(err, "解析新刷新令牌失败") + } + if _, err = dao.AuthRefreshSession.Ctx(ctx).Data(do.AuthRefreshSession{ + SubjectId: id, Scope: "admin", Terminal: terminal, + Jti: claims.JTI, ExpiredAt: gtime.NewFromTimeStamp(claims.ExpireAt), + }).Insert(); err != nil { + return nil, 0, gerror.Wrap(err, "保存刷新令牌会话失败") + } + return &dto.TokenPair{AccessToken: access, RefreshToken: refresh, ExpiresIn: exp}, id, nil } + func (s *adminAuth) HasPermission(ctx context.Context, adminID uint64, permission string) (bool, error) { // 多角色权限通过管理员-角色-菜单三表关联查询,菜单中的 permission 即接口权限标识。 count, err := dao.AdminUserRole.Ctx(ctx).As("ur").LeftJoin("admin_role_menu rm", "ur.role_id=rm.role_id").LeftJoin("admin_menu m", "rm.menu_id=m.id").Where("ur.admin_user_id", adminID).Where("m.permission", permission).Where("m.status", 1).Count() diff --git a/internal/service/admin_menu.go b/internal/service/admin/system/menu/menu.go similarity index 99% rename from internal/service/admin_menu.go rename to internal/service/admin/system/menu/menu.go index 0e5b19c..f87e447 100644 --- a/internal/service/admin_menu.go +++ b/internal/service/admin/system/menu/menu.go @@ -1,4 +1,4 @@ -package service +package menu import ( "context" diff --git a/internal/service/admin/system/menu_manage/menu_manage.go b/internal/service/admin/system/menu_manage/menu_manage.go new file mode 100644 index 0000000..b3302ad --- /dev/null +++ b/internal/service/admin/system/menu_manage/menu_manage.go @@ -0,0 +1,131 @@ +package menu_manage + +import ( + "context" + "sort" + + "github.com/gogf/gf/v2/errors/gerror" + + "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" +) + +// IMenuManage 管理菜单树(菜单 + 按钮权限)。 +type IMenuManage interface { + Tree(context.Context) ([]*dto.MenuNode, error) + Create(context.Context, dto.MenuCreateInput) (uint64, error) + Update(context.Context, dto.MenuUpdateInput) error + Delete(context.Context, uint64) error +} + +type menuManage struct{} + +var localMenuManage IMenuManage + +func NewMenuManage() IMenuManage { return &menuManage{} } + +func MenuManage() IMenuManage { + if localMenuManage == nil { + panic("MenuManage implementation not registered") + } + return localMenuManage +} +func RegisterMenuManage(i IMenuManage) { localMenuManage = i } + +// ---------------------- Menu manage ---------------------- + +func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) { + var list []entity.AdminMenu + if err := dao.AdminMenu.Ctx(ctx).OrderAsc("sort").Scan(&list); err != nil { + return nil, gerror.Wrap(err, "query menus") + } + byID := make(map[uint64]*dto.MenuNode, len(list)) + for i := range list { + m := &list[i] + byID[m.Id] = &dto.MenuNode{ + Id: m.Id, ParentId: m.ParentId, Name: m.Name, Icon: m.Icon, Type: m.Type, + Path: m.Path, Component: m.Component, Permission: m.Permission, + Sort: m.Sort, Status: m.Status, Hidden: m.Hidden, + } + } + var roots []*dto.MenuNode + for i := range list { + m := &list[i] + node := byID[m.Id] + if m.ParentId == 0 { + roots = append(roots, node) + } else if p, ok := byID[m.ParentId]; ok { + p.Children = append(p.Children, node) + } + } + sort.Slice(roots, func(i, j int) bool { return roots[i].Sort < roots[j].Sort }) + for _, r := range roots { + sortMenuChildren(r) + } + return roots, nil +} + +func sortMenuChildren(n *dto.MenuNode) { + sort.Slice(n.Children, func(i, j int) bool { return n.Children[i].Sort < n.Children[j].Sort }) + for _, c := range n.Children { + sortMenuChildren(c) + } +} + +func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64, error) { + count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count() + if err != nil { + return 0, gerror.Wrap(err, "check menu permission") + } + if count > 0 { + return 0, response.Error(consts.CodeInvalidParam, "permission already exists") + } + id, err := dao.AdminMenu.Ctx(ctx).Data(do.AdminMenu{ + ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, + Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, + }).InsertAndGetId() + if err != nil { + return 0, gerror.Wrap(err, "insert menu") + } + return uint64(id), nil +} + +func (s *menuManage) Update(ctx context.Context, in dto.MenuUpdateInput) error { + count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count() + if err != nil { + return gerror.Wrap(err, "check menu permission") + } + if count > 0 { + return response.Error(consts.CodeInvalidParam, "permission already exists") + } + _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: in.Id}).Data(do.AdminMenu{ + ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, + Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, + }).Update() + if err != nil { + return gerror.Wrap(err, "update menu") + } + return nil +} + +func (s *menuManage) Delete(ctx context.Context, id uint64) error { + // 删除前检查是否存在子节点。 + cnt, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{ParentId: id}).Count() + if err != nil { + return gerror.Wrap(err, "check menu children") + } + if cnt > 0 { + return response.Error(consts.CodeInvalidParam, "delete children first") + } + if _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete menu") + } + if _, err = dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{MenuId: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete role menu bindings") + } + return nil +} diff --git a/internal/service/admin/system/role/role.go b/internal/service/admin/system/role/role.go new file mode 100644 index 0000000..5cb7b97 --- /dev/null +++ b/internal/service/admin/system/role/role.go @@ -0,0 +1,128 @@ +package role + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + + "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" +) + +// IRoleManage 管理角色及其菜单绑定。 +type IRoleManage interface { + List(context.Context, dto.PageQuery) ([]*dto.RoleItem, int, error) + Create(context.Context, dto.RoleCreateInput) (uint64, error) + Update(context.Context, dto.RoleUpdateInput) error + Delete(context.Context, uint64) error +} + +type roleManage struct{} + +var localRoleManage IRoleManage + +func NewRoleManage() IRoleManage { return &roleManage{} } + +func RoleManage() IRoleManage { + if localRoleManage == nil { + panic("RoleManage implementation not registered") + } + return localRoleManage +} +func RegisterRoleManage(i IRoleManage) { localRoleManage = i } + +// ---------------------- Role manage ---------------------- + +func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem, int, error) { + total, err := dao.AdminRole.Ctx(ctx).Count() + if err != nil { + return nil, 0, gerror.Wrap(err, "count roles") + } + if total == 0 { + return nil, 0, nil + } + var list []entity.AdminRole + if err = dao.AdminRole.Ctx(ctx).Page(q.Page, q.Size).OrderAsc("id").Scan(&list); err != nil { + return nil, 0, gerror.Wrap(err, "query roles") + } + items := make([]*dto.RoleItem, 0, len(list)) + for i := range list { + r := &list[i] + menuIds, err := roleMenuIDs(ctx, r.Id) + if err != nil { + return nil, 0, err + } + items = append(items, &dto.RoleItem{ + Id: r.Id, Code: r.Code, Name: r.Name, Status: r.Status, MenuIds: menuIds, + CreatedAt: r.CreatedAt.Layout("2006-01-02 15:04:05"), + }) + } + return items, total, nil +} + +func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64, error) { + count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count() + if err != nil { + return 0, gerror.Wrap(err, "check role code") + } + if count > 0 { + return 0, response.Error(consts.CodeInvalidParam, "role code already exists") + } + id, err := dao.AdminRole.Ctx(ctx).Data(do.AdminRole{Code: in.Code, Name: in.Name, Status: in.Status}).InsertAndGetId() + if err != nil { + return 0, gerror.Wrap(err, "insert role") + } + if err = bindRoleMenus(ctx, uint64(id), in.MenuIds); err != nil { + return 0, err + } + return uint64(id), nil +} + +func (s *roleManage) Update(ctx context.Context, in dto.RoleUpdateInput) error { + if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: in.Id}).Data(do.AdminRole{Name: in.Name, Status: in.Status}).Update(); err != nil { + return gerror.Wrap(err, "update role") + } + return bindRoleMenus(ctx, in.Id, in.MenuIds) +} + +func (s *roleManage) Delete(ctx context.Context, id uint64) error { + if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete role") + } + if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete role menu bindings") + } + if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{RoleId: id}).Delete(); err != nil { + return gerror.Wrap(err, "delete user role bindings") + } + return nil +} + +func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) { + var rels []entity.AdminRoleMenu + if err := dao.AdminRoleMenu.Ctx(ctx).Where(do.AdminRoleMenu{RoleId: roleID}).Scan(&rels); err != nil { + return nil, gerror.Wrap(err, "query role menus") + } + ids := make([]uint64, 0, len(rels)) + for _, r := range rels { + ids = append(ids, r.MenuId) + } + return ids, nil +} + +// bindRoleMenus 全量重绑角色-菜单关系。 +func bindRoleMenus(ctx context.Context, roleID uint64, menuIds []uint64) error { + if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: roleID}).Delete(); err != nil { + return gerror.Wrap(err, "clear role menus") + } + for _, mid := range menuIds { + if _, err := dao.AdminRoleMenu.Ctx(ctx).Data(do.AdminRoleMenu{RoleId: roleID, MenuId: mid}).Insert(); err != nil { + return gerror.Wrap(err, "bind role menu") + } + } + return nil +} diff --git a/internal/service/admin_rbac.go b/internal/service/admin_rbac.go deleted file mode 100644 index e6867b1..0000000 --- a/internal/service/admin_rbac.go +++ /dev/null @@ -1,383 +0,0 @@ -package service - -import ( - "context" - "sort" - - "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 manages administrators. -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 -} - -// IRoleManage manages roles and their menu bindings. -type IRoleManage interface { - List(context.Context, dto.PageQuery) ([]*dto.RoleItem, int, error) - Create(context.Context, dto.RoleCreateInput) (uint64, error) - Update(context.Context, dto.RoleUpdateInput) error - Delete(context.Context, uint64) error -} - -// IMenuManage manages the menu tree (menus + button permissions). -type IMenuManage interface { - Tree(context.Context) ([]*dto.MenuNode, error) - Create(context.Context, dto.MenuCreateInput) (uint64, error) - Update(context.Context, dto.MenuUpdateInput) error - Delete(context.Context, uint64) error -} - -type adminManage struct{} -type roleManage struct{} -type menuManage struct{} - -func NewAdminManage() IAdminManage { return &adminManage{} } -func NewRoleManage() IRoleManage { return &roleManage{} } -func NewMenuManage() IMenuManage { return &menuManage{} } - -var ( - localAdminManage IAdminManage - localRoleManage IRoleManage - localMenuManage IMenuManage -) - -func AdminManage() IAdminManage { - if localAdminManage == nil { - panic("AdminManage implementation not registered") - } - return localAdminManage -} -func RegisterAdminManage(i IAdminManage) { localAdminManage = i } -func RoleManage() IRoleManage { - if localRoleManage == nil { - panic("RoleManage implementation not registered") - } - return localRoleManage -} -func RegisterRoleManage(i IRoleManage) { localRoleManage = i } -func MenuManage() IMenuManage { - if localMenuManage == nil { - panic("MenuManage implementation not registered") - } - return localMenuManage -} -func RegisterMenuManage(i IMenuManage) { localMenuManage = 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 -} - -// ---------------------- Role manage ---------------------- - -func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem, int, error) { - total, err := dao.AdminRole.Ctx(ctx).Count() - if err != nil { - return nil, 0, gerror.Wrap(err, "count roles") - } - if total == 0 { - return nil, 0, nil - } - var list []entity.AdminRole - if err = dao.AdminRole.Ctx(ctx).Page(q.Page, q.Size).OrderAsc("id").Scan(&list); err != nil { - return nil, 0, gerror.Wrap(err, "query roles") - } - items := make([]*dto.RoleItem, 0, len(list)) - for i := range list { - r := &list[i] - menuIds, err := roleMenuIDs(ctx, r.Id) - if err != nil { - return nil, 0, err - } - items = append(items, &dto.RoleItem{ - Id: r.Id, Code: r.Code, Name: r.Name, Status: r.Status, MenuIds: menuIds, - CreatedAt: r.CreatedAt.Layout("2006-01-02 15:04:05"), - }) - } - return items, total, nil -} - -func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64, error) { - count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count() - if err != nil { - return 0, gerror.Wrap(err, "check role code") - } - if count > 0 { - return 0, response.Error(consts.CodeInvalidParam, "role code already exists") - } - id, err := dao.AdminRole.Ctx(ctx).Data(do.AdminRole{Code: in.Code, Name: in.Name, Status: in.Status}).InsertAndGetId() - if err != nil { - return 0, gerror.Wrap(err, "insert role") - } - if err = bindRoleMenus(ctx, uint64(id), in.MenuIds); err != nil { - return 0, err - } - return uint64(id), nil -} - -func (s *roleManage) Update(ctx context.Context, in dto.RoleUpdateInput) error { - if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: in.Id}).Data(do.AdminRole{Name: in.Name, Status: in.Status}).Update(); err != nil { - return gerror.Wrap(err, "update role") - } - return bindRoleMenus(ctx, in.Id, in.MenuIds) -} - -func (s *roleManage) Delete(ctx context.Context, id uint64) error { - if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: id}).Delete(); err != nil { - return gerror.Wrap(err, "delete role") - } - if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: id}).Delete(); err != nil { - return gerror.Wrap(err, "delete role menu bindings") - } - if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{RoleId: id}).Delete(); err != nil { - return gerror.Wrap(err, "delete user role bindings") - } - return nil -} - -func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) { - var rels []entity.AdminRoleMenu - if err := dao.AdminRoleMenu.Ctx(ctx).Where(do.AdminRoleMenu{RoleId: roleID}).Scan(&rels); err != nil { - return nil, gerror.Wrap(err, "query role menus") - } - ids := make([]uint64, 0, len(rels)) - for _, r := range rels { - ids = append(ids, r.MenuId) - } - return ids, nil -} - -// bindRoleMenus 全量重绑角色-菜单关系。 -func bindRoleMenus(ctx context.Context, roleID uint64, menuIds []uint64) error { - if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: roleID}).Delete(); err != nil { - return gerror.Wrap(err, "clear role menus") - } - for _, mid := range menuIds { - if _, err := dao.AdminRoleMenu.Ctx(ctx).Data(do.AdminRoleMenu{RoleId: roleID, MenuId: mid}).Insert(); err != nil { - return gerror.Wrap(err, "bind role menu") - } - } - return nil -} - -// ---------------------- Menu manage ---------------------- - -func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) { - var list []entity.AdminMenu - if err := dao.AdminMenu.Ctx(ctx).OrderAsc("sort").Scan(&list); err != nil { - return nil, gerror.Wrap(err, "query menus") - } - byID := make(map[uint64]*dto.MenuNode, len(list)) - for i := range list { - m := &list[i] - byID[m.Id] = &dto.MenuNode{ - Id: m.Id, ParentId: m.ParentId, Name: m.Name, Icon: m.Icon, Type: m.Type, - Path: m.Path, Component: m.Component, Permission: m.Permission, - Sort: m.Sort, Status: m.Status, Hidden: m.Hidden, - } - } - var roots []*dto.MenuNode - for i := range list { - m := &list[i] - node := byID[m.Id] - if m.ParentId == 0 { - roots = append(roots, node) - } else if p, ok := byID[m.ParentId]; ok { - p.Children = append(p.Children, node) - } - } - sort.Slice(roots, func(i, j int) bool { return roots[i].Sort < roots[j].Sort }) - for _, r := range roots { - sortMenuChildren(r) - } - return roots, nil -} - -func sortMenuChildren(n *dto.MenuNode) { - sort.Slice(n.Children, func(i, j int) bool { return n.Children[i].Sort < n.Children[j].Sort }) - for _, c := range n.Children { - sortMenuChildren(c) - } -} - -func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64, error) { - count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count() - if err != nil { - return 0, gerror.Wrap(err, "check menu permission") - } - if count > 0 { - return 0, response.Error(consts.CodeInvalidParam, "permission already exists") - } - id, err := dao.AdminMenu.Ctx(ctx).Data(do.AdminMenu{ - ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, - Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, - }).InsertAndGetId() - if err != nil { - return 0, gerror.Wrap(err, "insert menu") - } - return uint64(id), nil -} - -func (s *menuManage) Update(ctx context.Context, in dto.MenuUpdateInput) error { - count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count() - if err != nil { - return gerror.Wrap(err, "check menu permission") - } - if count > 0 { - return response.Error(consts.CodeInvalidParam, "permission already exists") - } - _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: in.Id}).Data(do.AdminMenu{ - ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, - Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, - }).Update() - if err != nil { - return gerror.Wrap(err, "update menu") - } - return nil -} - -func (s *menuManage) Delete(ctx context.Context, id uint64) error { - // 删除前检查是否存在子节点。 - cnt, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{ParentId: id}).Count() - if err != nil { - return gerror.Wrap(err, "check menu children") - } - if cnt > 0 { - return response.Error(consts.CodeInvalidParam, "delete children first") - } - if _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: id}).Delete(); err != nil { - return gerror.Wrap(err, "delete menu") - } - if _, err = dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{MenuId: id}).Delete(); err != nil { - return gerror.Wrap(err, "delete role menu bindings") - } - return nil -} diff --git a/internal/service/auth.go b/internal/service/auth.go deleted file mode 100644 index e073661..0000000 --- a/internal/service/auth.go +++ /dev/null @@ -1,56 +0,0 @@ -package service - -import ( - "context" - "service.xpcool.com/internal/model/dto" -) - -type IUserAuth interface { - Login(context.Context, dto.UserLoginInput) (*dto.TokenPair, uint64, error) - Refresh(context.Context, string) (*dto.TokenPair, uint64, error) -} -type IAdminAuth interface { - Login(context.Context, dto.AdminLoginInput) (*dto.TokenPair, uint64, error) - HasPermission(context.Context, uint64, string) (bool, error) - Info(context.Context, uint64) (*dto.AdminInfo, error) - Codes(context.Context, uint64) ([]string, error) - // PermissionForPath resolves the permission code required by an endpoint - // from admin_menu (type=2 rows) by matching " ". - PermissionForPath(context.Context, string, string) (string, error) -} -type AuditEvent struct { - AdminID uint64 - Permission, Method, Path, IP, Param string - DurationMS, StatusCode int -} -type IAdminAudit interface { - Record(context.Context, AuditEvent) -} - -var ( - localUserAuth IUserAuth - localAdminAuth IAdminAuth - localAdminAudit IAdminAudit -) - -func UserAuth() IUserAuth { - if localUserAuth == nil { - panic("UserAuth implementation not registered") - } - return localUserAuth -} -func RegisterUserAuth(i IUserAuth) { localUserAuth = i } -func AdminAuth() IAdminAuth { - if localAdminAuth == nil { - panic("AdminAuth implementation not registered") - } - return localAdminAuth -} -func RegisterAdminAuth(i IAdminAuth) { localAdminAuth = i } -func AdminAudit() IAdminAudit { - if localAdminAudit == nil { - panic("AdminAudit implementation not registered") - } - return localAdminAudit -} -func RegisterAdminAudit(i IAdminAudit) { localAdminAudit = i } diff --git a/internal/service/log_manage.go b/internal/service/log_manage.go deleted file mode 100644 index 60288dd..0000000 --- a/internal/service/log_manage.go +++ /dev/null @@ -1,167 +0,0 @@ -package service - -import ( - "bytes" - "context" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gfile" - - "service.xpcool.com/internal/consts" - "service.xpcool.com/internal/library/response" - "service.xpcool.com/internal/model/dto" -) - -// ILogManage reads server log files for the monitoring page. -type ILogManage interface { - Files(context.Context) (string, []*dto.LogFile, error) - Tail(context.Context, string, int, string) ([]string, error) -} - -type logManage struct{} - -func NewLogManage() ILogManage { return &logManage{} } - -var localLogManage ILogManage - -func LogManage() ILogManage { - if localLogManage == nil { - panic("LogManage implementation not registered") - } - return localLogManage -} -func RegisterLogManage(i ILogManage) { localLogManage = i } - -// logDir returns the configured log directory (config logger.path), default "log". -func logDir(ctx context.Context) string { - dir := g.Cfg().MustGet(ctx, "logger.path").String() - if dir == "" { - dir = "log" - } - return dir -} - -// resolvePath 校验 file 为日志目录内文件,防止路径穿越。 -func resolvePath(ctx context.Context, file string) (string, error) { - dir := logDir(ctx) - absDir, err := filepath.Abs(dir) - if err != nil { - return "", gerror.Wrap(err, "resolve log dir") - } - full := filepath.Join(absDir, filepath.Clean(file)) - if !strings.HasPrefix(full, absDir+string(os.PathSeparator)) && full != absDir { - return "", response.Error(consts.CodeInvalidParam, "invalid log file path") - } - if !gfile.Exists(full) || gfile.IsDir(full) { - return "", response.Error(consts.CodeInvalidParam, "log file not found") - } - return full, nil -} - -func (s *logManage) Files(ctx context.Context) (string, []*dto.LogFile, error) { - dir := logDir(ctx) - if !gfile.Exists(dir) { - return dir, nil, nil - } - paths, err := gfile.ScanDirFile(dir, "*.log", true) - if err != nil { - return "", nil, gerror.Wrap(err, "scan log files") - } - files := make([]*dto.LogFile, 0, len(paths)) - for _, p := range paths { - info, err := os.Stat(p) - if err != nil { - continue - } - files = append(files, &dto.LogFile{ - Name: info.Name(), - Path: strings.TrimPrefix(filepath.ToSlash(p), filepath.ToSlash(dir)+"/"), - Size: info.Size(), - ModTime: info.ModTime().Format("2006-01-02 15:04:05"), - }) - } - sort.Slice(files, func(i, j int) bool { return files[i].ModTime > files[j].ModTime }) - return dir, files, nil -} - -func (s *logManage) Tail(ctx context.Context, file string, lines int, keyword string) ([]string, error) { - full, err := resolvePath(ctx, file) - if err != nil { - return nil, err - } - content, err := tailLines(full, lines) - if err != nil { - return nil, gerror.Wrap(err, "read log tail") - } - if keyword != "" { - var filtered []string - for _, l := range content { - if strings.Contains(l, keyword) { - filtered = append(filtered, l) - } - } - return filtered, nil - } - return content, nil -} - -// tailLines reads the last n lines of a file efficiently (reverse chunk scan). -func tailLines(path string, n int) ([]string, error) { - if n <= 0 { - n = 200 - } - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - info, err := f.Stat() - if err != nil { - return nil, err - } - size := info.Size() - if size == 0 { - return nil, nil - } - const chunkSize = 8 << 10 // 8KB - var ( - offset int64 = size - buf []byte - lines []string - ) - for offset > 0 && len(lines) < n { - readSize := int64(chunkSize) - if offset < readSize { - readSize = offset - } - offset -= readSize - chunk := make([]byte, readSize) - if _, err = f.ReadAt(chunk, offset); err != nil { - return nil, err - } - combined := append(chunk, buf...) - buf = nil - idx := len(combined) - 1 - for idx >= 0 && len(lines) < n { - j := bytes.LastIndexByte(combined[:idx+1], '\n') - if j < 0 { - buf = append([]byte(nil), combined[:idx+1]...) - break - } - lines = append(lines, string(combined[j+1:idx+1])) - idx = j - 1 - } - } - if len(buf) > 0 && len(lines) < n { - lines = append(lines, string(buf)) - } - for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 { - lines[i], lines[j] = lines[j], lines[i] - } - return lines, nil -} diff --git a/internal/service/user_auth.go b/internal/service/user/auth/auth.go similarity index 91% rename from internal/service/user_auth.go rename to internal/service/user/auth/auth.go index 1a6771a..ab6dfef 100644 --- a/internal/service/user_auth.go +++ b/internal/service/user/auth/auth.go @@ -1,4 +1,4 @@ -package service +package auth import ( "context" @@ -14,6 +14,22 @@ import ( "service.xpcool.com/internal/model/entity" ) +// IUserAuth 用户端认证服务接口。 +type IUserAuth interface { + Login(context.Context, dto.UserLoginInput) (*dto.TokenPair, uint64, error) + Refresh(context.Context, string) (*dto.TokenPair, uint64, error) +} + +var localUserAuth IUserAuth + +func UserAuth() IUserAuth { + if localUserAuth == nil { + panic("UserAuth implementation not registered") + } + return localUserAuth +} +func RegisterUserAuth(i IUserAuth) { localUserAuth = i } + // WechatResolver 抽象微信 code 换 openid,避免业务层依赖具体 HTTP 实现。 type WechatResolver interface { OpenID(context.Context, string) (string, error)