Files
backend/internal/service/schedule_sync_service.go
lan c561a0bb0c 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
2026-03-13 20:40:20 +08:00

193 lines
5.1 KiB
Go

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]
}