refactor: Phase 1 - Extract TimeSlotGenerator from God Class

- Create TimeSlot model class in schedule/model/
- Create TimeSlotGenerator component in schedule/generator/
- Modify MartialScheduleArrangeServiceImpl to use TimeSlotGenerator
- Remove internal TimeSlot class (now standalone)
- Add 11 unit tests for TimeSlotGenerator
- All 438 tests passing

Related to Issue #11
This commit is contained in:
2026-01-16 23:51:55 +08:00
parent 776e9e289d
commit 596e4a274d
13 changed files with 359 additions and 87 deletions
Binary file not shown.
@@ -25,7 +25,9 @@ import org.springblade.modules.martial.mapper.*;
import org.springblade.modules.martial.pojo.entity.*;
import org.springblade.modules.martial.config.ScheduleConfig;
import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
import org.springblade.modules.martial.service.schedule.generator.TimeSlotGenerator;
import org.springframework.stereotype.Service;
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@@ -55,6 +57,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
private final MartialVenueMapper venueMapper;
private final MartialProjectMapper projectMapper;
private final ScheduleConfig scheduleConfig;
private final TimeSlotGenerator timeSlotGenerator;
@Override
public List<Long> getUnlockedCompetitions() {
@@ -352,52 +355,14 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
return athleteMapper.selectList(wrapper);
}
/**
* Generate time slots for competition
* @deprecated Use TimeSlotGenerator.generate() directly
*/
@Deprecated
private List<TimeSlot> generateTimeSlots(MartialCompetition competition) {
List<TimeSlot> timeSlots = new ArrayList<>();
LocalDateTime startTime = competition.getCompetitionStartTime();
LocalDateTime endTime = competition.getCompetitionEndTime();
if (startTime == null || endTime == null) {
log.warn("赛事时间信息不完整, 使用默认时间段");
return timeSlots;
}
LocalDate currentDate = startTime.toLocalDate();
LocalDate endDate = endTime.toLocalDate();
while (!currentDate.isAfter(endDate)) {
// 上午时段 (08:00-12:00, 共240分钟)
TimeSlot morning = new TimeSlot();
morning.setDate(currentDate);
morning.setPeriod("morning");
morning.setStartTime("08:00");
morning.setCapacity(240); // 4小时 = 240分钟
timeSlots.add(morning);
// 下午时段 (14:00-18:00, 共240分钟)
TimeSlot afternoon = new TimeSlot();
afternoon.setDate(currentDate);
afternoon.setPeriod("afternoon");
afternoon.setStartTime("14:00");
afternoon.setCapacity(240); // 4小时 = 240分钟
timeSlots.add(afternoon);
// 晚上时段 (19:00-22:00, 共180分钟)
TimeSlot evening = new TimeSlot();
evening.setDate(currentDate);
evening.setPeriod("evening");
evening.setStartTime("19:00");
evening.setCapacity(180); // 3小时 = 180分钟
timeSlots.add(evening);
currentDate = currentDate.plusDays(1);
}
log.info("生成时间段: {}天, 每天3个时段(上午/下午/晚上), 共{}个时段",
java.time.temporal.ChronoUnit.DAYS.between(startTime.toLocalDate(), endDate) + 1, timeSlots.size());
return timeSlots;
return timeSlotGenerator.generate(competition);
}
@@ -953,48 +918,6 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
}
}
// ==================== 内部数据类 ====================
private static class TimeSlot {
private LocalDate date;
private String period; // morning/afternoon
private String startTime; // 08:30/13:30
private Integer capacity; // 容量(分钟)
// Getters and Setters
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public String getPeriod() {
return period;
}
public void setPeriod(String period) {
this.period = period;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public Integer getCapacity() {
return capacity;
}
public void setCapacity(Integer capacity) {
this.capacity = capacity;
}
}
private static class ScheduleGroupData {
private String groupName;
private Long projectId;
@@ -0,0 +1,127 @@
package org.springblade.modules.martial.service.schedule.generator;
import lombok.extern.slf4j.Slf4j;
import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
/**
* Time slot generator for schedule arrangement
* Generates available time slots based on competition dates
*
* @author Refactored from MartialScheduleArrangeServiceImpl
*/
@Slf4j
@Component
public class TimeSlotGenerator {
// Default time slot configurations
private static final String MORNING_START = "08:00";
private static final String AFTERNOON_START = "14:00";
private static final String EVENING_START = "19:00";
private static final int MORNING_CAPACITY = 240; // 4 hours = 240 minutes
private static final int AFTERNOON_CAPACITY = 240; // 4 hours = 240 minutes
private static final int EVENING_CAPACITY = 180; // 3 hours = 180 minutes
/**
* Generate time slots for a competition
*
* @param competition the competition entity
* @return list of time slots
*/
public List<TimeSlot> generate(MartialCompetition competition) {
if (competition == null) {
log.warn("Competition is null, returning empty time slots");
return new ArrayList<>();
}
return generate(
competition.getCompetitionStartTime(),
competition.getCompetitionEndTime()
);
}
/**
* Generate time slots between start and end time
*
* @param startTime competition start time
* @param endTime competition end time
* @return list of time slots
*/
public List<TimeSlot> generate(LocalDateTime startTime, LocalDateTime endTime) {
List<TimeSlot> timeSlots = new ArrayList<>();
if (startTime == null || endTime == null) {
log.warn("Competition time info incomplete, returning empty time slots");
return timeSlots;
}
LocalDate currentDate = startTime.toLocalDate();
LocalDate endDate = endTime.toLocalDate();
while (!currentDate.isAfter(endDate)) {
timeSlots.addAll(generateDailySlots(currentDate));
currentDate = currentDate.plusDays(1);
}
long days = ChronoUnit.DAYS.between(startTime.toLocalDate(), endDate) + 1;
log.info("Generated time slots: {} days, 3 slots per day (morning/afternoon/evening), total {} slots",
days, timeSlots.size());
return timeSlots;
}
/**
* Generate time slots for a single day
*
* @param date the date
* @return list of time slots for that day
*/
public List<TimeSlot> generateDailySlots(LocalDate date) {
List<TimeSlot> slots = new ArrayList<>();
// Morning slot (08:00-12:00)
slots.add(TimeSlot.builder()
.date(date)
.period("morning")
.startTime(MORNING_START)
.capacity(MORNING_CAPACITY)
.build());
// Afternoon slot (14:00-18:00)
slots.add(TimeSlot.builder()
.date(date)
.period("afternoon")
.startTime(AFTERNOON_START)
.capacity(AFTERNOON_CAPACITY)
.build());
// Evening slot (19:00-22:00)
slots.add(TimeSlot.builder()
.date(date)
.period("evening")
.startTime(EVENING_START)
.capacity(EVENING_CAPACITY)
.build());
return slots;
}
/**
* Calculate total capacity in minutes
*
* @param timeSlots list of time slots
* @return total capacity in minutes
*/
public int calculateTotalCapacity(List<TimeSlot> timeSlots) {
return timeSlots.stream()
.mapToInt(TimeSlot::getCapacity)
.sum();
}
}
@@ -0,0 +1,69 @@
package org.springblade.modules.martial.service.schedule.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import java.time.LocalDate;
/**
* Time slot model for schedule arrangement
* Represents a time period (morning/afternoon/evening) on a specific date
*
* @author Refactored from MartialScheduleArrangeServiceImpl
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TimeSlot {
/**
* Date of the time slot
*/
private LocalDate date;
/**
* Period: morning, afternoon, evening
*/
private String period;
/**
* Start time in HH:mm format (e.g., "08:00", "14:00", "19:00")
*/
private String startTime;
/**
* Capacity in minutes
*/
private Integer capacity;
/**
* Get unique key for this time slot
*/
public String getSlotKey() {
return date.toString() + "_" + period;
}
/**
* Check if this is a morning slot
*/
public boolean isMorning() {
return "morning".equals(period);
}
/**
* Check if this is an afternoon slot
*/
public boolean isAfternoon() {
return "afternoon".equals(period);
}
/**
* Check if this is an evening slot
*/
public boolean isEvening() {
return "evening".equals(period);
}
}
@@ -0,0 +1,153 @@
package org.springblade.modules.martial.service.schedule.generator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for TimeSlotGenerator
*/
@DisplayName("TimeSlotGenerator Tests")
class TimeSlotGeneratorTest {
private TimeSlotGenerator generator;
@BeforeEach
void setUp() {
generator = new TimeSlotGenerator();
}
@Test
@DisplayName("Should generate 3 slots per day")
void shouldGenerate3SlotsPerDay() {
LocalDate date = LocalDate.of(2026, 1, 15);
List<TimeSlot> slots = generator.generateDailySlots(date);
assertEquals(3, slots.size());
assertTrue(slots.get(0).isMorning());
assertTrue(slots.get(1).isAfternoon());
assertTrue(slots.get(2).isEvening());
}
@Test
@DisplayName("Should generate correct time slots for single day competition")
void shouldGenerateSlotsForSingleDay() {
LocalDateTime start = LocalDateTime.of(2026, 1, 15, 8, 0);
LocalDateTime end = LocalDateTime.of(2026, 1, 15, 22, 0);
List<TimeSlot> slots = generator.generate(start, end);
assertEquals(3, slots.size());
}
@Test
@DisplayName("Should generate correct time slots for multi-day competition")
void shouldGenerateSlotsForMultiDay() {
LocalDateTime start = LocalDateTime.of(2026, 1, 15, 8, 0);
LocalDateTime end = LocalDateTime.of(2026, 1, 17, 22, 0);
List<TimeSlot> slots = generator.generate(start, end);
assertEquals(9, slots.size()); // 3 days * 3 slots
}
@Test
@DisplayName("Should return empty list when times are null")
void shouldReturnEmptyWhenTimesNull() {
List<TimeSlot> slots = generator.generate(null, null);
assertTrue(slots.isEmpty());
}
@Test
@DisplayName("Should generate from competition entity")
void shouldGenerateFromCompetition() {
MartialCompetition competition = new MartialCompetition();
competition.setCompetitionStartTime(LocalDateTime.of(2026, 1, 15, 8, 0));
competition.setCompetitionEndTime(LocalDateTime.of(2026, 1, 16, 22, 0));
List<TimeSlot> slots = generator.generate(competition);
assertEquals(6, slots.size()); // 2 days * 3 slots
}
@Test
@DisplayName("Should return empty list when competition is null")
void shouldReturnEmptyWhenCompetitionNull() {
List<TimeSlot> slots = generator.generate((MartialCompetition) null);
assertTrue(slots.isEmpty());
}
@Test
@DisplayName("Should calculate correct total capacity")
void shouldCalculateTotalCapacity() {
LocalDateTime start = LocalDateTime.of(2026, 1, 15, 8, 0);
LocalDateTime end = LocalDateTime.of(2026, 1, 15, 22, 0);
List<TimeSlot> slots = generator.generate(start, end);
int totalCapacity = generator.calculateTotalCapacity(slots);
// 240 (morning) + 240 (afternoon) + 180 (evening) = 660
assertEquals(660, totalCapacity);
}
@Test
@DisplayName("Should generate correct slot keys")
void shouldGenerateCorrectSlotKeys() {
LocalDate date = LocalDate.of(2026, 1, 15);
List<TimeSlot> slots = generator.generateDailySlots(date);
assertEquals("2026-01-15_morning", slots.get(0).getSlotKey());
assertEquals("2026-01-15_afternoon", slots.get(1).getSlotKey());
assertEquals("2026-01-15_evening", slots.get(2).getSlotKey());
}
@Test
@DisplayName("Morning slot should have correct configuration")
void morningShouldHaveCorrectConfig() {
LocalDate date = LocalDate.of(2026, 1, 15);
List<TimeSlot> slots = generator.generateDailySlots(date);
TimeSlot morning = slots.get(0);
assertEquals("08:00", morning.getStartTime());
assertEquals(240, morning.getCapacity());
assertTrue(morning.isMorning());
assertFalse(morning.isAfternoon());
assertFalse(morning.isEvening());
}
@Test
@DisplayName("Afternoon slot should have correct configuration")
void afternoonShouldHaveCorrectConfig() {
LocalDate date = LocalDate.of(2026, 1, 15);
List<TimeSlot> slots = generator.generateDailySlots(date);
TimeSlot afternoon = slots.get(1);
assertEquals("14:00", afternoon.getStartTime());
assertEquals(240, afternoon.getCapacity());
assertFalse(afternoon.isMorning());
assertTrue(afternoon.isAfternoon());
assertFalse(afternoon.isEvening());
}
@Test
@DisplayName("Evening slot should have correct configuration")
void eveningShouldHaveCorrectConfig() {
LocalDate date = LocalDate.of(2026, 1, 15);
List<TimeSlot> slots = generator.generateDailySlots(date);
TimeSlot evening = slots.get(2);
assertEquals("19:00", evening.getStartTime());
assertEquals(180, evening.getCapacity());
assertFalse(evening.isMorning());
assertFalse(evening.isAfternoon());
assertTrue(evening.isEvening());
}
}