Files
gin-vue-admin/server/service/system/auto_code_plugin.go
T
2242f5d6e1 public: 发布v2.8.8 (#2167)
* refactor: 移除冗余的工具名称和描述,简化代码生成器的文档

* Add Content-Type to S3 upload input

Add Content-Type to S3 upload input

* style: 优化仪表盘组件样式,调整各个子组件的布局和样式,提升视觉效果

* fix: 更新插件链接

* 优化角色配置-首页配置相关内容

* feat: 默认首页只允许选择已选中的菜单

* feat: 更新样式和优化组件,调整背景色及文本颜色

* feat: 清理无用的右侧边线

* feat: 增加了导入导出模板的自定义sql能力,方便灵活化扩展导入导出

* feat: 优化日志输出,增加插件字典定义

* feat: 增加插件字典定义前端相关

* feat: 增加插件自定义字典方法

* feat: 增加文件名和路径合法性检查

* feat: 修复暗黑模式和亮色模式logo路径定义

* feat: 移除未使用的defineEmits导入

* feat: 更新授权链接至新地址

* feat: 移除菜单组件背景色

* feat: 更新导出和导入SQL语句的示例占位符

* feat: 错误日志必须有数据库链接才会存储

* feat: 移除仪表板中公告卡片的高度限制

* feat: 添加插件列表接口,更新插件表格组件,移除不必要的注释

* feat: 更新版本号至v2.8.8,调整插件表格组件的请求参数

---------

Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com>
Co-authored-by: Yexk_M <yexk@yexk.cn>
Co-authored-by: Azir-11 <2075125282@qq.com>
2026-01-11 14:41:19 +08:00

292 lines
8.2 KiB
Go

package system
import (
"bytes"
"context"
"fmt"
"go/parser"
"go/printer"
"go/token"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
"github.com/mholt/archives"
cp "github.com/otiai10/copy"
"github.com/pkg/errors"
"go.uber.org/zap"
)
var AutoCodePlugin = new(autoCodePlugin)
type autoCodePlugin struct{}
// Install 插件安装
func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, err error) {
const GVAPLUGPINATH = "./gva-plug-temp/"
defer os.RemoveAll(GVAPLUGPINATH)
_, err = os.Stat(GVAPLUGPINATH)
if os.IsNotExist(err) {
os.Mkdir(GVAPLUGPINATH, os.ModePerm)
}
src, err := file.Open()
if err != nil {
return -1, -1, err
}
defer src.Close()
// 在临时目录创建目标文件
// 使用完整路径拼接的好处:明确文件位置,避免路径混乱
out, err := os.Create(GVAPLUGPINATH + file.Filename)
if err != nil {
return -1, -1, err
}
// 将上传的文件内容复制到临时文件
// 使用io.Copy的好处:高效处理大文件,自动管理缓冲区,避免内存溢出
_, err = io.Copy(out, src)
if err != nil {
out.Close()
return -1, -1, err
}
// 立即关闭文件,确保数据写入磁盘并释放文件句柄
// 必须在解压前关闭,否则在Windows系统上会导致文件被占用无法解压
err = out.Close()
if err != nil {
return -1, -1, err
}
paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH)
paths = filterFile(paths)
var webIndex = -1
var serverIndex = -1
webPlugin := ""
serverPlugin := ""
for i := range paths {
paths[i] = filepath.ToSlash(paths[i])
pathArr := strings.Split(paths[i], "/")
ln := len(pathArr)
if ln < 4 {
continue
}
if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 {
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
}
if len(serverPlugin) == 0 && len(webPlugin) == 0 {
zap.L().Error("非标准插件,请按照文档自动迁移使用")
return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用")
}
if len(serverPlugin) != 0 {
err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server)
if err != nil {
return webIndex, serverIndex, err
}
}
if len(webPlugin) != 0 {
err = installation(webPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Web)
if err != nil {
return webIndex, serverIndex, err
}
}
return 1, 1, err
}
func installation(path string, formPath string, toPath string) error {
arr := strings.Split(filepath.ToSlash(path), "/")
ln := len(arr)
if ln < 3 {
return errors.New("arr")
}
name := arr[ln-3]
var form = filepath.Join(global.GVA_CONFIG.AutoCode.Root, formPath, path)
var to = filepath.Join(global.GVA_CONFIG.AutoCode.Root, toPath, "plugin")
_, err := os.Stat(to + name)
if err == nil {
zap.L().Error("autoPath 已存在同名插件,请自行手动安装", zap.String("to", to))
return errors.New(toPath + "已存在同名插件,请自行手动安装")
}
return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument})
}
func filterFile(paths []string) []string {
np := make([]string, 0, len(paths))
for _, path := range paths {
if ok, _ := skipMacSpecialDocument(nil, path, ""); ok {
continue
}
np = append(np, path)
}
return np
}
func skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) {
if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") {
return true, nil
}
return false, nil
}
func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) {
if plugName == "" {
return "", errors.New("插件名称不能为空")
}
// 防止路径穿越
plugName = filepath.Clean(plugName)
webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", plugName)
serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", plugName)
// 创建一个新的zip文件
// 判断目录是否存在
_, err = os.Stat(webPath)
if err != nil {
return "", errors.New("web路径不存在")
}
_, err = os.Stat(serverPath)
if err != nil {
return "", errors.New("server路径不存在")
}
fileName := plugName + ".zip"
// 创建一个新的zip文件
files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
webPath: plugName + "/web/plugin/" + plugName,
serverPath: plugName + "/server/plugin/" + plugName,
})
// create the output file we'll write to
out, err := os.Create(fileName)
if err != nil {
return
}
defer out.Close()
// we can use the CompressedArchive type to gzip a tarball
// (compression is not required; you could use Tar directly)
format := archives.CompressedArchive{
//Compression: archives.Gz{},
Archival: archives.Zip{},
}
// create the archive
err = format.Archive(context.Background(), out, files)
if err != nil {
return
}
return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil
}
func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) {
menuPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go")
src, err := os.ReadFile(menuPath)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
arrayAst := ast.FindArray(astFile, "model", "SysBaseMenu")
var menus []system.SysBaseMenu
parentMenu := []system.SysBaseMenu{
{
ParentId: 0,
Path: menuInfo.PlugName + "Menu",
Name: menuInfo.PlugName + "Menu",
Hidden: false,
Component: "view/routerHolder.vue",
Sort: 0,
Meta: system.Meta{
Title: menuInfo.ParentMenu,
Icon: "school",
},
},
}
// 查询菜单及其关联的参数和按钮
err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error
if err != nil {
return err
}
menus = append(parentMenu, menus...)
menuExpr := ast.CreateMenuStructAst(menus)
arrayAst.Elts = *menuExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(menuPath, bf.Bytes(), 0666)
return nil
}
func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) {
apiPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", apiInfo.PlugName, "initialize", "api.go")
src, err := os.ReadFile(apiPath)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
arrayAst := ast.FindArray(astFile, "model", "SysApi")
var apis []system.SysApi
err = global.GVA_DB.Find(&apis, "id in (?)", apiInfo.APIs).Error
if err != nil {
return err
}
apisExpr := ast.CreateApiStructAst(apis)
arrayAst.Elts = *apisExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(apiPath, bf.Bytes(), 0666)
return nil
}
func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) {
dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go")
src, err := os.ReadFile(dictPath)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
arrayAst := ast.FindArray(astFile, "model", "SysDictionary")
var dictionaries []system.SysDictionary
err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error
if err != nil {
return err
}
dictExpr := ast.CreateDictionaryStructAst(dictionaries)
arrayAst.Elts = *dictExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(dictPath, bf.Bytes(), 0666)
return nil
}