feat quickly

This commit is contained in:
2026-01-12 00:44:57 +08:00
parent 43ca340d54
commit 1d5bc97cdd
30 changed files with 2664 additions and 140 deletions

View File

@@ -0,0 +1,14 @@
package backend
import (
"context"
)
func NewApp() *App {
return &App{}
}
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
a.loadSettings()
}

View File

@@ -0,0 +1,56 @@
package backend
import (
"fmt"
)
func (a *App) AddDatabaseConfig(name string, targetPath string, modelPackagePath string) error {
if name == "" {
return fmt.Errorf("database name is empty")
}
if targetPath == "" {
return fmt.Errorf("target path is empty")
}
for _, db := range a.settings.Databases {
if db.Name == name {
return fmt.Errorf("database with name '%s' already exists", name)
}
}
a.settings.Databases = append(a.settings.Databases, DatabaseConfig{
Name: name,
TargetPath: targetPath,
ModelPackagePath: modelPackagePath,
})
return a.saveSettings()
}
func (a *App) RemoveDatabaseConfig(name string) error {
for i, db := range a.settings.Databases {
if db.Name == name {
a.settings.Databases = append(a.settings.Databases[:i], a.settings.Databases[i+1:]...)
return a.saveSettings()
}
}
return fmt.Errorf("database with name '%s' not found", name)
}
func (a *App) UpdateDatabaseConfig(oldName string, newName string, targetPath string, modelPackagePath string) error {
if newName == "" {
return fmt.Errorf("database name is empty")
}
if targetPath == "" {
return fmt.Errorf("target path is empty")
}
for i, db := range a.settings.Databases {
if db.Name == oldName {
a.settings.Databases[i].Name = newName
a.settings.Databases[i].TargetPath = targetPath
a.settings.Databases[i].ModelPackagePath = modelPackagePath
return a.saveSettings()
}
}
return fmt.Errorf("database with name '%s' not found", oldName)
}

View File

@@ -0,0 +1,9 @@
package backend
import (
"fmt"
)
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}

View File

@@ -0,0 +1,56 @@
package backend
import (
"os"
"path/filepath"
)
func copyDir(src string, dst string) error {
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
return err
}
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
if err := copyDir(srcPath, dstPath); err != nil {
return err
}
} else {
if err := copyFile(srcPath, dstPath); err != nil {
return err
}
}
}
return nil
}
func copyFile(src string, dst string) error {
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
_, err = destination.ReadFrom(source)
return err
}

View File

@@ -0,0 +1,34 @@
package backend
import (
"context"
)
type DatabaseConfig struct {
Name string `json:"name"`
TargetPath string `json:"targetPath"`
ModelPackagePath string `json:"modelPackagePath"`
}
type ProjectConfig struct {
Name string `json:"name"`
Path string `json:"path"`
}
type Settings struct {
Theme string `json:"theme"`
Language string `json:"language"`
Notifications bool `json:"notifications"`
AutoStart bool `json:"autoStart"`
MysqlModelPath string `json:"mysqlModelPath"`
DefaultQueryPackagePath string `json:"defaultQueryPackagePath"`
ModelBasePath string `json:"modelBasePath"`
SwaggerDir string `json:"swaggerDir"`
Databases []DatabaseConfig `json:"databases"`
Projects []ProjectConfig `json:"projects"`
}
type App struct {
ctx context.Context
settings Settings
}

View File

