package system import ( "bytes" "context" "go/parser" "go/printer" "go/token" "io" "mime/multipart" "os" "path/filepath" "strings" "github.com/go-kratos/kratos/v2/log" "github.com/mholt/archives" cp "github.com/otiai10/copy" "github.com/pkg/errors" "kra/internal/conf" "kra/pkg/utils" ) // AutoCodePluginRepo 插件仓储接口 type AutoCodePluginRepo interface { GetMenusByIDs(ctx context.Context, ids []uint) ([]SysBaseMenu, error) GetApisByIDs(ctx context.Context, ids []uint) ([]SysApi, error) } // AutoCodePluginUsecase 插件用例 type AutoCodePluginUsecase struct { repo AutoCodePluginRepo config *conf.AutoCodeConfig log *log.Helper } // NewAutoCodePluginUsecase 创建插件用例 func NewAutoCodePluginUsecase( repo AutoCodePluginRepo, config *conf.AutoCodeConfig, logger log.Logger, ) *AutoCodePluginUsecase { return &AutoCodePluginUsecase{ repo: repo, config: config, log: log.NewHelper(logger), } } // Install 插件安装 func (uc *AutoCodePluginUsecase) Install(file *multipart.FileHeader) (web, server int, err error) { const PLUGINPATH = "./kra-plug-temp/" defer os.RemoveAll(PLUGINPATH) if _, err = os.Stat(PLUGINPATH); os.IsNotExist(err) { os.Mkdir(PLUGINPATH, os.ModePerm) } src, err := file.Open() if err != nil { return -1, -1, err } defer src.Close() out, err := os.Create(PLUGINPATH + file.Filename) if err != nil { return -1, -1, err } defer out.Close() _, err = io.Copy(out, src) if err != nil { return -1, -1, err } paths, err := utils.Unzip(PLUGINPATH+file.Filename, PLUGINPATH) if err != nil { return -1, -1, err } paths = uc.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 { uc.log.Error("非标准插件,请按照文档自动迁移使用") return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用") } if len(serverPlugin) != 0 { err = uc.installation(serverPlugin, uc.config.Server, uc.config.Server) if err != nil { return webIndex, serverIndex, err } } if len(webPlugin) != 0 { err = uc.installation(webPlugin, uc.config.Server, uc.config.Web) if err != nil { return webIndex, serverIndex, err } } return 1, 1, err } // installation 安装插件 func (uc *AutoCodePluginUsecase) installation(path string, formPath string, toPath string) error { arr := strings.Split(filepath.ToSlash(path), "/") ln := len(arr) if ln < 3 { return errors.New("路径格式错误") } name := arr[ln-3] form := filepath.Join(uc.config.Root, formPath, path) to := filepath.Join(uc.config.Root, toPath, "plugin") if _, err := os.Stat(to + name); err == nil { uc.log.Error("已存在同名插件,请自行手动安装", "to", to) return errors.New(toPath + "已存在同名插件,请自行手动安装") } return cp.Copy(form, to, cp.Options{Skip: uc.skipMacSpecialDocument}) } // filterFile 过滤文件 func (uc *AutoCodePluginUsecase) filterFile(paths []string) []string { np := make([]string, 0, len(paths)) for _, path := range paths { if ok, _ := uc.skipMacSpecialDocument(nil, path, ""); ok { continue } np = append(np, path) } return np } // skipMacSpecialDocument 跳过Mac特殊文件 func (uc *AutoCodePluginUsecase) skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) { if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") { return true, nil } return false, nil } // PubPlug 发布插件 func (uc *AutoCodePluginUsecase) PubPlug(plugName string) (zipPath string, err error) { if plugName == "" { return "", errors.New("插件名称不能为空") } // 防止路径穿越 plugName = filepath.Clean(plugName) webPath := filepath.Join(uc.config.Root, uc.config.Web, "plugin", plugName) serverPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", plugName) // 判断目录是否存在 if _, err = os.Stat(webPath); err != nil { return "", errors.New("web路径不存在") } if _, err = os.Stat(serverPath); 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, }) if err != nil { return "", err } out, err := os.Create(fileName) if err != nil { return "", err } defer out.Close() format := archives.CompressedArchive{ Archival: archives.Zip{}, } if err = format.Archive(context.Background(), out, files); err != nil { return "", err } return filepath.Join(uc.config.Root, uc.config.Server, fileName), nil } // InitMenuRequest 初始化菜单请求 type InitMenuRequest struct { PlugName string `json:"plugName"` ParentMenu string `json:"parentMenu"` Menus []uint `json:"menus"` } // InitMenu 初始化菜单 func (uc *AutoCodePluginUsecase) InitMenu(ctx context.Context, menuInfo InitMenuRequest) error { menuPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go") src, err := os.ReadFile(menuPath) if err != nil { return err } fileSet := token.NewFileSet() astFile, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { return err } arrayAst := utils.FindArray(astFile, "model", "SysBaseMenu") if arrayAst == nil { return errors.New("未找到菜单数组") } // 查询菜单 menus, err := uc.repo.GetMenusByIDs(ctx, menuInfo.Menus) if err != nil { return err } // 添加父菜单 parentMenu := SysBaseMenu{ ParentId: 0, Path: menuInfo.PlugName + "Menu", Name: menuInfo.PlugName + "Menu", Hidden: false, Component: "view/routerHolder.vue", Sort: 0, Meta: Meta{ Title: menuInfo.ParentMenu, Icon: "school", }, } allMenus := append([]SysBaseMenu{parentMenu}, menus...) // 转换为utils.SysBaseMenu类型 utilsMenus := make([]utils.SysBaseMenu, len(allMenus)) for i, m := range allMenus { utilsMenus[i] = utils.SysBaseMenu{ ParentId: m.ParentId, Path: m.Path, Name: m.Name, Hidden: m.Hidden, Component: m.Component, Sort: m.Sort, Title: m.Meta.Title, Icon: m.Meta.Icon, } // 转换参数 for _, p := range m.Parameters { utilsMenus[i].Parameters = append(utilsMenus[i].Parameters, utils.SysBaseMenuParameter{ Type: p.Type, Key: p.Key, Value: p.Value, }) } // 转换按钮 for _, b := range m.MenuBtn { utilsMenus[i].MenuBtn = append(utilsMenus[i].MenuBtn, utils.SysBaseMenuBtn{ Name: b.Name, Desc: b.Desc, }) } } menuExpr := utils.CreateMenuStructAst(utilsMenus) arrayAst.Elts = *menuExpr var out []byte bf := bytes.NewBuffer(out) printer.Fprint(bf, fileSet, astFile) return os.WriteFile(menuPath, bf.Bytes(), 0666) } // InitApiRequest 初始化API请求 type InitApiRequest struct { PlugName string `json:"plugName"` APIs []uint `json:"apis"` } // InitAPI 初始化API func (uc *AutoCodePluginUsecase) InitAPI(ctx context.Context, apiInfo InitApiRequest) error { apiPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", apiInfo.PlugName, "initialize", "api.go") src, err := os.ReadFile(apiPath) if err != nil { return err } fileSet := token.NewFileSet() astFile, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { return err } arrayAst := utils.FindArray(astFile, "model", "SysApi") if arrayAst == nil { return errors.New("未找到API数组") } // 查询API apis, err := uc.repo.GetApisByIDs(ctx, apiInfo.APIs) if err != nil { return err } // 转换为utils.SysApi类型 utilsApis := make([]utils.SysApi, len(apis)) for i, a := range apis { utilsApis[i] = utils.SysApi{ Path: a.Path, Description: a.Description, ApiGroup: a.ApiGroup, Method: a.Method, } } apisExpr := utils.CreateApiStructAst(utilsApis) arrayAst.Elts = *apisExpr var out []byte bf := bytes.NewBuffer(out) printer.Fprint(bf, fileSet, astFile) return os.WriteFile(apiPath, bf.Bytes(), 0666) }