Files
st/server/service/system/auto_code_plugin.go
2026-02-10 17:48:27 +08:00

513 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package system
import (
"bytes"
"context"
"fmt"
goast "go/ast"
"go/parser"
"go/printer"
"go/token"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/model/system/request"
pluginUtils "git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
"git.echol.cn/loser/st/server/utils"
ast "git.echol.cn/loser/st/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 := ""
serverPackage := ""
serverRootName := ""
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` {
if len(serverPlugin) == 0 {
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
if serverRootName == "" && ln > 1 && pathArr[1] != "" {
serverRootName = pathArr[1]
}
if ln > 4 && serverPackage == "" && pathArr[4] != "" {
serverPackage = pathArr[4]
}
}
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 {
if serverPackage == "" {
serverPackage = serverRootName
}
err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server)
if err != nil {
return webIndex, serverIndex, err
}
err = ensurePluginRegisterImport(serverPackage)
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 ensurePluginRegisterImport(packageName string) error {
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
if module == "" {
return errors.New("autocode module is empty")
}
if packageName == "" {
return errors.New("plugin package is empty")
}
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
src, err := os.ReadFile(registerPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
if err != nil {
return err
}
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
if ast.CheckImport(astFile, importPath) {
return nil
}
importSpec := &goast.ImportSpec{
Name: goast.NewIdent("_"),
Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)},
}
var importDecl *goast.GenDecl
for _, decl := range astFile.Decls {
genDecl, ok := decl.(*goast.GenDecl)
if !ok {
continue
}
if genDecl.Tok == token.IMPORT {
importDecl = genDecl
break
}
}
if importDecl == nil {
astFile.Decls = append([]goast.Decl{
&goast.GenDecl{
Tok: token.IMPORT,
Specs: []goast.Spec{importSpec},
},
}, astFile.Decls...)
} else {
importDecl.Specs = append(importDecl.Specs, importSpec)
}
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(registerPath, bf.Bytes(), 0666)
}
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
}
func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) {
// 1. 删除前端代码
if pluginType == "web" || pluginType == "full" {
webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName)
err = os.RemoveAll(webDir)
if err != nil {
return errors.Wrap(err, "删除前端插件目录失败")
}
}
// 2. 删除后端代码
if pluginType == "server" || pluginType == "full" {
serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName)
err = os.RemoveAll(serverDir)
if err != nil {
return errors.Wrap(err, "删除后端插件目录失败")
}
// 移除注册
removePluginRegisterImport(pluginName)
}
// 通过utils 获取 api 菜单 字典
apis, menus, dicts := pluginUtils.GetPluginData(pluginName)
// 3. 删除菜单 (递归删除)
if len(menus) > 0 {
for _, menu := range menus {
var dbMenu system.SysBaseMenu
if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil {
// 获取该菜单及其所有子菜单的ID
var menuIds []int
GetMenuIds(dbMenu, &menuIds)
// 逆序删除,先删除子菜单
for i := len(menuIds) - 1; i >= 0; i-- {
err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i])
if err != nil {
zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err))
}
}
}
}
}
// 4. 删除API
if len(apis) > 0 {
for _, api := range apis {
var dbApi system.SysApi
if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil {
err := ApiServiceApp.DeleteApi(dbApi)
if err != nil {
zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err))
}
}
}
}
// 5. 删除字典
if len(dicts) > 0 {
for _, dict := range dicts {
var dbDict system.SysDictionary
if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil {
err := DictionaryServiceApp.DeleteSysDictionary(dbDict)
if err != nil {
zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err))
}
}
}
}
return nil
}
func GetMenuIds(menu system.SysBaseMenu, ids *[]int) {
*ids = append(*ids, int(menu.ID))
var children []system.SysBaseMenu
global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children)
for _, child := range children {
// 先递归收集子菜单
GetMenuIds(child, ids)
}
}
func removePluginRegisterImport(packageName string) error {
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
if module == "" {
return errors.New("autocode module is empty")
}
if packageName == "" {
return errors.New("plugin package is empty")
}
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
src, err := os.ReadFile(registerPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
if err != nil {
return err
}
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
importLit := fmt.Sprintf("%q", importPath)
// 移除 import
var newDecls []goast.Decl
for _, decl := range astFile.Decls {
genDecl, ok := decl.(*goast.GenDecl)
if !ok {
newDecls = append(newDecls, decl)
continue
}
if genDecl.Tok == token.IMPORT {
var newSpecs []goast.Spec
for _, spec := range genDecl.Specs {
importSpec, ok := spec.(*goast.ImportSpec)
if !ok {
newSpecs = append(newSpecs, spec)
continue
}
if importSpec.Path.Value != importLit {
newSpecs = append(newSpecs, spec)
}
}
// 如果还有其他import保留该 decl
if len(newSpecs) > 0 {
genDecl.Specs = newSpecs
newDecls = append(newDecls, genDecl)
}
} else {
newDecls = append(newDecls, decl)
}
}
astFile.Decls = newDecls
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(registerPath, bf.Bytes(), 0666)
}