Files
backend/internal/service/schedule_service.go

208 lines
5.4 KiB
Go
Raw Normal View History

package service
import (
"encoding/json"
"errors"
"regexp"
"sort"
"strings"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"gorm.io/gorm"
)
var (
ErrInvalidSchedulePayload = &ServiceError{Code: 400, Message: "invalid schedule payload"}
ErrScheduleCourseNotFound = &ServiceError{Code: 404, Message: "schedule course not found"}
ErrScheduleForbidden = &ServiceError{Code: 403, Message: "forbidden schedule operation"}
ErrScheduleColorDuplicated = &ServiceError{Code: 400, Message: "course color already used"}
)
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)
type CreateScheduleCourseInput struct {
Name string
Teacher string
Location string
DayOfWeek int
StartSection int
EndSection int
Weeks []int
Color string
}
type ScheduleService interface {
ListCourses(userID string, week int) ([]*dto.ScheduleCourseResponse, error)
CreateCourse(userID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error)
UpdateCourse(userID, courseID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error)
DeleteCourse(userID, courseID string) error
}
type scheduleService struct {
repo repository.ScheduleRepository
}
func NewScheduleService(repo repository.ScheduleRepository) ScheduleService {
return &scheduleService{repo: repo}
}
func (s *scheduleService) ListCourses(userID string, week int) ([]*dto.ScheduleCourseResponse, error) {
courses, err := s.repo.ListByUserID(userID)
if err != nil {
return nil, err
}
result := make([]*dto.ScheduleCourseResponse, 0, len(courses))
for _, item := range courses {
weeks := dto.ParseWeeksJSON(item.Weeks)
if week > 0 && !containsWeek(weeks, week) {
continue
}
result = append(result, dto.ConvertScheduleCourseToResponse(item, weeks))
}
return result, nil
}
func (s *scheduleService) CreateCourse(userID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error) {
entity, weeks, err := buildScheduleEntity(userID, input, nil)
if err != nil {
return nil, err
}
if err := s.ensureUniqueColor(userID, entity.Color, ""); err != nil {
return nil, err
}
if err := s.repo.Create(entity); err != nil {
return nil, err
}
return dto.ConvertScheduleCourseToResponse(entity, weeks), nil
}
func (s *scheduleService) UpdateCourse(userID, courseID string, input CreateScheduleCourseInput) (*dto.ScheduleCourseResponse, error) {
existing, err := s.repo.GetByID(courseID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrScheduleCourseNotFound
}
return nil, err
}
if existing.UserID != userID {
return nil, ErrScheduleForbidden
}
entity, weeks, err := buildScheduleEntity(userID, input, existing)
if err != nil {
return nil, err
}
if err := s.ensureUniqueColor(userID, entity.Color, entity.ID); err != nil {
return nil, err
}
if err := s.repo.Update(entity); err != nil {
return nil, err
}
return dto.ConvertScheduleCourseToResponse(entity, weeks), nil
}
func (s *scheduleService) DeleteCourse(userID, courseID string) error {
existing, err := s.repo.GetByID(courseID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrScheduleCourseNotFound
}
return err
}
if existing.UserID != userID {
return ErrScheduleForbidden
}
return s.repo.DeleteByID(courseID)
}
func buildScheduleEntity(userID string, input CreateScheduleCourseInput, target *model.ScheduleCourse) (*model.ScheduleCourse, []int, error) {
name := strings.TrimSpace(input.Name)
if name == "" || input.DayOfWeek < 0 || input.DayOfWeek > 6 || input.StartSection < 1 || input.EndSection < input.StartSection {
return nil, nil, ErrInvalidSchedulePayload
}
weeks := normalizeWeeks(input.Weeks)
if len(weeks) == 0 {
return nil, nil, ErrInvalidSchedulePayload
}
weeksJSON, err := json.Marshal(weeks)
if err != nil {
return nil, nil, err
}
entity := target
if entity == nil {
entity = &model.ScheduleCourse{
UserID: userID,
}
}
normalizedColor := normalizeHexColor(input.Color)
if normalizedColor == "" || !hexColorRegex.MatchString(normalizedColor) {
return nil, nil, ErrInvalidSchedulePayload
}
entity.Name = name
entity.Teacher = strings.TrimSpace(input.Teacher)
entity.Location = strings.TrimSpace(input.Location)
entity.DayOfWeek = input.DayOfWeek
entity.StartSection = input.StartSection
entity.EndSection = input.EndSection
entity.Weeks = string(weeksJSON)
entity.Color = normalizedColor
return entity, weeks, nil
}
func (s *scheduleService) ensureUniqueColor(userID, color, excludeID string) error {
exists, err := s.repo.ExistsColorByUser(userID, color, excludeID)
if err != nil {
return err
}
if exists {
return ErrScheduleColorDuplicated
}
return nil
}
func normalizeWeeks(source []int) []int {
unique := make(map[int]struct{}, len(source))
result := make([]int, 0, len(source))
for _, w := range source {
if w < 1 || w > 30 {
continue
}
if _, exists := unique[w]; exists {
continue
}
unique[w] = struct{}{}
result = append(result, w)
}
sort.Ints(result)
return result
}
func containsWeek(weeks []int, target int) bool {
for _, week := range weeks {
if week == target {
return true
}
}
return false
}
func normalizeHexColor(color string) string {
trimmed := strings.TrimSpace(color)
if trimmed == "" {
return ""
}
if strings.HasPrefix(trimmed, "#") {
return strings.ToUpper(trimmed)
}
return "#" + strings.ToUpper(trimmed)
}