🎉 更新系统版本

This commit is contained in:
2026-03-13 21:12:17 +08:00
parent 5ca65e3004
commit 5e3380f5ef
45 changed files with 1310 additions and 103 deletions

View File

@@ -331,3 +331,82 @@ func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint)
}
return *authority.ParentId, nil
}
// GetUserIdsByAuthorityId 获取拥有指定角色的所有用户ID
func (authorityService *AuthorityService) GetUserIdsByAuthorityId(authorityId uint) (userIds []uint, err error) {
var records []system.SysUserAuthority
err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&records).Error
if err != nil {
return nil, err
}
for _, r := range records {
userIds = append(userIds, r.SysUserId)
}
return userIds, nil
}
// SetRoleUsers 全量覆盖某角色关联的用户列表
// 入参角色ID + 目标用户ID列表保存时将该角色的关联关系完全替换为传入列表
func (authorityService *AuthorityService) SetRoleUsers(authorityId uint, userIds []uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 查出当前拥有该角色的所有用户ID
var existingRecords []system.SysUserAuthority
if err := tx.Where("sys_authority_authority_id = ?", authorityId).Find(&existingRecords).Error; err != nil {
return err
}
currentSet := make(map[uint]struct{})
for _, r := range existingRecords {
currentSet[r.SysUserId] = struct{}{}
}
targetSet := make(map[uint]struct{})
for _, id := range userIds {
targetSet[id] = struct{}{}
}
// 2. 删除该角色所有已有的用户关联
if err := tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", authorityId).Error; err != nil {
return err
}
// 3. 对被移除的用户:若该角色是其主角色,则将主角色切换为其剩余的其他角色
for userId := range currentSet {
if _, ok := targetSet[userId]; ok {
continue // 仍在目标列表中,不处理
}
var user system.SysUser
if err := tx.First(&user, "id = ?", userId).Error; err != nil {
continue
}
if user.AuthorityId == authorityId {
// 从剩余关联(已删除当前角色后)中找另一个角色作为主角色
var another system.SysUserAuthority
if err := tx.Where("sys_user_id = ?", userId).First(&another).Error; err != nil {
// 没有其他角色,主角色保持不变,不做处理
continue
}
if err := tx.Model(&system.SysUser{}).Where("id = ?", userId).
Update("authority_id", another.SysAuthorityAuthorityId).Error; err != nil {
return err
}
}
}
// 4. 批量插入新的关联记录
if len(userIds) > 0 {
newRecords := make([]system.SysUserAuthority, 0, len(userIds))
for _, userId := range userIds {
newRecords = append(newRecords, system.SysUserAuthority{
SysUserId: userId,
SysAuthorityAuthorityId: authorityId,
})
}
if err := tx.Create(&newRecords).Error; err != nil {
return err
}
}
return nil
})
}

View File

@@ -171,3 +171,45 @@ func (casbinService *CasbinService) FreshCasbin() (err error) {
err = e.LoadPolicy()
return err
}
// GetAuthoritiesByApi 获取拥有指定API权限的所有角色ID
func (casbinService *CasbinService) GetAuthoritiesByApi(path, method string) (authorityIds []uint, err error) {
var rules []gormadapter.CasbinRule
err = global.GVA_DB.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Find(&rules).Error
if err != nil {
return nil, err
}
for _, r := range rules {
id, e := strconv.Atoi(r.V0)
if e == nil {
authorityIds = append(authorityIds, uint(id))
}
}
return authorityIds, nil
}
// SetApiAuthorities 全量覆盖某API关联的角色列表
func (casbinService *CasbinService) SetApiAuthorities(path, method string, authorityIds []uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 删除该API所有已有的角色关联
if err := tx.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Delete(&gormadapter.CasbinRule{}).Error; err != nil {
return err
}
// 2. 批量插入新的关联记录
if len(authorityIds) > 0 {
newRules := make([]gormadapter.CasbinRule, 0, len(authorityIds))
for _, authorityId := range authorityIds {
newRules = append(newRules, gormadapter.CasbinRule{
Ptype: "p",
V0: strconv.Itoa(int(authorityId)),
V1: path,
V2: method,
})
}
if err := tx.Create(&newRules).Error; err != nil {
return err
}
}
return nil
})
}

