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
196 lines
5.2 KiB
Go
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]
|
|
}
|