@@ -0,0 +1,171 @@
package backend
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
func (a *App) SelectFile(title string, defaultDir string, filter string) (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: title,
DefaultDirectory: defaultDir,
Filters: []runtime.FileFilter{{Pattern: filter}},
})
}
func (a *App) SelectDirectory(title string, defaultDir string) (string, error) {
return runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: title,
DefaultDirectory: defaultDir,
})
}
func (a *App) CheckGenPs1Exists(filePath string) (bool, error) {
if filePath == "" {
return false, fmt.Errorf("file path is empty")
}
_, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func (a *App) ReadGoModModule(filePath string) (string, error) {
if filePath == "" {
return "", fmt.Errorf("file path is empty")
}
dirPath := filepath.Dir(filePath)
goModPath := filepath.Join(dirPath, "go.mod")
content, err := os.ReadFile(goModPath)
if err != nil {
return "", fmt.Errorf("go.mod not found: %w", err)
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "module ") {
moduleName := strings.TrimPrefix(line, "module ")
moduleName = strings.TrimSpace(moduleName)
return moduleName, nil
}
}
return "", fmt.Errorf("module declaration not found in go.mod")
}
func (a *App) ExecuteGenPs1(genPs1Path string, dbName string, targetPath string, modelPackagePath string) (string, error) {
if genPs1Path == "" {
return "", fmt.Errorf("gen.ps1 path is empty")
}
if dbName == "" {
return "", fmt.Errorf("database name is empty")
}
if _, err := os.Stat(genPs1Path); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("gen.ps1 not found")
}
return "", fmt.Errorf("error checking gen.ps1: %w", err)
}
dirPath := filepath.Dir(genPs1Path)
psPath, err := exec.LookPath("powershell.exe")
if err != nil {
return "", fmt.Errorf("PowerShell not found: %w", err)
}
args := []string{
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-Command",
fmt.Sprintf("Set-Location -Path '%s'; & .\\gen.ps1 -dbName '%s'", dirPath, dbName),
}
cmd := exec.Command(psPath, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("script execution failed: %w\nOutput: %s", err, string(output))
}
if targetPath != "" {
sourceDir := filepath.Join(dirPath, dbName)
if _, err := os.Stat(sourceDir); err == nil {
modelSource := filepath.Join(sourceDir, "model")
querySource := filepath.Join(sourceDir, "query")
modelTarget := filepath.Join(targetPath, "model")
queryTarget := filepath.Join(targetPath, "query")
if _, err := os.Stat(modelSource); err == nil {
if err := copyDir(modelSource, modelTarget); err != nil {
return string(output), fmt.Errorf("failed to copy model directory: %w", err)
}
}
if _, err := os.Stat(querySource); err == nil {
if err := copyDir(querySource, queryTarget); err != nil {
return string(output), fmt.Errorf("failed to copy query directory: %w", err)
}
if modelPackagePath != "" {
if err := replaceImportPaths(queryTarget, dbName, modelPackagePath, a.settings.DefaultQueryPackagePath); err != nil {
return string(output), fmt.Errorf("failed to replace import paths: %w", err)
}
}
}
}
}
return string(output), nil
}
func replaceImportPaths(queryDir string, dbName string, newPackagePath string, basePath string) error {
err := filepath.Walk(queryDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".gen.go") {
if err := replaceImportInFile(path, dbName, newPackagePath, basePath); err != nil {
return err
}
}
return nil
})
return err
}
func replaceImportInFile(filePath string, dbName string, newPackagePath string, basePath string) error {
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
if basePath == "" {
basePath = "git.hlsq.asia/mmorpg"
}
oldImport := fmt.Sprintf(`"%s/%s/model"`, basePath, dbName)
newImport := fmt.Sprintf(`"%s"`, newPackagePath)
newContent := bytes.ReplaceAll(content, []byte(oldImport), []byte(newImport))
return os.WriteFile(filePath, newContent, 0644)
}

View File

@@ -0,0 +1,73 @@
package backend
import (
"fmt"
"os"
"path/filepath"
)
func (a *App) AddProjectConfig(name string, path string) error {
if path == "" {
return fmt.Errorf("project path is empty")
}
if name == "" {
name = filepath.Base(path)
}
goModPath := filepath.Join(path, "go.mod")
if _, err := os.Stat(goModPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("go.mod file not found in the specified path")
}
return fmt.Errorf("error checking go.mod file: %w", err)
}
for _, project := range a.settings.Projects {
if project.Name == name {
return fmt.Errorf("project with name '%s' already exists", name)
}
}
a.settings.Projects = append(a.settings.Projects, ProjectConfig{
Name: name,
Path: path,
})
return a.saveSettings()
}
func (a *App) RemoveProjectConfig(name string) error {
for i, project := range a.settings.Projects {
if project.Name == name {
a.settings.Projects = append(a.settings.Projects[:i], a.settings.Projects[i+1:]...)
return a.saveSettings()
}
}
return fmt.Errorf("project with name '%s' not found", name)
}
func (a *App) UpdateProjectConfig(oldName string, newName string, path string) error {
if newName == "" {
return fmt.Errorf("project name is empty")
}
if path == "" {
return fmt.Errorf("project path is empty")
}
goModPath := filepath.Join(path, "go.mod")
if _, err := os.Stat(goModPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("go.mod file not found in the specified path")
}
return fmt.Errorf("error checking go.mod file: %w", err)
}
for i, project := range a.settings.Projects {
if project.Name == oldName {
a.settings.Projects[i].Name = newName
a.settings.Projects[i].Path = path
return a.saveSettings()
}
}
return fmt.Errorf("project with name '%s' not found", oldName)
}

