package main import ( "compress/gzip" "embed" "fmt" "io/fs" "log" "net/url" "os" "strings" "verification-platform-backend/internal/config" "verification-platform-backend/internal/database" "verification-platform-backend/internal/middleware" "verification-platform-backend/internal/router" "verification-platform-backend/internal/scheduler" "verification-platform-backend/internal/service" "verification-platform-backend/pkg/logger" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" ) //go:embed all:embedded/dist var embeddedFiles embed.FS func main() { if len(os.Args) > 1 { switch os.Args[1] { case "list-admin": listAdmins() case "reset-password": resetPassword() case "create-admin": createAdmin() case "help", "-h", "--help": printHelp() default: fmt.Printf("未知命令: %s\n", os.Args[1]) printHelp() os.Exit(1) } return } startServer() } func printHelp() { fmt.Println("用法:") fmt.Println(" verify 启动服务器") fmt.Println(" verify list-admin 列出所有管理员账号") fmt.Println(" verify reset-password <用户名> [新密码] 重置管理员密码") fmt.Println(" verify create-admin <用户名> <密码> [邮箱] 创建新管理员") fmt.Println(" verify help 显示帮助信息") fmt.Println() fmt.Println("示例:") fmt.Println(" verify list-admin") fmt.Println(" verify reset-password admin") fmt.Println(" verify reset-password admin newpass") fmt.Println(" verify create-admin newadmin password123 admin@example.com") } func listAdmins() { config.Init() database.Init() var users []struct { ID uint Username string Email *string Role string Status string CreatedAt string } if err := database.DB.Table("users").Where("role = ?", "admin").Find(&users).Error; err != nil { fmt.Printf("错误: 查询失败 - %v\n", err) os.Exit(1) } if len(users) == 0 { fmt.Println("没有找到管理员账号") return } fmt.Println("管理员账号列表:") fmt.Println("----------------------------------------") for _, u := range users { email := "" if u.Email != nil { email = *u.Email } fmt.Printf("ID: %d 用户名: %s 邮箱: %s 状态: %s\n", u.ID, u.Username, email, u.Status) } fmt.Println("----------------------------------------") } func createAdmin() { config.Init() database.Init() if len(os.Args) < 4 { fmt.Println("用法: verify create-admin <用户名> <密码> [邮箱]") os.Exit(1) } username := os.Args[2] password := os.Args[3] email := "" if len(os.Args) >= 5 { email = os.Args[4] } if len(password) < 6 { fmt.Println("错误: 密码长度至少6位") os.Exit(1) } var count int64 database.DB.Table("users").Where("username = ?", username).Count(&count) if count > 0 { fmt.Printf("错误: 用户名 '%s' 已存在\n", username) os.Exit(1) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { fmt.Printf("错误: 密码加密失败 - %v\n", err) os.Exit(1) } emailPtr := (*string)(nil) if email != "" { emailPtr = &email } if err := database.DB.Table("users").Create(map[string]interface{}{ "username": username, "password": string(hashedPassword), "email": emailPtr, "role": "admin", "status": "active", }).Error; err != nil { fmt.Printf("错误: 创建失败 - %v\n", err) os.Exit(1) } fmt.Printf("成功: 管理员账号 '%s' 已创建\n", username) } func resetPassword() { config.Init() database.Init() if len(os.Args) < 3 { fmt.Println("用法: verify reset-password <用户名> [新密码]") os.Exit(1) } username := os.Args[2] newPassword := "" if len(os.Args) >= 4 { newPassword = os.Args[3] } var user struct { ID uint Username string Role string } if err := database.DB.Table("users").Where("username = ? AND role = ?", username, "admin").First(&user).Error; err != nil { fmt.Printf("错误: 未找到管理员用户 '%s'\n", username) os.Exit(1) } if newPassword == "" { newPassword = generateRandomPassword(12) } if len(newPassword) < 6 { fmt.Println("错误: 密码长度至少6位") os.Exit(1) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { fmt.Printf("错误: 密码加密失败 - %v\n", err) os.Exit(1) } if err := database.DB.Table("users").Where("id = ?", user.ID).Update("password", string(hashedPassword)).Error; err != nil { fmt.Printf("错误: 更新密码失败 - %v\n", err) os.Exit(1) } fmt.Printf("成功: 用户 '%s' 的密码已重置\n", username) fmt.Printf("新密码: %s\n", newPassword) } func generateRandomPassword(length int) string { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%" result := make([]byte, length) for i := range result { result[i] = chars[i%len(chars)] } for i := len(result) - 1; i > 0; i-- { j := i % len(chars) result[i], result[j] = result[j], result[i] } return string(result) } func startServer() { config.Init() logger.Init() database.Init() os.MkdirAll("uploads", 0755) if config.GetString("app.env") == "production" { gin.SetMode(gin.ReleaseMode) } r := gin.Default() r.MaxMultipartMemory = 32 << 20 r.Use(middleware.Logger()) r.Use(middleware.Recovery()) r.Use(middleware.Cors()) r.Use(gzipMiddleware()) r.Use(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/uploads/") { c.Header("Cache-Control", "public, max-age=31536000, immutable") } c.Next() }) r.Static("/uploads", "uploads") router.SetupRoutes(r) setupEmbeddedFrontend(r) scheduler.StartCleanupScheduler() port := config.GetString("app.port") if port == "" { port = "8080" } log.Printf("Server starting on port %s", port) if err := r.Run(":" + port); err != nil { log.Fatal("Failed to start server: ", err) } } func setupEmbeddedFrontend(r *gin.Engine) { distFS, err := fs.Sub(embeddedFiles, "embedded/dist") if err != nil { log.Printf("Warning: embedded dist not found, frontend will not be served") return } indexHTML, err := fs.ReadFile(distFS, "index.html") if err != nil { log.Printf("Warning: index.html not found in embedded dist") return } r.GET("/assets/*filepath", func(c *gin.Context) { filepath := c.Param("filepath") filepath = strings.TrimPrefix(filepath, "/") decodedPath, err := url.PathUnescape(filepath) if err != nil { decodedPath = filepath } fullPath := "assets/" + decodedPath data, err := fs.ReadFile(distFS, fullPath) if err != nil { c.Status(404) return } contentType := getContentTypeByExtension(decodedPath) c.Header("Cache-Control", "public, max-age=31536000, immutable") c.Data(200, contentType, data) }) r.NoRoute(func(c *gin.Context) { if c.Request.Method != "GET" || strings.HasPrefix(c.Request.URL.Path, "/api/") { c.JSON(404, gin.H{"error": "not found"}) return } path := strings.TrimPrefix(c.Request.URL.Path, "/") decodedPath, err := url.PathUnescape(path) if err != nil { decodedPath = path } if data, err := fs.ReadFile(distFS, decodedPath); err == nil { contentType := getContentTypeByExtension(decodedPath) c.Data(200, contentType, data) return } c.Header("Cache-Control", "no-cache, no-store, must-revalidate") serveIndexHTML(c, string(indexHTML)) }) } func serveIndexHTML(c *gin.Context, indexHTML string) { settingService := service.NewSettingService() settings, err := settingService.GetSettingsByCategory("basic") if err != nil { settings = make(map[string]interface{}) } siteName := getSettingString(settings, "site_name", "微授权") siteDescription := getSettingString(settings, "site_description", "专业的应用验证平台") siteLogo := getSettingString(settings, "site_logo", "") siteFavicon := getSettingString(settings, "site_favicon", "") html := indexHTML html = strings.ReplaceAll(html, "{{.SiteName}}", siteName) html = strings.ReplaceAll(html, "{{.SiteDescription}}", siteDescription) html = strings.ReplaceAll(html, "{{.SiteKeywords}}", "验证平台,软件授权,卡密验证") html = strings.ReplaceAll(html, "{{.LogoLight}}", siteLogo) html = strings.ReplaceAll(html, "{{.LogoDark}}", siteLogo) html = strings.ReplaceAll(html, "{{.Favicon}}", siteFavicon) c.Data(200, "text/html; charset=utf-8", []byte(html)) } var fallbackMimeTypes = map[string]string{ ".js": "application/javascript; charset=utf-8", ".mjs": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".html": "text/html; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".ico": "image/x-icon", ".webp": "image/webp", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject", ".xml": "application/xml", ".txt": "text/plain; charset=utf-8", } func getContentTypeByExtension(filename string) string { ext := strings.ToLower(filename[strings.LastIndex(filename, "."):]) if ct, ok := fallbackMimeTypes[ext]; ok { return ct } return "application/octet-stream" } func getSettingString(settings map[string]interface{}, key, defaultValue string) string { if val, ok := settings[key]; ok { if str, ok := val.(string); ok && str != "" { return str } } return defaultValue } func isCompressibleContentType(contentType string) bool { return strings.HasPrefix(contentType, "text/") || strings.Contains(contentType, "javascript") || strings.Contains(contentType, "json") || strings.Contains(contentType, "xml") || strings.Contains(contentType, "svg") } func gzipMiddleware() gin.HandlerFunc { return func(c *gin.Context) { if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") { c.Next() return } c.Header("Vary", "Accept-Encoding") gw := &gzipResponseWriter{ResponseWriter: c.Writer, writer: gzip.NewWriter(c.Writer)} c.Writer = gw c.Next() if gw.compressed { gw.writer.Close() } } } type gzipResponseWriter struct { gin.ResponseWriter writer *gzip.Writer compressed bool } func (w *gzipResponseWriter) Write(data []byte) (int, error) { if !w.compressed { contentType := w.Header().Get("Content-Type") if isCompressibleContentType(contentType) { w.Header().Del("Content-Length") w.Header().Set("Content-Encoding", "gzip") w.compressed = true } } if w.compressed { return w.writer.Write(data) } return w.ResponseWriter.Write(data) } func (w *gzipResponseWriter) WriteString(s string) (int, error) { if !w.compressed { contentType := w.Header().Get("Content-Type") if isCompressibleContentType(contentType) { w.Header().Del("Content-Length") w.Header().Set("Content-Encoding", "gzip") w.compressed = true } } if w.compressed { return w.writer.Write([]byte(s)) } return w.ResponseWriter.WriteString(s) }