refactor(grpc): restructure task handling and migrate to slog
All checks were successful
Build / build (push) Successful in 5m28s
All checks were successful
Build / build (push) Successful in 5m28s
Refactor the gRPC client and task handling logic to improve modularity, error handling, and observability. - Split `grpc/handler.go` into specialized handler files (login, classroom, exams, grades, schedule) to reduce complexity. - Replace standard `log` with `log/slog` throughout the project for structured logging. - Refactor `grpc/client.go` to use a thread-safe connection management pattern with `sync.RWMutex`. - Remove the legacy `service` package and HTTP server implementation, transitioning the application to a pure gRPC runner mode. - Clean up `config` and `pkg/errors` to remove unused utilities and simplify the API. - Improve error reporting in parsers and HTTP clients by checking response status codes.
This commit is contained in:
176
main.go
176
main.go
@@ -3,26 +3,29 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"schedule_converter/client"
|
||||
"schedule_converter/config"
|
||||
grpcClient "schedule_converter/grpc"
|
||||
"schedule_converter/pkg/logger"
|
||||
"schedule_converter/service"
|
||||
)
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
const (
|
||||
defaultGRPCAddr = "localhost:50051"
|
||||
runnerIDPrefix = "runner-"
|
||||
defaultVersion = "1.0.0"
|
||||
initialReconnectInterval = 5 * time.Second
|
||||
maxReconnectInterval = 60 * time.Second
|
||||
heartbeatInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
func main() {
|
||||
mode := flag.String("mode", "http", "运行模式: http 或 grpc")
|
||||
grpcAddr := flag.String("grpc-addr", "localhost:50051", "gRPC 服务器地址")
|
||||
grpcAddr := flag.String("grpc-addr", defaultGRPCAddr, "gRPC 服务器地址")
|
||||
debug := flag.Bool("debug", false, "启用调试模式")
|
||||
flag.Parse()
|
||||
|
||||
@@ -32,26 +35,31 @@ func main() {
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
slog.Info("配置加载完成", "mode", *mode, "base_url", cfg.BaseURL)
|
||||
slog.Info("配置加载完成", "base_url", cfg.BaseURL)
|
||||
|
||||
if *mode == "grpc" {
|
||||
runGRPCClient(*grpcAddr)
|
||||
} else {
|
||||
runHTTPServer()
|
||||
runGRPCClient(*grpcAddr)
|
||||
}
|
||||
|
||||
func resolveRunnerID() string {
|
||||
if id := os.Getenv("RUNNER_ID"); id != "" {
|
||||
return id
|
||||
}
|
||||
if hostname, err := os.Hostname(); err == nil && hostname != "" {
|
||||
return runnerIDPrefix + hostname
|
||||
}
|
||||
return fmt.Sprintf("%s%d", runnerIDPrefix, os.Getpid())
|
||||
}
|
||||
|
||||
func resolveVersion() string {
|
||||
if v := os.Getenv("RUNNER_VERSION"); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultVersion
|
||||
}
|
||||
|
||||
func runGRPCClient(serverAddr string) {
|
||||
runnerID := os.Getenv("RUNNER_ID")
|
||||
if runnerID == "" {
|
||||
hostname, _ := os.Hostname()
|
||||
runnerID = "runner-" + hostname
|
||||
}
|
||||
|
||||
version := os.Getenv("RUNNER_VERSION")
|
||||
if version == "" {
|
||||
version = "1.0.0"
|
||||
}
|
||||
runnerID := resolveRunnerID()
|
||||
version := resolveVersion()
|
||||
|
||||
slog.Info("启动 gRPC 客户端", "runner_id", runnerID, "version", version)
|
||||
|
||||
@@ -67,8 +75,6 @@ func runGRPCClient(serverAddr string) {
|
||||
cancel()
|
||||
}()
|
||||
|
||||
reconnectInterval := 5 * time.Second
|
||||
const maxReconnectInterval = 60 * time.Second
|
||||
retryCount := 0
|
||||
|
||||
for {
|
||||
@@ -80,9 +86,9 @@ func runGRPCClient(serverAddr string) {
|
||||
}
|
||||
|
||||
retryCount++
|
||||
currentInterval := reconnectInterval
|
||||
currentInterval := initialReconnectInterval
|
||||
if retryCount > 1 {
|
||||
currentInterval = min(reconnectInterval*time.Duration(retryCount-1), maxReconnectInterval)
|
||||
currentInterval = min(initialReconnectInterval*time.Duration(retryCount-1), maxReconnectInterval)
|
||||
}
|
||||
|
||||
slog.Info("正在连接服务器", "attempt", retryCount, "server", serverAddr)
|
||||
@@ -101,7 +107,7 @@ func runGRPCClient(serverAddr string) {
|
||||
retryCount = 0
|
||||
|
||||
heartbeatCtx, heartbeatCancel := context.WithCancel(ctx)
|
||||
go c.HeartbeatLoop(heartbeatCtx, 30*time.Second)
|
||||
go c.HeartbeatLoop(heartbeatCtx, heartbeatInterval)
|
||||
|
||||
err := c.Run(ctx)
|
||||
heartbeatCancel()
|
||||
@@ -112,120 +118,10 @@ func runGRPCClient(serverAddr string) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
slog.Warn("连接断开", "error", err, "retry_after", reconnectInterval)
|
||||
slog.Warn("连接断开", "error", err, "retry_after", initialReconnectInterval)
|
||||
} else {
|
||||
slog.Info("连接已关闭")
|
||||
}
|
||||
time.Sleep(reconnectInterval)
|
||||
time.Sleep(initialReconnectInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func runHTTPServer() {
|
||||
client.InitClient()
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(requestLogger())
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.POST("/recognize", handleRecognize)
|
||||
r.POST("/task", handleTask)
|
||||
|
||||
slog.Info("HTTP 服务器启动", "port", 8080)
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
slog.Error("服务器启动失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func requestLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
status := c.Writer.Status()
|
||||
|
||||
if query != "" {
|
||||
path = path + "?" + query
|
||||
}
|
||||
|
||||
slog.Info("HTTP请求",
|
||||
"method", c.Request.Method,
|
||||
"path", path,
|
||||
"status", status,
|
||||
"latency", latency.String(),
|
||||
"client_ip", c.ClientIP(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRecognize(c *gin.Context) {
|
||||
file, header, err := c.Request.FormFile("image")
|
||||
if err != nil {
|
||||
slog.Warn("图片上传失败", "error", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请上传图片文件", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
slog.Info("收到图片上传", "filename", header.Filename, "size", header.Size)
|
||||
|
||||
if header.Size > 10*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "图片文件过大", "max_size": "10MB"})
|
||||
return
|
||||
}
|
||||
|
||||
imageData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
slog.Error("读取图片失败", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取图片失败", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
recognizer := service.NewRecognizerService()
|
||||
courses, err := recognizer.RecognizeFromBytes(c.Request.Context(), imageData)
|
||||
if err != nil {
|
||||
slog.Error("识别失败", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "识别失败", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
service.PrintCourses(courses)
|
||||
|
||||
if err := service.SaveToJSON(courses, "schedule_result.json"); err != nil {
|
||||
slog.Warn("保存JSON失败", "error", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "识别成功",
|
||||
"courses": courses,
|
||||
"count": len(courses),
|
||||
})
|
||||
}
|
||||
|
||||
func handleTask(c *gin.Context) {
|
||||
cfg := config.Get()
|
||||
if cfg.Username == "" || cfg.Password == "" {
|
||||
slog.Warn("环境变量未设置")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "环境变量未设置"})
|
||||
return
|
||||
}
|
||||
|
||||
scheduleSvc := service.NewScheduleService()
|
||||
_, err := scheduleSvc.FetchSchedule(c.Request.Context(), cfg.Username, cfg.Password, cfg.Semester)
|
||||
if err != nil {
|
||||
slog.Error("任务执行失败", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("任务执行成功")
|
||||
c.JSON(http.StatusOK, gin.H{"message": "任务执行成功"})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user