View File

@@ -315,6 +315,65 @@ func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) (
return menus, err
}
// GetAuthoritiesByMenuId 获取拥有指定菜单的所有角色ID
func (menuService *MenuService) GetAuthoritiesByMenuId(menuId uint) (authorityIds []uint, err error) {
var records []system.SysAuthorityMenu
err = global.GVA_DB.Where("sys_base_menu_id = ?", menuId).Find(&records).Error
if err != nil {
return nil, err
}
for _, r := range records {
id, e := strconv.Atoi(r.AuthorityId)
if e == nil {
authorityIds = append(authorityIds, uint(id))
}
}
return authorityIds, nil
}
// GetDefaultRouterAuthorityIds 获取将指定菜单设为首页的角色ID列表
func (menuService *MenuService) GetDefaultRouterAuthorityIds(menuId uint) (authorityIds []uint, err error) {
var menu system.SysBaseMenu
err = global.GVA_DB.First(&menu, menuId).Error
if err != nil {
return nil, err
}
var authorities []system.SysAuthority
err = global.GVA_DB.Where("default_router = ?", menu.Name).Find(&authorities).Error
if err != nil {
return nil, err
}
for _, auth := range authorities {
authorityIds = append(authorityIds, auth.AuthorityId)
}
return authorityIds, nil
}
// SetMenuAuthorities 全量覆盖某菜单关联的角色列表
func (menuService *MenuService) SetMenuAuthorities(menuId uint, authorityIds []uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 删除该菜单所有已有的角色关联
if err := tx.Where("sys_base_menu_id = ?", menuId).Delete(&system.SysAuthorityMenu{}).Error; err != nil {
return err
}
// 2. 批量插入新的关联记录
if len(authorityIds) > 0 {
menuIdStr := strconv.Itoa(int(menuId))
newRecords := make([]system.SysAuthorityMenu, 0, len(authorityIds))
for _, authorityId := range authorityIds {
newRecords = append(newRecords, system.SysAuthorityMenu{
MenuId: menuIdStr,
AuthorityId: strconv.Itoa(int(authorityId)),
})
}
if err := tx.Create(&newRecords).Error; err != nil {
return err
}
}
return nil
})
}
// UserAuthorityDefaultRouter 用户角色默认路由检查
//
// Author [SliverHorn](https://github.com/SliverHorn)

View File

