refactor: 重构代码架构并使用 Modern Go 特性
- 重命名模块 jwts -> schedule_converter
- 新增 service 包,统一业务逻辑层
- 新增 pkg/logger 包,使用 slog 结构化日志
- 新增 pkg/errors 包,统一错误处理
- 拆分 captcha 包,图片识别移至 service/recognizer
- 清理 models 包,只保留数据结构定义
- 消除 grpc/handler.go 中的重复代码
- 简化 main.go,使用 slog 和 context
- 配置支持单例模式和结构化配置
- 使用 any 替代 interface{}
- 使用 sync.Map 替代 map + RWMutex
This commit is contained in:
246
main.go
246
main.go
@@ -4,20 +4,18 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"jwts/auth"
|
||||
"jwts/captcha"
|
||||
"jwts/client"
|
||||
"jwts/config"
|
||||
grpcClient "jwts/grpc"
|
||||
"jwts/models"
|
||||
"jwts/parser"
|
||||
"schedule_converter/client"
|
||||
"schedule_converter/config"
|
||||
grpcClient "schedule_converter/grpc"
|
||||
"schedule_converter/pkg/logger"
|
||||
"schedule_converter/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -25,8 +23,17 @@ import (
|
||||
func main() {
|
||||
mode := flag.String("mode", "http", "运行模式: http 或 grpc")
|
||||
grpcAddr := flag.String("grpc-addr", "localhost:50051", "gRPC 服务器地址")
|
||||
debug := flag.Bool("debug", false, "启用调试模式")
|
||||
flag.Parse()
|
||||
|
||||
if *debug {
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
logger.SetTextHandler()
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
slog.Info("配置加载完成", "mode", *mode, "base_url", cfg.BaseURL)
|
||||
|
||||
if *mode == "grpc" {
|
||||
runGRPCClient(*grpcAddr)
|
||||
} else {
|
||||
@@ -34,7 +41,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// runGRPCClient 运行 gRPC 客户端模式
|
||||
func runGRPCClient(serverAddr string) {
|
||||
runnerID := os.Getenv("RUNNER_ID")
|
||||
if runnerID == "" {
|
||||
@@ -47,7 +53,7 @@ func runGRPCClient(serverAddr string) {
|
||||
version = "1.0.0"
|
||||
}
|
||||
|
||||
log.Printf("启动 gRPC 客户端, Runner ID: %s, 版本: %s", runnerID, version)
|
||||
slog.Info("启动 gRPC 客户端", "runner_id", runnerID, "version", version)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -56,8 +62,8 @@ func runGRPCClient(serverAddr string) {
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Println("正在关闭...")
|
||||
sig := <-sigChan
|
||||
slog.Info("收到信号,正在关闭...", "signal", sig)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
@@ -66,161 +72,147 @@ func runGRPCClient(serverAddr string) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("客户端已停止")
|
||||
slog.Info("客户端已停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
client := grpcClient.NewClient(serverAddr, runnerID, version)
|
||||
c := grpcClient.NewClient(serverAddr, runnerID, version)
|
||||
handler := grpcClient.NewTaskHandler()
|
||||
client.SetHandler(handler)
|
||||
c.SetHandler(handler)
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Printf("连接失败: %v, %v 后重试...", err, reconnectInterval)
|
||||
if err := c.Connect(ctx); err != nil {
|
||||
slog.Warn("连接失败,稍后重试", "error", err, "retry_after", reconnectInterval)
|
||||
time.Sleep(reconnectInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
heartbeatCtx, heartbeatCancel := context.WithCancel(ctx)
|
||||
go client.HeartbeatLoop(heartbeatCtx, 30*time.Second)
|
||||
go c.HeartbeatLoop(heartbeatCtx, 30*time.Second)
|
||||
|
||||
err := client.Run(ctx)
|
||||
err := c.Run(ctx)
|
||||
heartbeatCancel()
|
||||
client.Close()
|
||||
c.Close()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("连接断开: %v, %v 后重连...", err, reconnectInterval)
|
||||
slog.Warn("连接断开,稍后重连", "error", err, "retry_after", reconnectInterval)
|
||||
} else {
|
||||
log.Printf("连接已关闭, %v 后重连...", reconnectInterval)
|
||||
slog.Info("连接已关闭,稍后重连", "retry_after", reconnectInterval)
|
||||
}
|
||||
time.Sleep(reconnectInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// runHTTPServer 运行 HTTP 服务器模式
|
||||
func runHTTPServer() {
|
||||
// 初始化客户端
|
||||
client.InitClient()
|
||||
|
||||
r := gin.Default()
|
||||
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",
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
// 课表图片识别接口
|
||||
r.POST("/recognize", func(c *gin.Context) {
|
||||
file, header, err := c.Request.FormFile("image")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请上传图片文件", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
r.POST("/recognize", handleRecognize)
|
||||
r.POST("/task", handleTask)
|
||||
|
||||
log.Printf("收到图片上传: %s, 大小: %d bytes", header.Filename, 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 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取图片失败", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
courses, err := captcha.RecognizeScheduleFromBytes(imageData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "识别失败", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
models.PrintCourses(courses)
|
||||
|
||||
err = models.SaveToJSON(courses, "schedule_result.json")
|
||||
if err != nil {
|
||||
log.Printf("保存JSON失败: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "识别成功",
|
||||
"courses": courses,
|
||||
"count": len(courses),
|
||||
})
|
||||
})
|
||||
|
||||
// 任务处理接口
|
||||
r.POST("/task", func(c *gin.Context) {
|
||||
// 这里可以根据请求体参数决定执行什么任务
|
||||
// 目前默认执行原有的登录和获取课表逻辑
|
||||
if config.Username == "" || config.Password == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "环境变量未设置"})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
err := runTask()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "任务执行成功"})
|
||||
})
|
||||
|
||||
// 启动服务
|
||||
log.Println("Server starting on :8080")
|
||||
slog.Info("HTTP 服务器启动", "port", 8080)
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatalf("Failed to run server: %v", err)
|
||||
slog.Error("服务器启动失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runTask() error {
|
||||
// 步骤1: 获取初始Cookie
|
||||
err := client.GetInitialCookies()
|
||||
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 {
|
||||
log.Printf("获取Cookie失败: %v", err)
|
||||
return err
|
||||
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
|
||||
}
|
||||
|
||||
// 重试登录
|
||||
maxRetries := 3
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
log.Printf("尝试登录 (第%d次)...", attempt)
|
||||
|
||||
// 获取并识别验证码
|
||||
captchaCode, err := captcha.GetAndRecognize(client.Client)
|
||||
if err != nil {
|
||||
log.Printf(" 验证码获取失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 登录
|
||||
success, err := auth.Login(captchaCode)
|
||||
if err != nil {
|
||||
log.Printf(" 登录失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if success {
|
||||
// 获取课表
|
||||
_, err = parser.FetchSchedule(client.Client)
|
||||
if err != nil {
|
||||
log.Printf("获取课表失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println(" 登录失败,3秒后重试...")
|
||||
imageData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
slog.Error("读取图片失败", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取图片失败", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
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