refactor(service): 重构服务层结构并更新导入路径
- 将 admin 相关服务移动到 internal/service/admin 目录下 - 更新控制器中的服务导入路径引用 - 移除已合并的服务文件 - 添加统一工作约定文档 - 更新 API 接口定义的包路径
This commit is contained in:
parent
061dbde8ce
commit
6e3ef67709
@ -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. **中文优先**:与用户的思考、输出、交流,能中文尽量中文。
|
||||
|
||||
@ -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"
|
||||
@ -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"`
|
||||
}
|
||||
@ -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{}
|
||||
@ -1,4 +1,4 @@
|
||||
package system
|
||||
package menu
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package system
|
||||
package menu_manage
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package system
|
||||
package role
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package v1
|
||||
package auth
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
@ -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())
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
158
internal/service/admin/admin/admin.go
Normal file
158
internal/service/admin/admin/admin.go
Normal file
@ -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
|
||||
}
|
||||
@ -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{} }
|
||||
@ -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 "<METHOD> <path>".
|
||||
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()
|
||||
@ -1,4 +1,4 @@
|
||||
package service
|
||||
package menu
|
||||
|
||||
import (
|
||||
"context"
|
||||
131
internal/service/admin/system/menu_manage/menu_manage.go
Normal file
131
internal/service/admin/system/menu_manage/menu_manage.go
Normal file
@ -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
|
||||
}
|
||||
128
internal/service/admin/system/role/role.go
Normal file
128
internal/service/admin/system/role/role.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 "<METHOD> <path>".
|
||||
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 }
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
Loading…
Reference in New Issue
Block a user