@@ -1,10 +1,15 @@
package system
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"sort"
@@ -158,6 +163,107 @@ func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) er
return nil
}
func (s *SkillsService) Delete(_ context.Context, req request.SkillDeleteRequest) error {
if strings.TrimSpace(req.Tool) == "" {
return errors.New("工具类型不能为空")
}
if !isSafeName(req.Skill) {
return errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(req.Tool, req.Skill)
if err != nil {
return err
}
info, err := os.Stat(skillDir)
if err != nil {
if os.IsNotExist(err) {
return errors.New("技能不存在")
}
return err
}
if !info.IsDir() {
return errors.New("技能目录异常")
}
return os.RemoveAll(skillDir)
}
func (s *SkillsService) Package(_ context.Context, req request.SkillPackageRequest) (string, []byte, error) {
if strings.TrimSpace(req.Tool) == "" {
return "", nil, errors.New("工具类型不能为空")
}
if !isSafeName(req.Skill) {
return "", nil, errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(req.Tool, req.Skill)
if err != nil {
return "", nil, err
}
info, err := os.Stat(skillDir)
if err != nil {
if os.IsNotExist(err) {
return "", nil, errors.New("技能不存在")
}
return "", nil, err
}
if !info.IsDir() {
return "", nil, errors.New("技能目录异常")
}
buf := bytes.NewBuffer(nil)
zw := zip.NewWriter(buf)
walkErr := filepath.WalkDir(skillDir, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(skillDir, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
zipName := filepath.ToSlash(rel)
if d.IsDir() {
_, err = zw.Create(strings.TrimSuffix(zipName, "/") + "/")
return err
}
fileInfo, err := d.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
header.Name = zipName
header.Method = zip.Deflate
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
_, err = writer.Write(content)
return err
})
if walkErr != nil {
_ = zw.Close()
return "", nil, walkErr
}
if err = zw.Close(); err != nil {
return "", nil, err
}
return req.Skill + ".zip", buf.Bytes(), nil
}
func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
if !isSafeName(req.Skill) {
return "", "", errors.New("技能名称不合法")
@@ -279,6 +385,136 @@ func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.Skil
return nil
}
func (s *SkillsService) DownloadOnlineSkill(_ context.Context, req request.DownloadOnlineSkillReq) error {
skillsDir, err := s.toolSkillsDir(req.Tool)
if err != nil {
return err
}
body, err := json.Marshal(map[string]interface{}{
"plugin_id": req.ID,
"version": req.Version,
})
if err != nil {
return fmt.Errorf("构建下载请求失败: %w", err)
}
downloadReq, err := http.NewRequest(http.MethodPost, "https://plugin.gin-vue-admin.com/api/shopPlugin/downloadSkill", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("构建下载请求失败: %w", err)
}
downloadReq.Header.Set("Content-Type", "application/json")
downloadResp, err := http.DefaultClient.Do(downloadReq)
if err != nil {
return fmt.Errorf("下载技能失败: %w", err)
}
defer downloadResp.Body.Close()
if downloadResp.StatusCode != http.StatusOK {
return fmt.Errorf("下载技能失败, HTTP状态码: %d", downloadResp.StatusCode)
}
metaBody, err := io.ReadAll(downloadResp.Body)
if err != nil {
return fmt.Errorf("读取下载结果失败: %w", err)
}
var meta struct {
Data struct {
URL string `json:"url"`
} `json:"data"`
}
if err = json.Unmarshal(metaBody, &meta); err != nil {
return fmt.Errorf("解析下载结果失败: %w", err)
}
realDownloadURL := strings.TrimSpace(meta.Data.URL)
if realDownloadURL == "" {
return errors.New("下载结果缺少 url")
}
zipResp, err := http.Get(realDownloadURL)
if err != nil {
return fmt.Errorf("下载压缩包失败: %w", err)
}
defer zipResp.Body.Close()
if zipResp.StatusCode != http.StatusOK {
return fmt.Errorf("下载压缩包失败, HTTP状态码: %d", zipResp.StatusCode)
}
tmpFile, err := os.CreateTemp("", "gva-skill-*.zip")
if err != nil {
return fmt.Errorf("创建临时文件失败: %w", err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err = io.Copy(tmpFile, zipResp.Body); err != nil {
tmpFile.Close()
return fmt.Errorf("保存技能包失败: %w", err)
}
tmpFile.Close()
if err = extractZipToDir(tmpPath, skillsDir); err != nil {
return fmt.Errorf("解压技能包失败: %w", err)
}
return nil
}
func extractZipToDir(zipPath, destDir string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
name := filepath.FromSlash(f.Name)
if strings.Contains(name, "..") {
continue
}
target := filepath.Join(destDir, name)
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) {
continue
}
if f.FileInfo().IsDir() {
if err := os.MkdirAll(target, os.ModePerm); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
rc.Close()
return err
}
_, err = io.Copy(out, rc)
rc.Close()
out.Close()
if err != nil {
return err
}
}
return nil
}
func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
toolDir, ok := skillToolDirs[tool]
if !ok {

View File

@@ -109,7 +109,25 @@ func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) (lis
if err != nil {
return
}
err = db.Limit(limit).Offset(offset).Preload("Authorities").Preload("Authority").Find(&userList).Error
orderStr := "id desc"
if info.OrderKey != "" {
allowedOrders := map[string]bool{
"id": true,
"username": true,
"nick_name": true,
"phone": true,
"email": true,
}
if allowedOrders[info.OrderKey] {
orderStr = info.OrderKey
if info.Desc {
orderStr = info.OrderKey + " desc"
}
}
}
err = db.Limit(limit).Offset(offset).Order(orderStr).Preload("Authorities").Preload("Authority").Find(&userList).Error
return userList, total, err
}