View File

@@ -0,0 +1,104 @@
package backend
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
func (a *App) getSettingsPath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
appConfigDir := filepath.Join(configDir, "quickly")
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
return "", err
}
return filepath.Join(appConfigDir, "settings.json"), nil
}
func (a *App) loadSettings() {
settingsPath, err := a.getSettingsPath()
if err != nil {
fmt.Println("Error getting settings path:", err)
a.settings = Settings{
Theme: "light",
Language: "zh-CN",
Notifications: true,
AutoStart: false,
MysqlModelPath: "",
DefaultQueryPackagePath: "",
ModelBasePath: "",
SwaggerDir: "",
Databases: []DatabaseConfig{},
Projects: []ProjectConfig{},
}
return
}
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
a.settings = Settings{
Theme: "light",
Language: "zh-CN",
Notifications: true,
AutoStart: false,
MysqlModelPath: "",
DefaultQueryPackagePath: "",
ModelBasePath: "",
SwaggerDir: "",
Databases: []DatabaseConfig{},
Projects: []ProjectConfig{},
}
return
}
fmt.Println("Error reading settings file:", err)
return
}
if err := json.Unmarshal(data, &a.settings); err != nil {
fmt.Println("Error parsing settings file:", err)
a.settings = Settings{
Theme: "light",
Language: "zh-CN",
Notifications: true,
AutoStart: false,
MysqlModelPath: "",
DefaultQueryPackagePath: "",
ModelBasePath: "",
SwaggerDir: "",
Databases: []DatabaseConfig{},
Projects: []ProjectConfig{},
}
}
}
func (a *App) saveSettings() error {
settingsPath, err := a.getSettingsPath()
if err != nil {
return fmt.Errorf("error getting settings path: %w", err)
}
data, err := json.MarshalIndent(a.settings, "", " ")
if err != nil {
return fmt.Errorf("error marshaling settings: %w", err)
}
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
return fmt.Errorf("error writing settings file: %w", err)
}
return nil
}
func (a *App) GetSettings() Settings {
return a.settings
}
func (a *App) SaveSettings(settings Settings) error {
a.settings = settings
return a.saveSettings()
}

View File

