feat(grpc): add gRPC server infrastructure and schedule sync service
Add gRPC server support with configurable TLS, environment-based settings, and Wire injection. Implement schedule synchronization service with task runner integration for external course data fetching. - Add gRPC configuration with env var overrides (APP_GRPC_ENABLED, APP_GRPC_PORT, etc.) - Create gRPC server infrastructure with runner task management - Implement ScheduleSyncService for course data synchronization - Add sync endpoint to schedule handler for external system integration - Update repository with context-aware batch delete operation - Wire all dependencies including zap logger for structured logging
This commit is contained in:
@@ -190,3 +190,14 @@ conversation_cache:
|
|||||||
batch_threshold: 100 # 条数阈值
|
batch_threshold: 100 # 条数阈值
|
||||||
batch_max_size: 500 # 单次最大批量
|
batch_max_size: 500 # 单次最大批量
|
||||||
buffer_max_size: 10000 # 写缓冲最大条数
|
buffer_max_size: 10000 # 写缓冲最大条数
|
||||||
|
|
||||||
|
# gRPC 服务器配置
|
||||||
|
# 环境变量:
|
||||||
|
# APP_GRPC_ENABLED, APP_GRPC_PORT
|
||||||
|
# APP_GRPC_TLS_ENABLED, APP_GRPC_TLS_CERT_FILE, APP_GRPC_TLS_KEY_FILE
|
||||||
|
grpc:
|
||||||
|
enabled: true
|
||||||
|
port: 50051
|
||||||
|
tls_enabled: false
|
||||||
|
tls_cert_file: ""
|
||||||
|
tls_key_file: ""
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -15,6 +15,8 @@ require (
|
|||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
golang.org/x/image v0.24.0
|
golang.org/x/image v0.24.0
|
||||||
|
google.golang.org/grpc v1.59.0
|
||||||
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
gorm.io/driver/postgres v1.5.4
|
gorm.io/driver/postgres v1.5.4
|
||||||
gorm.io/driver/sqlite v1.5.4
|
gorm.io/driver/sqlite v1.5.4
|
||||||
@@ -38,6 +40,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
@@ -79,7 +82,7 @@ require (
|
|||||||
golang.org/x/net v0.28.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sys v0.23.0 // indirect
|
golang.org/x/sys v0.23.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
7
go.sum
7
go.sum
@@ -57,6 +57,8 @@ github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -192,7 +194,12 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||||
|
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||||
|
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Config struct {
|
|||||||
OpenAI OpenAIConfig `mapstructure:"openai"`
|
OpenAI OpenAIConfig `mapstructure:"openai"`
|
||||||
Email EmailConfig `mapstructure:"email"`
|
Email EmailConfig `mapstructure:"email"`
|
||||||
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
|
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
|
||||||
|
GRPC GRPCConfig `mapstructure:"grpc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 加载配置文件
|
// Load 加载配置文件
|
||||||
@@ -127,6 +128,12 @@ func Load(configPath string) (*Config, error) {
|
|||||||
viper.SetDefault("conversation_cache.batch_threshold", 100)
|
viper.SetDefault("conversation_cache.batch_threshold", 100)
|
||||||
viper.SetDefault("conversation_cache.batch_max_size", 500)
|
viper.SetDefault("conversation_cache.batch_max_size", 500)
|
||||||
viper.SetDefault("conversation_cache.buffer_max_size", 10000)
|
viper.SetDefault("conversation_cache.buffer_max_size", 10000)
|
||||||
|
// GRPC 默认值
|
||||||
|
viper.SetDefault("grpc.enabled", true)
|
||||||
|
viper.SetDefault("grpc.port", 50051)
|
||||||
|
viper.SetDefault("grpc.tls_enabled", false)
|
||||||
|
viper.SetDefault("grpc.tls_cert_file", "")
|
||||||
|
viper.SetDefault("grpc.tls_key_file", "")
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||||
@@ -202,6 +209,12 @@ func Load(configPath string) (*Config, error) {
|
|||||||
cfg.Email.UseTLS, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_USE_TLS", fmt.Sprintf("%t", cfg.Email.UseTLS)))
|
cfg.Email.UseTLS, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_USE_TLS", fmt.Sprintf("%t", cfg.Email.UseTLS)))
|
||||||
cfg.Email.InsecureSkipVerify, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_INSECURE_SKIP_VERIFY", fmt.Sprintf("%t", cfg.Email.InsecureSkipVerify)))
|
cfg.Email.InsecureSkipVerify, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_INSECURE_SKIP_VERIFY", fmt.Sprintf("%t", cfg.Email.InsecureSkipVerify)))
|
||||||
cfg.Email.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_TIMEOUT", fmt.Sprintf("%d", cfg.Email.Timeout)))
|
cfg.Email.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_TIMEOUT", fmt.Sprintf("%d", cfg.Email.Timeout)))
|
||||||
|
// GRPC 环境变量覆盖
|
||||||
|
cfg.GRPC.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_ENABLED", fmt.Sprintf("%t", cfg.GRPC.Enabled)))
|
||||||
|
cfg.GRPC.Port, _ = strconv.Atoi(getEnvOrDefault("APP_GRPC_PORT", fmt.Sprintf("%d", cfg.GRPC.Port)))
|
||||||
|
cfg.GRPC.TLSEnabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_TLS_ENABLED", fmt.Sprintf("%t", cfg.GRPC.TLSEnabled)))
|
||||||
|
cfg.GRPC.TLSCertFile = getEnvOrDefault("APP_GRPC_TLS_CERT_FILE", cfg.GRPC.TLSCertFile)
|
||||||
|
cfg.GRPC.TLSKeyFile = getEnvOrDefault("APP_GRPC_TLS_KEY_FILE", cfg.GRPC.TLSKeyFile)
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
17
internal/config/grpc.go
Normal file
17
internal/config/grpc.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// GRPCConfig gRPC 服务器配置
|
||||||
|
type GRPCConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
TLSEnabled bool `mapstructure:"tls_enabled"`
|
||||||
|
TLSCertFile string `mapstructure:"tls_cert_file"`
|
||||||
|
TLSKeyFile string `mapstructure:"tls_key_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address 返回 gRPC 服务器监听地址
|
||||||
|
func (c *GRPCConfig) Address() string {
|
||||||
|
return fmt.Sprintf(":%d", c.Port)
|
||||||
|
}
|
||||||
299
internal/grpc/runner/hub.go
Normal file
299
internal/grpc/runner/hub.go
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"carrot_bbs/proto/runner"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunnerConnection 表示一个已连接的 Runner
|
||||||
|
type RunnerConnection struct {
|
||||||
|
ID string
|
||||||
|
Version string
|
||||||
|
Capabilities map[string]string
|
||||||
|
Stream runner.RunnerHub_ConnectServer
|
||||||
|
LastSeen time.Time
|
||||||
|
sendChan chan *runner.StreamMessage
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerHub 管理 Runner 连接和任务分发
|
||||||
|
type RunnerHub struct {
|
||||||
|
runner.UnimplementedRunnerHubServer // 嵌入以实现 gRPC 接口
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
runners map[string]*RunnerConnection
|
||||||
|
taskManager *TaskManager
|
||||||
|
registerChan chan *RunnerConnection
|
||||||
|
unregisterChan chan string
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRunnerHub 创建新的 RunnerHub
|
||||||
|
func NewRunnerHub(taskManager *TaskManager, logger *zap.Logger) *RunnerHub {
|
||||||
|
return &RunnerHub{
|
||||||
|
runners: make(map[string]*RunnerConnection),
|
||||||
|
taskManager: taskManager,
|
||||||
|
registerChan: make(chan *RunnerConnection, 100),
|
||||||
|
unregisterChan: make(chan string, 100),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 实现 RunnerHub 服务的 Connect 方法
|
||||||
|
func (h *RunnerHub) Connect(stream runner.RunnerHub_ConnectServer) error {
|
||||||
|
var conn *RunnerConnection
|
||||||
|
ctx := stream.Context()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if conn != nil {
|
||||||
|
h.unregisterRunner(conn.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 启动发送协程
|
||||||
|
sendDone := make(chan struct{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-sendDone:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
msg, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Debug("Stream receive error", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m := msg.Message.(type) {
|
||||||
|
case *runner.StreamMessage_Register:
|
||||||
|
conn = h.handleRegister(m.Register, stream)
|
||||||
|
if conn != nil {
|
||||||
|
// 启动发送协程
|
||||||
|
go h.sendLoop(conn, sendDone)
|
||||||
|
}
|
||||||
|
case *runner.StreamMessage_Heartbeat:
|
||||||
|
h.handleHeartbeat(conn, m.Heartbeat)
|
||||||
|
case *runner.StreamMessage_Result:
|
||||||
|
h.handleTaskResult(m.Result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendLoop 处理向 Runner 发送消息
|
||||||
|
func (h *RunnerHub) sendLoop(conn *RunnerConnection, done chan struct{}) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg, ok := <-conn.sendChan:
|
||||||
|
if !ok {
|
||||||
|
// Channel 已关闭
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg == nil {
|
||||||
|
// 忽略 nil 消息
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := conn.Stream.Send(msg); err != nil {
|
||||||
|
h.logger.Error("Failed to send message to runner",
|
||||||
|
zap.String("runner_id", conn.ID),
|
||||||
|
zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RunnerHub) handleRegister(req *runner.RegisterRequest, stream runner.RunnerHub_ConnectServer) *RunnerConnection {
|
||||||
|
h.logger.Info("Runner registering",
|
||||||
|
zap.String("runner_id", req.RunnerId),
|
||||||
|
zap.String("version", req.Version),
|
||||||
|
zap.Any("capabilities", req.Capabilities))
|
||||||
|
|
||||||
|
conn := &RunnerConnection{
|
||||||
|
ID: req.RunnerId,
|
||||||
|
Version: req.Version,
|
||||||
|
Capabilities: req.Capabilities,
|
||||||
|
Stream: stream,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
sendChan: make(chan *runner.StreamMessage, 100),
|
||||||
|
logger: h.logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
// 如果已存在旧连接,先关闭
|
||||||
|
if oldConn, ok := h.runners[req.RunnerId]; ok {
|
||||||
|
close(oldConn.sendChan)
|
||||||
|
}
|
||||||
|
h.runners[req.RunnerId] = conn
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
// 发送注册响应
|
||||||
|
if err := conn.send(&runner.StreamMessage{
|
||||||
|
Message: &runner.StreamMessage_RegisterResponse{
|
||||||
|
RegisterResponse: &runner.RegisterResponse{
|
||||||
|
Success: true,
|
||||||
|
SessionId: conn.ID,
|
||||||
|
Message: "Registered successfully",
|
||||||
|
HeartbeatInterval: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
h.logger.Error("Failed to send register response", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("Runner registered successfully", zap.String("runner_id", req.RunnerId))
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RunnerHub) handleHeartbeat(conn *RunnerConnection, hb *runner.Heartbeat) {
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.LastSeen = time.Now()
|
||||||
|
|
||||||
|
if err := conn.send(&runner.StreamMessage{
|
||||||
|
Message: &runner.StreamMessage_HeartbeatAck{
|
||||||
|
HeartbeatAck: &runner.HeartbeatAck{
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
h.logger.Debug("Failed to send heartbeat ack",
|
||||||
|
zap.String("runner_id", conn.ID),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RunnerHub) handleTaskResult(result *runner.TaskResult) {
|
||||||
|
h.logger.Debug("Received task result",
|
||||||
|
zap.String("task_id", result.TaskId),
|
||||||
|
zap.String("status", result.Status.String()))
|
||||||
|
|
||||||
|
if h.taskManager != nil {
|
||||||
|
h.taskManager.HandleResult(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unregisterRunner 注销 Runner
|
||||||
|
func (h *RunnerHub) unregisterRunner(runnerID string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
if conn, ok := h.runners[runnerID]; ok {
|
||||||
|
close(conn.sendChan)
|
||||||
|
delete(h.runners, runnerID)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
h.logger.Info("Runner unregistered", zap.String("runner_id", runnerID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTask 向指定 Runner 发送任务
|
||||||
|
func (h *RunnerHub) SendTask(runnerID string, task *runner.Task) error {
|
||||||
|
h.mu.RLock()
|
||||||
|
conn, ok := h.runners[runnerID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("runner not found: %s", runnerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn.send(&runner.StreamMessage{
|
||||||
|
Message: &runner.StreamMessage_Task{
|
||||||
|
Task: task,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAvailableRunner 获取支持指定任务类型的可用 Runner
|
||||||
|
func (h *RunnerHub) GetAvailableRunner(taskType runner.TaskType) (*RunnerConnection, error) {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
|
taskTypeStr := taskType.String()
|
||||||
|
for _, conn := range h.runners {
|
||||||
|
for capKey, capValue := range conn.Capabilities {
|
||||||
|
// 支持两种格式:
|
||||||
|
// 1. key = "TASK_TYPE_GET_SCHEDULE", value = "true"
|
||||||
|
// 2. key = "schedule", value = "TASK_TYPE_GET_SCHEDULE"
|
||||||
|
if capKey == taskTypeStr && capValue == "true" {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
if capValue == taskTypeStr {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no available runner for task type: %s", taskType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllRunners 获取所有已连接的 Runner 信息
|
||||||
|
func (h *RunnerHub) GetAllRunners() []map[string]interface{} {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]map[string]interface{}, 0, len(h.runners))
|
||||||
|
for id, conn := range h.runners {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"version": conn.Version,
|
||||||
|
"capabilities": conn.Capabilities,
|
||||||
|
"last_seen": conn.LastSeen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTaskManager 设置任务管理器(用于解决循环依赖)
|
||||||
|
func (h *RunnerHub) SetTaskManager(tm *TaskManager) {
|
||||||
|
h.taskManager = tm
|
||||||
|
}
|
||||||
|
|
||||||
|
// send 向 RunnerConnection 发送消息
|
||||||
|
func (c *RunnerConnection) send(msg *runner.StreamMessage) error {
|
||||||
|
select {
|
||||||
|
case c.sendChan <- msg:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("send buffer full for runner: %s", c.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckStaleRunners 检查并清理超时的 Runner 连接
|
||||||
|
func (h *RunnerHub) CheckStaleRunners(timeout time.Duration) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for id, conn := range h.runners {
|
||||||
|
if now.Sub(conn.LastSeen) > timeout {
|
||||||
|
h.logger.Warn("Runner timeout, removing",
|
||||||
|
zap.String("runner_id", id),
|
||||||
|
zap.Time("last_seen", conn.LastSeen))
|
||||||
|
close(conn.sendChan)
|
||||||
|
delete(h.runners, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown 关闭所有 Runner 连接
|
||||||
|
func (h *RunnerHub) Shutdown(ctx context.Context) error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
for id, conn := range h.runners {
|
||||||
|
h.logger.Info("Closing runner connection", zap.String("runner_id", id))
|
||||||
|
close(conn.sendChan)
|
||||||
|
delete(h.runners, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
145
internal/grpc/runner/server.go
Normal file
145
internal/grpc/runner/server.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"carrot_bbs/internal/config"
|
||||||
|
"carrot_bbs/proto/runner"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server gRPC 服务器
|
||||||
|
type Server struct {
|
||||||
|
cfg *config.GRPCConfig
|
||||||
|
logger *zap.Logger
|
||||||
|
hub *RunnerHub
|
||||||
|
grpcSrv *grpc.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer 创建新的 gRPC 服务器
|
||||||
|
func NewServer(cfg *config.GRPCConfig, logger *zap.Logger) *Server {
|
||||||
|
// 创建 TaskManager 和 RunnerHub,解决循环依赖
|
||||||
|
taskManager := NewTaskManager(nil, 60*time.Second, logger)
|
||||||
|
hub := NewRunnerHub(taskManager, logger)
|
||||||
|
taskManager.SetHub(hub)
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
cfg: cfg,
|
||||||
|
logger: logger,
|
||||||
|
hub: hub,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动 gRPC 服务器
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
if !s.cfg.Enabled {
|
||||||
|
s.logger.Info("gRPC server is disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", s.cfg.Address())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to listen on %s: %w", s.cfg.Address(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置 gRPC 服务器选项
|
||||||
|
var opts []grpc.ServerOption
|
||||||
|
|
||||||
|
// 配置 TLS
|
||||||
|
if s.cfg.TLSEnabled {
|
||||||
|
if s.cfg.TLSCertFile == "" || s.cfg.TLSKeyFile == "" {
|
||||||
|
return fmt.Errorf("TLS enabled but cert/key files not specified")
|
||||||
|
}
|
||||||
|
creds, err := credentials.NewServerTLSFromFile(s.cfg.TLSCertFile, s.cfg.TLSKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load TLS credentials: %w", err)
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.Creds(creds))
|
||||||
|
s.logger.Info("gRPC server TLS enabled")
|
||||||
|
} else {
|
||||||
|
s.logger.Warn("gRPC server running without TLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.grpcSrv = grpc.NewServer(opts...)
|
||||||
|
runner.RegisterRunnerHubServer(s.grpcSrv, s.hub)
|
||||||
|
|
||||||
|
s.logger.Info("gRPC server starting", zap.String("address", s.cfg.Address()))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.grpcSrv.Serve(lis); err != nil {
|
||||||
|
s.logger.Error("gRPC server error", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 启动后台任务清理协程
|
||||||
|
go s.backgroundCleanup()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// backgroundCleanup 后台清理任务
|
||||||
|
func (s *Server) backgroundCleanup() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
// 清理超时的 Runner 连接
|
||||||
|
s.hub.CheckStaleRunners(90 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止 gRPC 服务器
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
if s.grpcSrv != nil {
|
||||||
|
s.logger.Info("Stopping gRPC server")
|
||||||
|
s.grpcSrv.GracefulStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHub 获取 RunnerHub
|
||||||
|
func (s *Server) GetHub() *RunnerHub {
|
||||||
|
return s.hub
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskManager 获取 TaskManager
|
||||||
|
func (s *Server) GetTaskManager() *TaskManager {
|
||||||
|
return s.hub.taskManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown 优雅关闭
|
||||||
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
if s.grpcSrv == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
s.grpcSrv.GracefulStop()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
s.logger.Info("gRPC server stopped gracefully")
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
s.logger.Warn("gRPC server shutdown timeout, forcing stop")
|
||||||
|
s.grpcSrv.Stop()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientCredentials 获取客户端凭证配置
|
||||||
|
func GetClientCredentials(cfg *config.GRPCConfig) (credentials.TransportCredentials, error) {
|
||||||
|
if cfg.TLSEnabled {
|
||||||
|
return credentials.NewClientTLSFromFile(cfg.TLSCertFile, "")
|
||||||
|
}
|
||||||
|
return insecure.NewCredentials(), nil
|
||||||
|
}
|
||||||
235
internal/grpc/runner/task_manager.go
Normal file
235
internal/grpc/runner/task_manager.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"carrot_bbs/proto/runner"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskCallback 任务完成回调函数类型
|
||||||
|
type TaskCallback func(result *runner.TaskResult)
|
||||||
|
|
||||||
|
// PendingTask 表示一个等待处理的任务
|
||||||
|
type PendingTask struct {
|
||||||
|
Task *runner.Task
|
||||||
|
Callback TaskCallback
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskManager 管理任务的生命周期
|
||||||
|
type TaskManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
pending map[string]*PendingTask
|
||||||
|
hub *RunnerHub
|
||||||
|
timeout time.Duration
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskManager 创建新的任务管理器
|
||||||
|
func NewTaskManager(hub *RunnerHub, timeout time.Duration, logger *zap.Logger) *TaskManager {
|
||||||
|
return &TaskManager{
|
||||||
|
pending: make(map[string]*PendingTask),
|
||||||
|
hub: hub,
|
||||||
|
timeout: timeout,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitTask 提交任务并等待结果
|
||||||
|
func (tm *TaskManager) SubmitTask(ctx context.Context, taskType runner.TaskType, payload interface{}, callback TaskCallback) (*runner.Task, error) {
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
task := &runner.Task{
|
||||||
|
TaskId: uuid.New().String(),
|
||||||
|
Type: taskType,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
TimeoutSeconds: int32(tm.timeout.Seconds()),
|
||||||
|
Payload: payloadBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.mu.Lock()
|
||||||
|
tm.pending[task.TaskId] = &PendingTask{
|
||||||
|
Task: task,
|
||||||
|
Callback: callback,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
tm.mu.Unlock()
|
||||||
|
|
||||||
|
tm.logger.Debug("Task submitted",
|
||||||
|
zap.String("task_id", task.TaskId),
|
||||||
|
zap.String("type", taskType.String()))
|
||||||
|
|
||||||
|
// 获取可用 Runner
|
||||||
|
conn, err := tm.hub.GetAvailableRunner(taskType)
|
||||||
|
if err != nil {
|
||||||
|
tm.cleanupTask(task.TaskId)
|
||||||
|
return nil, fmt.Errorf("no available runner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送任务
|
||||||
|
if err := tm.hub.SendTask(conn.ID, task); err != nil {
|
||||||
|
tm.cleanupTask(task.TaskId)
|
||||||
|
return nil, fmt.Errorf("failed to send task: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动超时检查
|
||||||
|
go tm.waitForTimeout(task.TaskId)
|
||||||
|
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitTaskSync 同步提交任务并等待结果
|
||||||
|
func (tm *TaskManager) SubmitTaskSync(ctx context.Context, taskType runner.TaskType, payload interface{}) (*runner.TaskResult, error) {
|
||||||
|
resultChan := make(chan *runner.TaskResult, 1)
|
||||||
|
|
||||||
|
task, err := tm.SubmitTask(ctx, taskType, payload, func(result *runner.TaskResult) {
|
||||||
|
select {
|
||||||
|
case resultChan <- result:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
return result, nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
tm.cleanupTask(task.TaskId)
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-time.After(tm.timeout):
|
||||||
|
tm.cleanupTask(task.TaskId)
|
||||||
|
return nil, fmt.Errorf("task timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResult 处理任务结果
|
||||||
|
func (tm *TaskManager) HandleResult(result *runner.TaskResult) {
|
||||||
|
tm.mu.Lock()
|
||||||
|
pending, ok := tm.pending[result.TaskId]
|
||||||
|
if ok {
|
||||||
|
delete(tm.pending, result.TaskId)
|
||||||
|
}
|
||||||
|
tm.mu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
tm.logger.Warn("Received result for unknown task",
|
||||||
|
zap.String("task_id", result.TaskId))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.logger.Debug("Task completed",
|
||||||
|
zap.String("task_id", result.TaskId),
|
||||||
|
zap.String("status", result.Status.String()))
|
||||||
|
|
||||||
|
if pending.Callback != nil {
|
||||||
|
pending.Callback(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForTimeout 等待任务超时
|
||||||
|
func (tm *TaskManager) waitForTimeout(taskID string) {
|
||||||
|
time.Sleep(tm.timeout)
|
||||||
|
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
if pending, ok := tm.pending[taskID]; ok {
|
||||||
|
tm.logger.Warn("Task timeout",
|
||||||
|
zap.String("task_id", taskID),
|
||||||
|
zap.String("type", pending.Task.Type.String()))
|
||||||
|
|
||||||
|
delete(tm.pending, taskID)
|
||||||
|
|
||||||
|
// 调用回调,返回超时结果
|
||||||
|
if pending.Callback != nil {
|
||||||
|
pending.Callback(&runner.TaskResult{
|
||||||
|
TaskId: taskID,
|
||||||
|
Status: runner.TaskStatus_TASK_STATUS_TIMEOUT,
|
||||||
|
ErrorMessage: "task timeout",
|
||||||
|
CompletedAt: time.Now().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupTask 清理任务
|
||||||
|
func (tm *TaskManager) cleanupTask(taskID string) {
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
delete(tm.pending, taskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelTask 取消任务
|
||||||
|
func (tm *TaskManager) CancelTask(taskID string) error {
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
if pending, ok := tm.pending[taskID]; ok {
|
||||||
|
tm.logger.Debug("Task cancelled", zap.String("task_id", taskID))
|
||||||
|
|
||||||
|
delete(tm.pending, taskID)
|
||||||
|
|
||||||
|
// 调用回调,返回取消结果
|
||||||
|
if pending.Callback != nil {
|
||||||
|
pending.Callback(&runner.TaskResult{
|
||||||
|
TaskId: taskID,
|
||||||
|
Status: runner.TaskStatus_TASK_STATUS_CANCELLED,
|
||||||
|
ErrorMessage: "task cancelled",
|
||||||
|
CompletedAt: time.Now().Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("task not found: %s", taskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingTaskCount 获取等待处理的任务数量
|
||||||
|
func (tm *TaskManager) GetPendingTaskCount() int {
|
||||||
|
tm.mu.RLock()
|
||||||
|
defer tm.mu.RUnlock()
|
||||||
|
return len(tm.pending)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHub 设置 RunnerHub(用于解决循环依赖)
|
||||||
|
func (tm *TaskManager) SetHub(hub *RunnerHub) {
|
||||||
|
tm.hub = hub
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupStaleTasks 清理超时的任务
|
||||||
|
func (tm *TaskManager) CleanupStaleTasks() {
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for taskID, pending := range tm.pending {
|
||||||
|
if now.Sub(pending.CreatedAt) > tm.timeout {
|
||||||
|
tm.logger.Warn("Cleaning up stale task",
|
||||||
|
zap.String("task_id", taskID),
|
||||||
|
zap.String("type", pending.Task.Type.String()))
|
||||||
|
|
||||||
|
delete(tm.pending, taskID)
|
||||||
|
|
||||||
|
if pending.Callback != nil {
|
||||||
|
pending.Callback(&runner.TaskResult{
|
||||||
|
TaskId: taskID,
|
||||||
|
Status: runner.TaskStatus_TASK_STATUS_TIMEOUT,
|
||||||
|
ErrorMessage: "task timeout",
|
||||||
|
CompletedAt: now.Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,14 @@ import (
|
|||||||
|
|
||||||
type ScheduleHandler struct {
|
type ScheduleHandler struct {
|
||||||
scheduleService service.ScheduleService
|
scheduleService service.ScheduleService
|
||||||
|
scheduleSyncService service.ScheduleSyncService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScheduleHandler(scheduleService service.ScheduleService) *ScheduleHandler {
|
func NewScheduleHandler(scheduleService service.ScheduleService, scheduleSyncService service.ScheduleSyncService) *ScheduleHandler {
|
||||||
return &ScheduleHandler{scheduleService: scheduleService}
|
return &ScheduleHandler{
|
||||||
|
scheduleService: scheduleService,
|
||||||
|
scheduleSyncService: scheduleSyncService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type createScheduleCourseRequest struct {
|
type createScheduleCourseRequest struct {
|
||||||
@@ -138,3 +142,42 @@ func (h *ScheduleHandler) DeleteCourse(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.SuccessWithMessage(c, "course deleted", nil)
|
response.SuccessWithMessage(c, "course deleted", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// syncScheduleRequest 同步课表请求
|
||||||
|
type syncScheduleRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Semester string `json:"semester"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncSchedule 同步课表接口
|
||||||
|
func (h *ScheduleHandler) SyncSchedule(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
if userID == "" {
|
||||||
|
response.Unauthorized(c, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req syncScheduleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.scheduleSyncService.SyncUserSchedule(c.Request.Context(), userID, &service.SyncScheduleRequest{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
Semester: req.Semester,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err, "failed to sync schedule")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Success {
|
||||||
|
response.Success(c, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.SuccessWithMessage(c, result.Message, result)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -12,6 +14,7 @@ type ScheduleRepository interface {
|
|||||||
Create(course *model.ScheduleCourse) error
|
Create(course *model.ScheduleCourse) error
|
||||||
Update(course *model.ScheduleCourse) error
|
Update(course *model.ScheduleCourse) error
|
||||||
DeleteByID(id string) error
|
DeleteByID(id string) error
|
||||||
|
DeleteByUserID(ctx context.Context, userID string) error
|
||||||
ExistsColorByUser(userID, color, excludeID string) (bool, error)
|
ExistsColorByUser(userID, color, excludeID string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +55,10 @@ func (r *scheduleRepository) DeleteByID(id string) error {
|
|||||||
return r.db.Delete(&model.ScheduleCourse{}, "id = ?", id).Error
|
return r.db.Delete(&model.ScheduleCourse{}, "id = ?", id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *scheduleRepository) DeleteByUserID(ctx context.Context, userID string) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&model.ScheduleCourse{}, "user_id = ?", userID).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *scheduleRepository) ExistsColorByUser(userID, color, excludeID string) (bool, error) {
|
func (r *scheduleRepository) ExistsColorByUser(userID, color, excludeID string) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
query := r.db.Model(&model.ScheduleCourse{}).
|
query := r.db.Model(&model.ScheduleCourse{}).
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ func (r *Router) setupRoutes() {
|
|||||||
{
|
{
|
||||||
schedule.GET("/courses", r.scheduleHandler.ListCourses)
|
schedule.GET("/courses", r.scheduleHandler.ListCourses)
|
||||||
schedule.POST("/courses", r.scheduleHandler.CreateCourse)
|
schedule.POST("/courses", r.scheduleHandler.CreateCourse)
|
||||||
|
schedule.POST("/sync", r.scheduleHandler.SyncSchedule) // 同步课表
|
||||||
schedule.PUT("/courses/:id", r.scheduleHandler.UpdateCourse)
|
schedule.PUT("/courses/:id", r.scheduleHandler.UpdateCourse)
|
||||||
schedule.DELETE("/courses/:id", r.scheduleHandler.DeleteCourse)
|
schedule.DELETE("/courses/:id", r.scheduleHandler.DeleteCourse)
|
||||||
}
|
}
|
||||||
|
|||||||
192
internal/service/schedule_sync_service.go
Normal file
192
internal/service/schedule_sync_service.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"carrot_bbs/internal/grpc/runner"
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
pb "carrot_bbs/proto/runner"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncScheduleRequest 同步课表请求
|
||||||
|
type SyncScheduleRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Semester string `json:"semester"` // 可选,默认当前学期
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncScheduleResult 同步课表结果
|
||||||
|
type SyncScheduleResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
SyncedCount int `json:"synced_count"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleSyncService 课表同步服务接口
|
||||||
|
type ScheduleSyncService interface {
|
||||||
|
SyncUserSchedule(ctx context.Context, userID string, req *SyncScheduleRequest) (*SyncScheduleResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleSyncService struct {
|
||||||
|
taskManager *runner.TaskManager
|
||||||
|
scheduleRepo repository.ScheduleRepository
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScheduleSyncService 创建课表同步服务
|
||||||
|
func NewScheduleSyncService(
|
||||||
|
taskManager *runner.TaskManager,
|
||||||
|
scheduleRepo repository.ScheduleRepository,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) ScheduleSyncService {
|
||||||
|
return &scheduleSyncService{
|
||||||
|
taskManager: taskManager,
|
||||||
|
scheduleRepo: scheduleRepo,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncUserSchedule 同步用户课表
|
||||||
|
func (s *scheduleSyncService) SyncUserSchedule(ctx context.Context, userID string, req *SyncScheduleRequest) (*SyncScheduleResult, error) {
|
||||||
|
// 构建任务载荷
|
||||||
|
payload := &pb.GetSchedulePayload{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
Semester: req.Semester,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步提交任务并等待结果
|
||||||
|
taskResult, err := s.taskManager.SubmitTaskSync(ctx, pb.TaskType_TASK_TYPE_GET_SCHEDULE, payload)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to submit sync task",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to submit task: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskResult.Status != pb.TaskStatus_TASK_STATUS_SUCCESS {
|
||||||
|
return &SyncScheduleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: "课表获取失败",
|
||||||
|
ErrorMessage: taskResult.ErrorMessage,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析结果数据
|
||||||
|
var resultData pb.ScheduleResultData
|
||||||
|
if err := json.Unmarshal(taskResult.Data, &resultData); err != nil {
|
||||||
|
s.logger.Error("failed to parse schedule result",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to parse result: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换并保存课表
|
||||||
|
courses := s.convertCourses(&resultData)
|
||||||
|
if err := s.saveCourses(ctx, userID, courses); err != nil {
|
||||||
|
s.logger.Error("failed to save courses",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to save courses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("schedule sync completed",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.Int("course_count", len(courses)))
|
||||||
|
|
||||||
|
return &SyncScheduleResult{
|
||||||
|
Success: true,
|
||||||
|
Message: "课表同步成功",
|
||||||
|
SyncedCount: len(courses),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertCourses 转换 converter 课表数据为 backend 模型
|
||||||
|
func (s *scheduleSyncService) convertCourses(data *pb.ScheduleResultData) []*model.ScheduleCourse {
|
||||||
|
var courses []*model.ScheduleCourse
|
||||||
|
|
||||||
|
for _, c := range data.Courses {
|
||||||
|
for _, st := range c.Schedule {
|
||||||
|
dayOfWeek := 0
|
||||||
|
fmt.Sscanf(st.Day, "%d", &dayOfWeek)
|
||||||
|
|
||||||
|
startSection := 1
|
||||||
|
endSection := 1
|
||||||
|
if len(st.Sections) > 0 {
|
||||||
|
startSection = int(st.Sections[0])
|
||||||
|
endSection = int(st.Sections[len(st.Sections)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换周次数组
|
||||||
|
var weeks []int
|
||||||
|
for _, w := range st.Weeks {
|
||||||
|
weeks = append(weeks, int(w))
|
||||||
|
}
|
||||||
|
weeksJSON, _ := json.Marshal(weeks)
|
||||||
|
|
||||||
|
course := &model.ScheduleCourse{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Name: c.Name,
|
||||||
|
Teacher: c.Teacher,
|
||||||
|
Location: c.Room,
|
||||||
|
DayOfWeek: dayOfWeek,
|
||||||
|
StartSection: startSection,
|
||||||
|
EndSection: endSection,
|
||||||
|
Weeks: string(weeksJSON),
|
||||||
|
Color: generateCourseColor(c.Name),
|
||||||
|
}
|
||||||
|
courses = append(courses, course)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return courses
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveCourses 保存课表到数据库
|
||||||
|
func (s *scheduleSyncService) saveCourses(ctx context.Context, userID string, courses []*model.ScheduleCourse) error {
|
||||||
|
// 先删除用户旧课表
|
||||||
|
if err := s.scheduleRepo.DeleteByUserID(ctx, userID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old courses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户 ID 并保存
|
||||||
|
for _, c := range courses {
|
||||||
|
c.UserID = userID
|
||||||
|
if err := s.scheduleRepo.Create(c); err != nil {
|
||||||
|
return fmt.Errorf("failed to create course: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCourseColor 根据课程名生成颜色
|
||||||
|
func generateCourseColor(name string) string {
|
||||||
|
// 预定义的课程颜色列表
|
||||||
|
colors := []string{
|
||||||
|
"#3498DB", // 蓝色
|
||||||
|
"#2ECC71", // 绿色
|
||||||
|
"#E74C3C", // 红色
|
||||||
|
"#F39C12", // 橙色
|
||||||
|
"#9B59B6", // 紫色
|
||||||
|
"#1ABC9C", // 青色
|
||||||
|
"#E67E22", // 深橙色
|
||||||
|
"#34495E", // 深灰色
|
||||||
|
"#16A085", // 深青色
|
||||||
|
"#27AE60", // 深绿色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据课程名哈希选择颜色
|
||||||
|
hash := 0
|
||||||
|
for _, c := range name {
|
||||||
|
hash = (hash + int(c)) % len(colors)
|
||||||
|
}
|
||||||
|
return colors[hash]
|
||||||
|
}
|
||||||
25
internal/wire/grpc.go
Normal file
25
internal/wire/grpc.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/config"
|
||||||
|
"carrot_bbs/internal/grpc/runner"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GRPCSet gRPC 相关的 Provider Set
|
||||||
|
var GRPCSet = wire.NewSet(
|
||||||
|
ProvideGRPCServer,
|
||||||
|
ProvideTaskManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProvideGRPCServer 提供 gRPC 服务器
|
||||||
|
func ProvideGRPCServer(cfg *config.Config, logger *zap.Logger) *runner.Server {
|
||||||
|
return runner.NewServer(&cfg.GRPC, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideTaskManager 提供 TaskManager
|
||||||
|
func ProvideTaskManager(server *runner.Server) *runner.TaskManager {
|
||||||
|
return server.GetTaskManager()
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ var HandlerSet = wire.NewSet(
|
|||||||
handler.NewPushHandler,
|
handler.NewPushHandler,
|
||||||
handler.NewStickerHandler,
|
handler.NewStickerHandler,
|
||||||
handler.NewVoteHandler,
|
handler.NewVoteHandler,
|
||||||
handler.NewScheduleHandler,
|
|
||||||
|
|
||||||
// 需要特殊处理的 Handler
|
// 需要特殊处理的 Handler
|
||||||
ProvidePostHandler,
|
ProvidePostHandler,
|
||||||
@@ -30,6 +29,7 @@ var HandlerSet = wire.NewSet(
|
|||||||
ProvideSystemMessageHandler,
|
ProvideSystemMessageHandler,
|
||||||
ProvideGroupHandler,
|
ProvideGroupHandler,
|
||||||
ProvideGorseHandler,
|
ProvideGorseHandler,
|
||||||
|
ProvideScheduleHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProvidePostHandler 提供帖子处理器
|
// ProvidePostHandler 提供帖子处理器
|
||||||
@@ -72,3 +72,11 @@ func ProvideGroupHandler(
|
|||||||
func ProvideGorseHandler(cfg *config.Config, db *gorm.DB) *handler.GorseHandler {
|
func ProvideGorseHandler(cfg *config.Config, db *gorm.DB) *handler.GorseHandler {
|
||||||
return handler.NewGorseHandler(cfg.Gorse, db)
|
return handler.NewGorseHandler(cfg.Gorse, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideScheduleHandler 提供课表处理器
|
||||||
|
func ProvideScheduleHandler(
|
||||||
|
scheduleService service.ScheduleService,
|
||||||
|
scheduleSyncService service.ScheduleSyncService,
|
||||||
|
) *handler.ScheduleHandler {
|
||||||
|
return handler.NewScheduleHandler(scheduleService, scheduleSyncService)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"carrot_bbs/internal/repository"
|
"carrot_bbs/internal/repository"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ var InfrastructureSet = wire.NewSet(
|
|||||||
// 配置
|
// 配置
|
||||||
ProvideConfig,
|
ProvideConfig,
|
||||||
|
|
||||||
|
// 日志
|
||||||
|
ProvideLogger,
|
||||||
|
|
||||||
// 数据库
|
// 数据库
|
||||||
ProvideDB,
|
ProvideDB,
|
||||||
|
|
||||||
@@ -119,3 +123,12 @@ func ProvideS3Client(cfg *config.Config) (*s3.Client, error) {
|
|||||||
func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager {
|
func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager {
|
||||||
return repository.NewTransactionManager(db)
|
return repository.NewTransactionManager(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideLogger 提供 Zap Logger
|
||||||
|
func ProvideLogger() *zap.Logger {
|
||||||
|
logger, err := zap.NewProduction()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create logger: %v", err)
|
||||||
|
}
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ var AllSet = wire.NewSet(
|
|||||||
RepositorySet,
|
RepositorySet,
|
||||||
ServiceSet,
|
ServiceSet,
|
||||||
HandlerSet,
|
HandlerSet,
|
||||||
|
GRPCSet,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package wire
|
|||||||
import (
|
import (
|
||||||
"carrot_bbs/internal/cache"
|
"carrot_bbs/internal/cache"
|
||||||
"carrot_bbs/internal/config"
|
"carrot_bbs/internal/config"
|
||||||
|
"carrot_bbs/internal/grpc/runner"
|
||||||
"carrot_bbs/internal/pkg/email"
|
"carrot_bbs/internal/pkg/email"
|
||||||
"carrot_bbs/internal/pkg/gorse"
|
"carrot_bbs/internal/pkg/gorse"
|
||||||
"carrot_bbs/internal/pkg/openai"
|
"carrot_bbs/internal/pkg/openai"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"carrot_bbs/internal/service"
|
"carrot_bbs/internal/service"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ var ServiceSet = wire.NewSet(
|
|||||||
ProvideVoteService,
|
ProvideVoteService,
|
||||||
ProvideChatService,
|
ProvideChatService,
|
||||||
ProvideScheduleService,
|
ProvideScheduleService,
|
||||||
|
ProvideScheduleSyncService,
|
||||||
ProvideGroupService,
|
ProvideGroupService,
|
||||||
ProvideUploadService,
|
ProvideUploadService,
|
||||||
)
|
)
|
||||||
@@ -168,6 +171,15 @@ func ProvideScheduleService(
|
|||||||
return service.NewScheduleService(scheduleRepo)
|
return service.NewScheduleService(scheduleRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideScheduleSyncService 提供课表同步服务
|
||||||
|
func ProvideScheduleSyncService(
|
||||||
|
taskManager *runner.TaskManager,
|
||||||
|
scheduleRepo repository.ScheduleRepository,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) service.ScheduleSyncService {
|
||||||
|
return service.NewScheduleSyncService(taskManager, scheduleRepo, logger)
|
||||||
|
}
|
||||||
|
|
||||||
// ProvideGroupService 提供群组服务
|
// ProvideGroupService 提供群组服务
|
||||||
func ProvideGroupService(
|
func ProvideGroupService(
|
||||||
db *gorm.DB,
|
db *gorm.DB,
|
||||||
|
|||||||
1835
proto/runner/runner.pb.go
Normal file
1835
proto/runner/runner.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
224
proto/runner/runner.proto
Normal file
224
proto/runner/runner.proto
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package runner;
|
||||||
|
|
||||||
|
option go_package = "carrot_bbs/proto/runner;runner";
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 服务定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// RunnerHub 服务 - 由 Backend 服务器实现
|
||||||
|
// Runner 作为客户端主动连接,建立双向流通信
|
||||||
|
service RunnerHub {
|
||||||
|
// Connect 建立双向流连接
|
||||||
|
// Runner 通过此连接注册、接收任务、返回结果
|
||||||
|
rpc Connect(stream StreamMessage) returns (stream StreamMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 流消息包装 - 所有消息通过 oneof 包装
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// StreamMessage 双向流消息包装
|
||||||
|
message StreamMessage {
|
||||||
|
oneof message {
|
||||||
|
RegisterRequest register = 1; // Runner 注册请求
|
||||||
|
RegisterResponse register_response = 2; // 注册响应
|
||||||
|
Heartbeat heartbeat = 3; // 心跳请求
|
||||||
|
HeartbeatAck heartbeat_ack = 4; // 心跳响应
|
||||||
|
Task task = 5; // 下发的任务
|
||||||
|
TaskResult result = 6; // 任务结果
|
||||||
|
TaskCancel task_cancel = 7; // 取消任务
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 注册相关消息
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// RegisterRequest Runner 注册请求
|
||||||
|
message RegisterRequest {
|
||||||
|
string runner_id = 1; // Runner 唯一标识 (UUID)
|
||||||
|
string version = 2; // Runner 版本号
|
||||||
|
map<string, string> capabilities = 3; // 能力标签
|
||||||
|
// 例如: {"login": "true", "schedule": "true", "grades": "true"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterResponse 注册响应
|
||||||
|
message RegisterResponse {
|
||||||
|
bool success = 1; // 是否成功
|
||||||
|
string session_id = 2; // 会话ID,用于标识本次连接
|
||||||
|
string message = 3; // 消息说明
|
||||||
|
int32 heartbeat_interval = 4; // 心跳间隔 (秒)
|
||||||
|
int32 task_timeout = 5; // 默认任务超时时间 (秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 心跳消息
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Heartbeat 心跳请求
|
||||||
|
message Heartbeat {
|
||||||
|
int64 timestamp = 1; // 客户端时间戳 (毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeartbeatAck 心跳响应
|
||||||
|
message HeartbeatAck {
|
||||||
|
int64 timestamp = 1; // 服务端时间戳 (毫秒)
|
||||||
|
int64 latency = 2; // 往返延迟 (毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 任务定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// TaskType 任务类型枚举
|
||||||
|
enum TaskType {
|
||||||
|
TASK_TYPE_UNKNOWN = 0; // 未知类型
|
||||||
|
TASK_TYPE_LOGIN = 1; // 登录验证
|
||||||
|
TASK_TYPE_GET_SCHEDULE = 2; // 获取课表
|
||||||
|
TASK_TYPE_GET_GRADES = 3; // 获取成绩
|
||||||
|
TASK_TYPE_GET_EXAMS = 4; // 获取考试安排
|
||||||
|
TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskStatus 任务状态枚举
|
||||||
|
enum TaskStatus {
|
||||||
|
TASK_STATUS_UNKNOWN = 0; // 未知状态
|
||||||
|
TASK_STATUS_SUCCESS = 1; // 成功
|
||||||
|
TASK_STATUS_FAILED = 2; // 失败
|
||||||
|
TASK_STATUS_TIMEOUT = 3; // 超时
|
||||||
|
TASK_STATUS_CANCELLED = 4; // 已取消
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 任务定义
|
||||||
|
message Task {
|
||||||
|
string task_id = 1; // 任务唯一ID (UUID)
|
||||||
|
TaskType type = 2; // 任务类型
|
||||||
|
int64 created_at = 3; // 创建时间戳 (毫秒)
|
||||||
|
int32 timeout_seconds = 4; // 超时时间 (秒)
|
||||||
|
bytes payload = 5; // 任务载荷 (JSON 编码)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCancel 取消任务
|
||||||
|
message TaskCancel {
|
||||||
|
string task_id = 1; // 要取消的任务ID
|
||||||
|
string reason = 2; // 取消原因
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskResult 任务结果
|
||||||
|
message TaskResult {
|
||||||
|
string task_id = 1; // 任务ID
|
||||||
|
TaskStatus status = 2; // 执行状态
|
||||||
|
int64 completed_at = 3; // 完成时间戳 (毫秒)
|
||||||
|
bytes data = 4; // 结果数据 (JSON 编码)
|
||||||
|
string error_code = 5; // 错误码
|
||||||
|
string error_message = 6; // 错误消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 任务载荷定义 - 用于 JSON 序列化到 bytes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// LoginPayload 登录任务载荷
|
||||||
|
message LoginPayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string twfid = 3; // 初始 Cookie (可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchedulePayload 获取课表任务载荷
|
||||||
|
message GetSchedulePayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string semester = 3; // 学期,如 "2025-20262"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGradesPayload 获取成绩任务载荷
|
||||||
|
message GetGradesPayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string semester = 3; // 学期 (可选,为空则获取全部)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExamsPayload 获取考试安排任务载荷
|
||||||
|
message GetExamsPayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string semester = 3; // 学期
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 结果数据定义 - 用于 JSON 序列化到 bytes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ScheduleResultData 课表结果数据
|
||||||
|
message ScheduleResultData {
|
||||||
|
string semester = 1; // 学期
|
||||||
|
string student_id = 2; // 学号
|
||||||
|
string student_name = 3; // 学生姓名
|
||||||
|
repeated Course courses = 4; // 课程列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course 课程信息
|
||||||
|
message Course {
|
||||||
|
string code = 1; // 课程代码
|
||||||
|
string name = 2; // 课程名称
|
||||||
|
string teacher = 3; // 授课教师
|
||||||
|
string room = 4; // 教室
|
||||||
|
float credit = 5; // 学分
|
||||||
|
repeated ScheduleTime schedule = 6; // 上课时间安排
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleTime 上课时间安排
|
||||||
|
message ScheduleTime {
|
||||||
|
string day = 1; // 星期几,"0"-"6" 表示周日到周六
|
||||||
|
repeated int32 sections = 2; // 节次,如 [1, 2, 3]
|
||||||
|
repeated int32 weeks = 3; // 周次,如 [1, 2, 3, 4, 5]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GradesResultData 成绩结果数据
|
||||||
|
message GradesResultData {
|
||||||
|
string semester = 1; // 学期
|
||||||
|
string student_id = 2; // 学号
|
||||||
|
repeated Grade grades = 3; // 成绩列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grade 成绩信息
|
||||||
|
message Grade {
|
||||||
|
string course_code = 1; // 课程代码
|
||||||
|
string course_name = 2; // 课程名称
|
||||||
|
float credit = 3; // 学分
|
||||||
|
string grade = 4; // 成绩 (可能是等级或分数)
|
||||||
|
float grade_point = 5; // 绩点
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamsResultData 考试安排结果数据
|
||||||
|
message ExamsResultData {
|
||||||
|
string semester = 1; // 学期
|
||||||
|
string student_id = 2; // 学号
|
||||||
|
repeated Exam exams = 3; // 考试列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exam 考试信息
|
||||||
|
message Exam {
|
||||||
|
string course_code = 1; // 课程代码
|
||||||
|
string course_name = 2; // 课程名称
|
||||||
|
string exam_type = 3; // 考试类型 (期末/期中/补考)
|
||||||
|
string date = 4; // 考试日期 (YYYY-MM-DD)
|
||||||
|
string time = 5; // 考试时间 (HH:MM-HH:MM)
|
||||||
|
string room = 6; // 考场
|
||||||
|
string seat = 7; // 座位号
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfoResultData 用户信息结果数据
|
||||||
|
message UserInfoResultData {
|
||||||
|
string student_id = 1; // 学号
|
||||||
|
string name = 2; // 姓名
|
||||||
|
string gender = 3; // 性别
|
||||||
|
string college = 4; // 学院
|
||||||
|
string major = 5; // 专业
|
||||||
|
string class_name = 6; // 班级
|
||||||
|
string grade = 7; // 年级
|
||||||
|
}
|
||||||
145
proto/runner/runner_grpc.pb.go
Normal file
145
proto/runner/runner_grpc.pb.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.3.0
|
||||||
|
// - protoc v6.33.1
|
||||||
|
// source: proto/runner/runner.proto
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
const (
|
||||||
|
RunnerHub_Connect_FullMethodName = "/runner.RunnerHub/Connect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunnerHubClient is the client API for RunnerHub service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type RunnerHubClient interface {
|
||||||
|
// Connect 建立双向流连接
|
||||||
|
// Runner 通过此连接注册、接收任务、返回结果
|
||||||
|
Connect(ctx context.Context, opts ...grpc.CallOption) (RunnerHub_ConnectClient, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type runnerHubClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunnerHubClient(cc grpc.ClientConnInterface) RunnerHubClient {
|
||||||
|
return &runnerHubClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *runnerHubClient) Connect(ctx context.Context, opts ...grpc.CallOption) (RunnerHub_ConnectClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &runnerHubConnectClient{stream}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerHub_ConnectClient interface {
|
||||||
|
Send(*StreamMessage) error
|
||||||
|
Recv() (*StreamMessage, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type runnerHubConnectClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *runnerHubConnectClient) Send(m *StreamMessage) error {
|
||||||
|
return x.ClientStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *runnerHubConnectClient) Recv() (*StreamMessage, error) {
|
||||||
|
m := new(StreamMessage)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerHubServer is the server API for RunnerHub service.
|
||||||
|
// All implementations must embed UnimplementedRunnerHubServer
|
||||||
|
// for forward compatibility
|
||||||
|
type RunnerHubServer interface {
|
||||||
|
// Connect 建立双向流连接
|
||||||
|
// Runner 通过此连接注册、接收任务、返回结果
|
||||||
|
Connect(RunnerHub_ConnectServer) error
|
||||||
|
mustEmbedUnimplementedRunnerHubServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedRunnerHubServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedRunnerHubServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedRunnerHubServer) Connect(RunnerHub_ConnectServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method Connect not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {}
|
||||||
|
|
||||||
|
// UnsafeRunnerHubServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to RunnerHubServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeRunnerHubServer interface {
|
||||||
|
mustEmbedUnimplementedRunnerHubServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterRunnerHubServer(s grpc.ServiceRegistrar, srv RunnerHubServer) {
|
||||||
|
s.RegisterService(&RunnerHub_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(RunnerHubServer).Connect(&runnerHubConnectServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerHub_ConnectServer interface {
|
||||||
|
Send(*StreamMessage) error
|
||||||
|
Recv() (*StreamMessage, error)
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type runnerHubConnectServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *runnerHubConnectServer) Send(m *StreamMessage) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *runnerHubConnectServer) Recv() (*StreamMessage, error) {
|
||||||
|
m := new(StreamMessage)
|
||||||
|
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerHub_ServiceDesc is the grpc.ServiceDesc for RunnerHub service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var RunnerHub_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "runner.RunnerHub",
|
||||||
|
HandlerType: (*RunnerHubServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "Connect",
|
||||||
|
Handler: _RunnerHub_Connect_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
ClientStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "proto/runner/runner.proto",
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user