refactor(grpc): restructure task handling and migrate to slog
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:
2026-06-09 23:08:03 +08:00
parent ee5c36f3ac
commit accc6567f2
26 changed files with 720 additions and 1442 deletions

176
main.go
View File

@@ -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": "任务执行成功"})
}
}