@@ -0,0 +1,239 @@
package backend
import (
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type SwaggerFile struct {
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
ModifiedTime string `json:"modifiedTime"`
}
type SwaggerServer struct {
server *http.Server
port int
running bool
mu sync.Mutex
}
var swaggerServer *SwaggerServer
var swaggerServerMu sync.Mutex
func (a *App) GetSwaggerFiles(dirPath string) ([]SwaggerFile, error) {
if dirPath == "" {
return nil, fmt.Errorf("directory path is empty")
}
var files []SwaggerFile
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(strings.ToLower(info.Name()), ".swagger.json") {
files = append(files, SwaggerFile{
Name: info.Name(),
Path: path,
Size: info.Size(),
ModifiedTime: info.ModTime().Format("2006-01-02 15:04:05"),
})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk directory: %w", err)
}
return files, nil
}
func (a *App) ReadSwaggerFile(filePath string) (string, error) {
if filePath == "" {
return "", fmt.Errorf("file path is empty")
}
content, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("failed to read file: %w", err)
}
return string(content), nil
}
func (a *App) StartSwaggerServer(dirPath string) (string, error) {
if dirPath == "" {
return "", fmt.Errorf("directory path is empty")
}
swaggerServerMu.Lock()
defer swaggerServerMu.Unlock()
if swaggerServer != nil && swaggerServer.running {
return fmt.Sprintf("http://localhost:%d", swaggerServer.port), nil
}
port := 8080
for i := 0; i < 100; i++ {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err == nil {
listener.Close()
break
}
port++
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
swaggerHTML := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui.css">
<style>
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
.topbar { display: none; }
.swagger-ui .topbar { display: none; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
const urlParams = new URLSearchParams(window.location.search);
const fileParam = urlParams.get('file');
const swaggerUrl = fileParam ? '/swagger.json?file=' + encodeURIComponent(fileParam) : '/swagger.json';
const ui = SwaggerUIBundle({
url: swaggerUrl,
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true,
showExtensions: true,
showCommonExtensions: true,
docExpansion: "list",
filter: true,
tryItOutEnabled: true
});
};
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(swaggerHTML))
})
mux.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) {
files, err := a.GetSwaggerFiles(dirPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(files) == 0 {
http.Error(w, "No swagger files found", http.StatusNotFound)
return
}
fileName := r.URL.Query().Get("file")
selectedFile := files[0]
if fileName != "" {
for _, file := range files {
if file.Name == fileName {
selectedFile = file
break
}
}
}
content, err := os.ReadFile(selectedFile.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(content)
})
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
swaggerServer = &SwaggerServer{
server: server,
port: port,
running: true,
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Swagger server error: %v\n", err)
}
}()
return fmt.Sprintf("http://localhost:%d", port), nil
}
func (a *App) StopSwaggerServer() error {
swaggerServerMu.Lock()
defer swaggerServerMu.Unlock()
if swaggerServer == nil || !swaggerServer.running {
return nil
}
if err := swaggerServer.server.Close(); err != nil {
return fmt.Errorf("failed to stop server: %w", err)
}
swaggerServer.running = false
return nil
}
func (a *App) IsSwaggerServerRunning() bool {
swaggerServerMu.Lock()
defer swaggerServerMu.Unlock()
if swaggerServer == nil {
return false
}
return swaggerServer.running
}
func (a *App) GetSwaggerServerURL() string {
swaggerServerMu.Lock()
defer swaggerServerMu.Unlock()
if swaggerServer == nil || !swaggerServer.running {
return ""
}
return fmt.Sprintf("http://localhost:%d", swaggerServer.port)
}

View File

@@ -0,0 +1,90 @@
package backend
import (
"fmt"
"os/exec"
"strings"
)
func (a *App) UpdateServiceCommon(commitId string, projectNames []string) (string, error) {
if commitId == "" {
return "", fmt.Errorf("commit id is empty")
}
if len(projectNames) == 0 {
return "", fmt.Errorf("no projects selected")
}
var results strings.Builder
results.WriteString(fmt.Sprintf("开始更新 Common 版本: %s\n", commitId))
results.WriteString(fmt.Sprintf("共选择 %d 个项目\n\n", len(projectNames)))
for _, projectName := range projectNames {
var projectPath string
found := false
for _, project := range a.settings.Projects {
if project.Name == projectName {
projectPath = project.Path
found = true
break
}
}
if !found {
results.WriteString(fmt.Sprintf("❌ 项目 '%s' 未找到配置\n\n", projectName))
continue
}
results.WriteString(fmt.Sprintf("正在更新项目: %s\n", projectName))
results.WriteString(fmt.Sprintf("路径: %s\n", projectPath))
if err := updateProjectCommonVersion(projectPath, commitId, &results, a.settings.ModelBasePath); err != nil {
results.WriteString(fmt.Sprintf("❌ 更新失败: %v\n\n", err))
continue
}
results.WriteString(fmt.Sprintf("✅ 更新成功\n\n"))
}
results.WriteString("所有项目更新完成!")
return results.String(), nil
}
func updateProjectCommonVersion(projectPath string, commitId string, results *strings.Builder, moduleName string) error {
goPath, err := exec.LookPath("go")
if err != nil {
return fmt.Errorf("Go not found: %w", err)
}
if moduleName == "" {
return fmt.Errorf("Go Module Name 未配置,请在设置中填写")
}
results.WriteString("执行: go get -u " + moduleName + "@" + commitId + "\n")
cmd := exec.Command(goPath, "get", "-u", fmt.Sprintf("%s@%s", moduleName, commitId))
cmd.Dir = projectPath
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("go get failed: %w\nOutput: %s", err, string(output))
}
results.WriteString(string(output))
results.WriteString("\n")
results.WriteString("执行: go mod tidy\n")
cmd = exec.Command(goPath, "mod", "tidy")
cmd.Dir = projectPath
output, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("go mod tidy failed: %w\nOutput: %s", err, string(output))
}
results.WriteString(string(output))
results.WriteString("\n")
return nil
}