任务三

This commit is contained in:
Yvan 2026-01-07 15:00:27 +08:00
parent c83b773a40
commit ada3084683
5 changed files with 482 additions and 0 deletions

View File

@ -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("此按钮正在被使用无法删除")

View File

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

View File

@ -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": "删除成功"})
}

View File

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

View File

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