refactor(service): 重构服务层结构并更新导入路径

- 将 admin 相关服务移动到 internal/service/admin 目录下
- 更新控制器中的服务导入路径引用
- 移除已合并的服务文件
- 添加统一工作约定文档
- 更新 API 接口定义的包路径
This commit is contained in:
夏犀麟 2026-08-26 15:40:13 +08:00
parent 061dbde8ce
commit 6e3ef67709
26 changed files with 656 additions and 745 deletions

View File

@ -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. **中文优先**与用户的思考输出交流能中文尽量中文

View File

@ -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"

View File

@ -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"`
}

View File

@ -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{}

View File

@ -1,4 +1,4 @@
package system
package menu
import "github.com/gogf/gf/v2/frame/g"

View File

@ -1,4 +1,4 @@
package system
package menu_manage
import "github.com/gogf/gf/v2/frame/g"

View File

@ -1,4 +1,4 @@
package system
package role
import "github.com/gogf/gf/v2/frame/g"

View File

@ -1,4 +1,4 @@
package v1
package auth
import "github.com/gogf/gf/v2/frame/g"

View File

@ -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())
})

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -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{} }

View File

@ -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()

View File

@ -1,4 +1,4 @@
package service
package menu
import (
"context"

View 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
}

View 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
}

View File

@ -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
}

View File

@ -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 }

View File

@ -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
}

View File

@ -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)