From ada3084683a7e173a003958822ce42964e40c374 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Wed, 7 Jan 2026 15:00:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/system/authority_btn.go | 40 ++++++ internal/data/system/authority_btn.go | 71 +++++++++++ internal/service/system/authority_btn.go | 88 +++++++++++++ internal/service/system/captcha.go | 156 +++++++++++++++++++++++ internal/service/system/casbin.go | 127 ++++++++++++++++++ 5 files changed, 482 insertions(+) create mode 100644 internal/biz/system/authority_btn.go create mode 100644 internal/data/system/authority_btn.go create mode 100644 internal/service/system/authority_btn.go create mode 100644 internal/service/system/captcha.go create mode 100644 internal/service/system/casbin.go diff --git a/internal/biz/system/authority_btn.go b/internal/biz/system/authority_btn.go new file mode 100644 index 0000000..59bdc7a --- /dev/null +++ b/internal/biz/system/authority_btn.go @@ -0,0 +1,40 @@ +package system + +import ( + "context" + "errors" +) + +// AuthorityBtnRepo 角色按钮权限仓储接口 +type AuthorityBtnRepo interface { + GetAuthorityBtn(ctx context.Context, authorityId, menuId uint) ([]uint, error) + SetAuthorityBtn(ctx context.Context, authorityId, menuId uint, btnIds []uint) error + CanRemoveAuthorityBtn(ctx context.Context, btnId uint) error +} + +// AuthorityBtnUsecase 角色按钮权限用例 +type AuthorityBtnUsecase struct { + repo AuthorityBtnRepo +} + +// NewAuthorityBtnUsecase 创建角色按钮权限用例 +func NewAuthorityBtnUsecase(repo AuthorityBtnRepo) *AuthorityBtnUsecase { + return &AuthorityBtnUsecase{repo: repo} +} + +// GetAuthorityBtn 获取角色按钮权限 +func (uc *AuthorityBtnUsecase) GetAuthorityBtn(ctx context.Context, authorityId, menuId uint) ([]uint, error) { + return uc.repo.GetAuthorityBtn(ctx, authorityId, menuId) +} + +// SetAuthorityBtn 设置角色按钮权限 +func (uc *AuthorityBtnUsecase) SetAuthorityBtn(ctx context.Context, authorityId, menuId uint, btnIds []uint) error { + return uc.repo.SetAuthorityBtn(ctx, authorityId, menuId, btnIds) +} + +// CanRemoveAuthorityBtn 检查按钮是否可以删除 +func (uc *AuthorityBtnUsecase) CanRemoveAuthorityBtn(ctx context.Context, btnId uint) error { + return uc.repo.CanRemoveAuthorityBtn(ctx, btnId) +} + +var ErrBtnInUse = errors.New("此按钮正在被使用无法删除") diff --git a/internal/data/system/authority_btn.go b/internal/data/system/authority_btn.go new file mode 100644 index 0000000..50561e4 --- /dev/null +++ b/internal/data/system/authority_btn.go @@ -0,0 +1,71 @@ +package system + +import ( + "context" + "errors" + + "kra/internal/biz/system" + "kra/internal/data/model" + + "gorm.io/gorm" +) + +type authorityBtnRepo struct { + db *gorm.DB +} + +// NewAuthorityBtnRepo 创建角色按钮权限仓储 +func NewAuthorityBtnRepo(db *gorm.DB) system.AuthorityBtnRepo { + return &authorityBtnRepo{db: db} +} + +func (r *authorityBtnRepo) GetAuthorityBtn(ctx context.Context, authorityId, menuId uint) ([]uint, error) { + var btns []model.SysAuthorityBtn + if err := r.db.WithContext(ctx). + Where("authority_id = ? AND sys_menu_id = ?", authorityId, menuId). + Find(&btns).Error; err != nil { + return nil, err + } + selected := make([]uint, len(btns)) + for i, b := range btns { + selected[i] = uint(b.SysBaseMenuBtnID) + } + return selected, nil +} + +func (r *authorityBtnRepo) SetAuthorityBtn(ctx context.Context, authorityId, menuId uint, btnIds []uint) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 删除旧权限 + if err := tx.Where("authority_id = ? AND sys_menu_id = ?", authorityId, menuId). + Delete(&model.SysAuthorityBtn{}).Error; err != nil { + return err + } + // 创建新权限 + if len(btnIds) > 0 { + btns := make([]model.SysAuthorityBtn, len(btnIds)) + for i, btnId := range btnIds { + btns[i] = model.SysAuthorityBtn{ + AuthorityID: int64(authorityId), + SysMenuID: int64(menuId), + SysBaseMenuBtnID: int64(btnId), + } + } + if err := tx.Create(&btns).Error; err != nil { + return err + } + } + return nil + }) +} + +func (r *authorityBtnRepo) CanRemoveAuthorityBtn(ctx context.Context, btnId uint) error { + var count int64 + if err := r.db.WithContext(ctx).Model(&model.SysAuthorityBtn{}). + Where("sys_base_menu_btn_id = ?", btnId).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return errors.New("此按钮正在被使用无法删除") + } + return nil +} diff --git a/internal/service/system/authority_btn.go b/internal/service/system/authority_btn.go new file mode 100644 index 0000000..810932d --- /dev/null +++ b/internal/service/system/authority_btn.go @@ -0,0 +1,88 @@ +package system + +import ( + "strconv" + + "kra/internal/biz/system" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// AuthorityBtnService 角色按钮权限服务 +type AuthorityBtnService struct { + uc *system.AuthorityBtnUsecase +} + +// NewAuthorityBtnService 创建角色按钮权限服务 +func NewAuthorityBtnService(uc *system.AuthorityBtnUsecase) *AuthorityBtnService { + return &AuthorityBtnService{uc: uc} +} + +// GetAuthorityBtnRequest 获取角色按钮权限请求 +type GetAuthorityBtnRequest struct { + AuthorityId uint `json:"authorityId"` + MenuID uint `json:"menuID"` +} + +// GetAuthorityBtnResponse 获取角色按钮权限响应 +type GetAuthorityBtnResponse struct { + Selected []uint `json:"selected"` +} + +// SetAuthorityBtnRequest 设置角色按钮权限请求 +type SetAuthorityBtnRequest struct { + AuthorityId uint `json:"authorityId"` + MenuID uint `json:"menuID"` + Selected []uint `json:"selected"` +} + +// RegisterRoutes 注册路由 +func (s *AuthorityBtnService) RegisterRoutes(srv *http.Server) { + r := srv.Route("/") + r.POST("/authorityBtn/getAuthorityBtn", s.handleGetAuthorityBtn) + r.POST("/authorityBtn/setAuthorityBtn", s.handleSetAuthorityBtn) + r.POST("/authorityBtn/canRemoveAuthorityBtn", s.handleCanRemoveAuthorityBtn) +} + +func (s *AuthorityBtnService) handleGetAuthorityBtn(ctx http.Context) error { + var req GetAuthorityBtnRequest + if err := ctx.Bind(&req); err != nil { + return err + } + selected, err := s.uc.GetAuthorityBtn(ctx, req.AuthorityId, req.MenuID) + if err != nil { + return errors.InternalServer("QUERY_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{ + "code": 0, + "msg": "查询成功", + "data": GetAuthorityBtnResponse{Selected: selected}, + }) +} + +func (s *AuthorityBtnService) handleSetAuthorityBtn(ctx http.Context) error { + var req SetAuthorityBtnRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.uc.SetAuthorityBtn(ctx, req.AuthorityId, req.MenuID, req.Selected); err != nil { + return errors.InternalServer("SET_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "分配成功"}) +} + +func (s *AuthorityBtnService) handleCanRemoveAuthorityBtn(ctx http.Context) error { + idStr := ctx.Request().URL.Query().Get("id") + if idStr == "" { + return errors.BadRequest("INVALID_ID", "id参数不能为空") + } + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return errors.BadRequest("INVALID_ID", "id参数格式错误") + } + if err := s.uc.CanRemoveAuthorityBtn(ctx, uint(id)); err != nil { + return errors.BadRequest("BTN_IN_USE", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) +} diff --git a/internal/service/system/captcha.go b/internal/service/system/captcha.go new file mode 100644 index 0000000..d717e11 --- /dev/null +++ b/internal/service/system/captcha.go @@ -0,0 +1,156 @@ +package system + +import ( + "sync" + "time" + + "github.com/go-kratos/kratos/v2/transport/http" + "github.com/mojocn/base64Captcha" +) + +// CaptchaService 验证码服务 +type CaptchaService struct { + config CaptchaConfig + store base64Captcha.Store + blackCache *BlackCache +} + +// CaptchaConfig 验证码配置 +type CaptchaConfig struct { + KeyLong int + ImgWidth int + ImgHeight int + OpenCaptcha int // 防爆次数,0表示关闭 + OpenCaptchaTimeOut int // 缓存超时时间(秒) +} + +// BlackCache 黑名单缓存 +type BlackCache struct { + mu sync.RWMutex + items map[string]*cacheItem +} + +type cacheItem struct { + value int + expiration time.Time +} + +// NewBlackCache 创建黑名单缓存 +func NewBlackCache() *BlackCache { + bc := &BlackCache{ + items: make(map[string]*cacheItem), + } + go bc.cleanup() + return bc +} + +func (c *BlackCache) Set(key string, value int, duration time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.items[key] = &cacheItem{ + value: value, + expiration: time.Now().Add(duration), + } +} + +func (c *BlackCache) Get(key string) (int, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + item, ok := c.items[key] + if !ok || time.Now().After(item.expiration) { + return 0, false + } + return item.value, true +} + +func (c *BlackCache) cleanup() { + ticker := time.NewTicker(time.Minute) + for range ticker.C { + c.mu.Lock() + now := time.Now() + for k, v := range c.items { + if now.After(v.expiration) { + delete(c.items, k) + } + } + c.mu.Unlock() + } +} + +// CaptchaResponse 验证码响应 +type CaptchaResponse struct { + CaptchaId string `json:"captchaId"` + PicPath string `json:"picPath"` + CaptchaLength int `json:"captchaLength"` + OpenCaptcha bool `json:"openCaptcha"` +} + +// NewCaptchaService 创建验证码服务 +func NewCaptchaService(config CaptchaConfig, store base64Captcha.Store) *CaptchaService { + if store == nil { + store = base64Captcha.DefaultMemStore + } + return &CaptchaService{ + config: config, + store: store, + blackCache: NewBlackCache(), + } +} + +// RegisterRoutes 注册路由 +func (s *CaptchaService) RegisterRoutes(srv *http.Server) { + r := srv.Route("/") + r.POST("/base/captcha", s.handleCaptcha) +} + +func (s *CaptchaService) handleCaptcha(ctx http.Context) error { + // 获取客户端IP + clientIP := ctx.Request().RemoteAddr + + // 判断验证码是否开启 + openCaptcha := s.config.OpenCaptcha + openCaptchaTimeOut := s.config.OpenCaptchaTimeOut + + v, ok := s.blackCache.Get(clientIP) + if !ok { + s.blackCache.Set(clientIP, 1, time.Second*time.Duration(openCaptchaTimeOut)) + } + + var oc bool + if openCaptcha == 0 || openCaptcha < v { + oc = true + } + + // 生成验证码 + driver := base64Captcha.NewDriverDigit( + s.config.ImgHeight, + s.config.ImgWidth, + s.config.KeyLong, + 0.7, + 80, + ) + cp := base64Captcha.NewCaptcha(driver, s.store) + id, b64s, _, err := cp.Generate() + if err != nil { + return ctx.Result(200, map[string]any{ + "code": 7, + "msg": "验证码获取失败", + }) + } + + return ctx.Result(200, map[string]any{ + "code": 0, + "msg": "验证码获取成功", + "data": CaptchaResponse{ + CaptchaId: id, + PicPath: b64s, + CaptchaLength: s.config.KeyLong, + OpenCaptcha: oc, + }, + }) +} + +// Verify 验证验证码 +func (s *CaptchaService) Verify(id, answer string) bool { + return s.store.Verify(id, answer, true) +} diff --git a/internal/service/system/casbin.go b/internal/service/system/casbin.go new file mode 100644 index 0000000..6a81d40 --- /dev/null +++ b/internal/service/system/casbin.go @@ -0,0 +1,127 @@ +package system + +import ( + "errors" + "strconv" + + "kra/internal/biz/system" + "kra/internal/server/middleware" + + kerrors "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// CasbinService Casbin服务 +type CasbinService struct { + casbinRepo system.CasbinRepo + authUc *system.AuthorityUsecase + apiUc *system.ApiUsecase + useStrictAuth bool +} + +// NewCasbinService 创建Casbin服务 +func NewCasbinService(casbinRepo system.CasbinRepo, authUc *system.AuthorityUsecase, apiUc *system.ApiUsecase, useStrictAuth bool) *CasbinService { + return &CasbinService{ + casbinRepo: casbinRepo, + authUc: authUc, + apiUc: apiUc, + useStrictAuth: useStrictAuth, + } +} + +// UpdateCasbinRequest 更新Casbin请求 +type UpdateCasbinRequest struct { + AuthorityId uint `json:"authorityId"` + CasbinInfos []system.CasbinRule `json:"casbinInfos"` +} + +// PolicyPathResponse 权限路径响应 +type PolicyPathResponse struct { + Paths []system.CasbinRule `json:"paths"` +} + +// GetPolicyPathRequest 获取权限路径请求 +type GetPolicyPathRequest struct { + AuthorityId uint `json:"authorityId"` +} + +// UpdateCasbin 更新Casbin权限 +func (s *CasbinService) UpdateCasbin(ctx http.Context, adminAuthorityID uint, req *UpdateCasbinRequest) error { + // 检查权限 + if err := s.authUc.CheckAuthorityIDAuth(ctx, adminAuthorityID, req.AuthorityId); err != nil { + return err + } + + // 严格模式下检查API权限 + if s.useStrictAuth { + apis, err := s.apiUc.GetAllApis(ctx, adminAuthorityID) + if err != nil { + return err + } + for _, info := range req.CasbinInfos { + hasApi := false + for _, api := range apis { + if api.Path == info.Path && api.Method == info.Method { + hasApi = true + break + } + } + if !hasApi { + return errors.New("存在api不在权限列表中") + } + } + } + + return s.casbinRepo.UpdateCasbin(adminAuthorityID, req.AuthorityId, req.CasbinInfos) +} + +// GetPolicyPathByAuthorityId 获取权限列表 +func (s *CasbinService) GetPolicyPathByAuthorityId(authorityId uint) []system.CasbinRule { + return s.casbinRepo.GetPolicyPathByAuthorityId(authorityId) +} + +// RegisterRoutes 注册路由 +func (s *CasbinService) RegisterRoutes(srv *http.Server) { + r := srv.Route("/") + r.POST("/casbin/UpdateCasbin", s.handleUpdateCasbin) + r.POST("/casbin/getPolicyPathByAuthorityId", s.handleGetPolicyPath) +} + +func (s *CasbinService) handleUpdateCasbin(ctx http.Context) error { + var req UpdateCasbinRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if req.AuthorityId == 0 { + return kerrors.BadRequest("INVALID_AUTHORITY", "角色ID不能为空") + } + adminAuthorityID := middleware.GetAuthorityID(ctx) + if adminAuthorityID == 0 { + return kerrors.Unauthorized("UNAUTHORIZED", "请先登录") + } + if err := s.UpdateCasbin(ctx, adminAuthorityID, &req); err != nil { + return kerrors.InternalServer("UPDATE_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "更新成功"}) +} + +func (s *CasbinService) handleGetPolicyPath(ctx http.Context) error { + var req GetPolicyPathRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if req.AuthorityId == 0 { + return kerrors.BadRequest("INVALID_AUTHORITY", "角色ID不能为空") + } + paths := s.GetPolicyPathByAuthorityId(req.AuthorityId) + return ctx.Result(200, map[string]any{ + "code": 0, + "msg": "获取成功", + "data": PolicyPathResponse{Paths: paths}, + }) +} + +// 辅助函数 +func uintToStr(n uint) string { + return strconv.FormatUint(uint64(n), 10) +}