feat: 部署初版测试
Some checks failed
Extension Build & Release / build (push) Failing after 1m5s
Backend Deploy (Go + Docker) / deploy (push) Failing after 1m40s
Web Console Deploy (Vue 3 + Vite) / deploy (push) Has been cancelled

This commit is contained in:
zs
2026-03-02 21:25:21 +08:00
parent db3abb3174
commit 8cf6cb944b
97 changed files with 10250 additions and 209 deletions

View File

@@ -1,53 +1,214 @@
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
chiMiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/joho/godotenv"
"github.com/zs/InsightReply/internal/handler"
appMiddleware "github.com/zs/InsightReply/internal/middleware"
"github.com/zs/InsightReply/internal/repository"
"github.com/zs/InsightReply/internal/service"
"github.com/zs/InsightReply/internal/worker"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
// Database connection (Using the provided string)
dsn := "postgres://root:QG%237X*HHt3CqbZ@100.64.0.5:5432/InsightReply?sslmode=disable"
// 1. Load environment variables
if err := godotenv.Load(); err != nil {
log.Println("No .env file found, relying on system environment variables")
}
// 1.5 Setup application file logging
if logPath := os.Getenv("LOG_FILE_PATH"); logPath != "" {
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
multiWriter := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(multiWriter)
log.Printf("Application logs are now being mirrored to: %s", logPath)
} else {
log.Printf("Failed to open log file %s: %v", logPath, err)
}
}
// 2. Database connection setup
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
log.Fatal("DATABASE_URL environment variable is required")
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database:", err)
}
fmt.Println("Database connection established")
// Initialize Layers
// 2.1 Run Database Migrations
log.Println("Running database migrations...")
m, err := migrate.New("file://migrations", dsn)
if err != nil {
log.Printf("Failed to initialize migrate, skipping: %v", err)
} else {
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Printf("Failed to run migrate (maybe tables already exist), continuing: %v", err)
} else {
log.Println("Database migrations applied successfully")
}
}
// 3. Initialize Layers
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
// AI Service (Using Env Var for API Key)
apiKey := os.Getenv("OPENAI_API_KEY")
aiSvc := service.NewAIService(apiKey)
aiHandler := handler.NewAIHandler(aiSvc)
profileRepo := repository.NewProductProfileRepository(db)
profileSvc := service.NewProductProfileService(profileRepo)
profileHandler := handler.NewProductProfileHandler(profileSvc)
// Router setup
strategyRepo := repository.NewCustomStrategyRepository(db)
strategySvc := service.NewCustomStrategyService(strategyRepo)
strategyHandler := handler.NewCustomStrategyHandler(strategySvc)
monitorRepo := repository.NewCompetitorMonitorRepository(db)
monitorSvc := service.NewCompetitorMonitorService(monitorRepo)
monitorHandler := handler.NewCompetitorMonitorHandler(monitorSvc)
// AI Service (Multi-LLM Routing Support)
aiSvc := service.NewAIService()
aiHandler := handler.NewAIHandler(aiSvc, profileSvc, strategySvc)
// Start Background Workers & Tweet Handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Ensure cleanup on exit
tweetRepo := repository.NewTweetRepository(db)
replyRepo := repository.NewReplyRepository(db)
tweetHandler := handler.NewTweetHandler(tweetRepo) // Mount API handler
replyHandler := handler.NewReplyHandler(replyRepo)
monitorWorker := worker.NewMonitorWorker(monitorRepo, tweetRepo)
go monitorWorker.Start(ctx, 15*time.Minute)
performanceWorker := worker.NewPerformanceWorker(replyRepo, aiSvc)
go performanceWorker.Start(ctx, 30*time.Minute) // Runs every 30 minutes to check 24h+ old replies
// 4. Router setup
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(chiMiddleware.Logger)
r.Use(chiMiddleware.Recoverer)
r.Route("/api/v1", func(r chi.Router) {
r.Post("/users/register", userHandler.Register)
r.Get("/ai/test", aiHandler.Test)
r.Post("/ai/generate", aiHandler.Generate)
// CORS Configuration
corsOrigins := os.Getenv("CORS_ORIGINS")
if corsOrigins == "" {
corsOrigins = "*" // default fallback
}
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{corsOrigins},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300,
}))
// 5. Routes
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code":200,"message":"ok","data":{"status":"healthy","version":"1.0.0"}}`))
})
fmt.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
r.Route("/api/v1", func(r chi.Router) {
// Public routes
r.Post("/users/register", userHandler.Register)
// Protected routes
r.Group(func(r chi.Router) {
r.Use(appMiddleware.JWTAuth)
r.Use(appMiddleware.RateLimit(db))
// User and Profile APIs
r.Get("/users/me", userHandler.GetProfile)
r.Put("/users/me/preferences", userHandler.UpdatePreferences)
r.Get("/users/me/product_profiles", profileHandler.GetProfile)
r.Put("/users/me/product_profiles", profileHandler.SaveProfile)
// Strategy APIs
r.Get("/users/me/strategies", strategyHandler.ListStrategies)
r.Post("/users/me/strategies", strategyHandler.CreateStrategy)
r.Delete("/users/me/strategies/{id}", strategyHandler.DeleteStrategy)
// Monitor APIs
r.Get("/monitors/competitors", monitorHandler.ListMonitors)
r.Post("/monitors/competitors", monitorHandler.CreateMonitor)
r.Delete("/monitors/competitors/{id}", monitorHandler.DeleteMonitor)
// Hot Opportunity Tweers API
r.Get("/tweets/hot", tweetHandler.GetHotTweets)
r.Get("/tweets/search", tweetHandler.GetSearchTweets)
// AI APIs
r.Get("/ai/test", aiHandler.Test)
r.Post("/ai/generate", aiHandler.Generate)
r.Post("/replies/record", replyHandler.RecordReply)
r.Get("/replies", replyHandler.GetReplies)
})
})
// 6. Graceful Shutdown Setup
port := os.Getenv("SERVER_PORT")
if port == "" {
port = "8080"
}
srv := &http.Server{
Addr: ":" + port,
Handler: r,
}
serverErrors := make(chan error, 1)
// Start the server
go func() {
fmt.Printf("Server starting on :%s\n", port)
serverErrors <- srv.ListenAndServe()
}()
// Channel to listen for an interrupt or terminate signal from the OS.
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
// Blocking main and waiting for shutdown.
select {
case err := <-serverErrors:
log.Fatalf("Error starting server: %v", err)
case sig := <-shutdown:
log.Printf("Start shutdown... signal: %v", sig)
// Create context with timeout for shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Graceful shutdown did not complete in time: %v", err)
if err := srv.Close(); err != nil {
log.Fatalf("Could not stop server gracefully: %v", err)
}
}
}
log.Println("Server stopped")
}