Files
backend/internal/service/schedule_sync_service.go
lafay b2b55ea52d
All checks were successful
Build Backend / build (push) Successful in 1m56s
Build Backend / build-docker (push) Successful in 1m15s
feat: enhance security with IP banning, ownership checks, and SSRF protection
Add comprehensive security improvements across the application:

- **IP-based login protection**: Implement IP ban system tracking login failures, auto-banning after threshold exceeded
- **Ownership verification**: Add userID parameter to Delete/Update operations for posts and comments to prevent unauthorized modifications
- **SSRF protection**: Add URL and resolved host validation for image URLs in chat and OpenAI client
- **SQL injection prevention**: Add EscapeLikeWildcard utility to escape special characters in LIKE queries
- **HTTP security**: Configure server timeouts and add security headers middleware
- **Rate limiting**: Refactor to support configurable duration and per-endpoint rate limits for auth routes
- **Error handling**: Standardize error responses using HandleError and proper error types
2026-04-30 12:26:25 +08:00

196 lines
5.2 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"with_you/internal/grpc/runner"
"with_you/internal/model"
"with_you/internal/repository"
pb "with_you/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
if _, err := fmt.Sscanf(st.Day, "%d", &dayOfWeek); err != nil {
zap.L().Warn("invalid day format in schedule sync", zap.String("day", st.Day), zap.Error(err))
dayOfWeek = 0
}
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]
}