修改课表模块
This commit is contained in:
@@ -35,6 +35,12 @@ public class User {
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "last_school_student_id", length = 64)
|
||||
private String lastSchoolStudentId;
|
||||
|
||||
@Column(name = "last_school_semester", length = 64)
|
||||
private String lastSchoolSemester;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
if (createdAt == null) {
|
||||
@@ -81,4 +87,20 @@ public class User {
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getLastSchoolStudentId() {
|
||||
return lastSchoolStudentId;
|
||||
}
|
||||
|
||||
public void setLastSchoolStudentId(String lastSchoolStudentId) {
|
||||
this.lastSchoolStudentId = lastSchoolStudentId;
|
||||
}
|
||||
|
||||
public String getLastSchoolSemester() {
|
||||
return lastSchoolSemester;
|
||||
}
|
||||
|
||||
public void setLastSchoolSemester(String lastSchoolSemester) {
|
||||
this.lastSchoolSemester = lastSchoolSemester;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ package com.yoyuzh.cqu;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CourseRepository extends JpaRepository<Course, Long> {
|
||||
List<Course> findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(Long userId, String studentId, String semester);
|
||||
|
||||
Optional<Course> findTopByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
void deleteByUserIdAndStudentIdAndSemester(Long userId, String studentId, String semester);
|
||||
}
|
||||
|
||||
@@ -26,16 +26,24 @@ public class CquController {
|
||||
@GetMapping("/schedule")
|
||||
public ApiResponse<List<CourseResponse>> schedule(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestParam String semester,
|
||||
@RequestParam String studentId) {
|
||||
return ApiResponse.success(cquDataService.getSchedule(resolveUser(userDetails), semester, studentId));
|
||||
@RequestParam String studentId,
|
||||
@RequestParam(defaultValue = "false") boolean refresh) {
|
||||
return ApiResponse.success(cquDataService.getSchedule(resolveUser(userDetails), semester, studentId, refresh));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取成绩")
|
||||
@GetMapping("/grades")
|
||||
public ApiResponse<List<GradeResponse>> grades(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestParam String semester,
|
||||
@RequestParam String studentId) {
|
||||
return ApiResponse.success(cquDataService.getGrades(resolveUser(userDetails), semester, studentId));
|
||||
@RequestParam String studentId,
|
||||
@RequestParam(defaultValue = "false") boolean refresh) {
|
||||
return ApiResponse.success(cquDataService.getGrades(resolveUser(userDetails), semester, studentId, refresh));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取最近一次教务数据")
|
||||
@GetMapping("/latest")
|
||||
public ApiResponse<LatestSchoolDataResponse> latest(@AuthenticationPrincipal UserDetails userDetails) {
|
||||
return ApiResponse.success(cquDataService.getLatest(resolveUser(userDetails)));
|
||||
}
|
||||
|
||||
private User resolveUser(UserDetails userDetails) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yoyuzh.cqu;
|
||||
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.common.ErrorCode;
|
||||
@@ -10,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -18,47 +20,82 @@ public class CquDataService {
|
||||
private final CquApiClient cquApiClient;
|
||||
private final CourseRepository courseRepository;
|
||||
private final GradeRepository gradeRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final CquApiProperties cquApiProperties;
|
||||
|
||||
@Transactional
|
||||
public List<CourseResponse> getSchedule(User user, String semester, String studentId) {
|
||||
return getSchedule(user, semester, studentId, false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<CourseResponse> getSchedule(User user, String semester, String studentId, boolean refresh) {
|
||||
requireLoginIfNecessary(user);
|
||||
if (user != null && !refresh) {
|
||||
List<CourseResponse> stored = readSavedSchedule(user.getId(), studentId, semester);
|
||||
if (!stored.isEmpty()) {
|
||||
rememberLastSchoolQuery(user, studentId, semester);
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
|
||||
List<CourseResponse> responses = cquApiClient.fetchSchedule(semester, studentId).stream()
|
||||
.map(this::toCourseResponse)
|
||||
.toList();
|
||||
if (user != null) {
|
||||
saveCourses(user, semester, studentId, responses);
|
||||
return courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(
|
||||
user.getId(), studentId, semester)
|
||||
.stream()
|
||||
.map(item -> new CourseResponse(
|
||||
item.getCourseName(),
|
||||
item.getTeacher(),
|
||||
item.getClassroom(),
|
||||
item.getDayOfWeek(),
|
||||
item.getStartTime(),
|
||||
item.getEndTime()))
|
||||
.toList();
|
||||
rememberLastSchoolQuery(user, studentId, semester);
|
||||
return readSavedSchedule(user.getId(), studentId, semester);
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<GradeResponse> getGrades(User user, String semester, String studentId) {
|
||||
return getGrades(user, semester, studentId, false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<GradeResponse> getGrades(User user, String semester, String studentId, boolean refresh) {
|
||||
requireLoginIfNecessary(user);
|
||||
if (user != null && !refresh
|
||||
&& gradeRepository.existsByUserIdAndStudentIdAndSemester(user.getId(), studentId, semester)) {
|
||||
rememberLastSchoolQuery(user, studentId, semester);
|
||||
return readSavedGrades(user.getId(), studentId);
|
||||
}
|
||||
|
||||
List<GradeResponse> responses = cquApiClient.fetchGrades(semester, studentId).stream()
|
||||
.map(this::toGradeResponse)
|
||||
.toList();
|
||||
if (user != null) {
|
||||
saveGrades(user, semester, studentId, responses);
|
||||
return gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(user.getId(), studentId)
|
||||
.stream()
|
||||
.map(item -> new GradeResponse(item.getCourseName(), item.getGrade(), item.getSemester()))
|
||||
.toList();
|
||||
rememberLastSchoolQuery(user, studentId, semester);
|
||||
return readSavedGrades(user.getId(), studentId);
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LatestSchoolDataResponse getLatest(User user) {
|
||||
requireLoginIfNecessary(user);
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
QueryContext context = resolveLatestContext(user);
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<CourseResponse> schedule = readSavedSchedule(user.getId(), context.studentId(), context.semester());
|
||||
List<GradeResponse> grades = readSavedGrades(user.getId(), context.studentId());
|
||||
if (schedule.isEmpty() && grades.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new LatestSchoolDataResponse(context.studentId(), context.semester(), schedule, grades);
|
||||
}
|
||||
|
||||
private void requireLoginIfNecessary(User user) {
|
||||
if (cquApiProperties.isRequireLogin() && user == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_LOGGED_IN, "该接口需要登录后访问");
|
||||
@@ -97,6 +134,77 @@ public class CquDataService {
|
||||
}).toList());
|
||||
}
|
||||
|
||||
private List<CourseResponse> readSavedSchedule(Long userId, String studentId, String semester) {
|
||||
return courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(
|
||||
userId, studentId, semester)
|
||||
.stream()
|
||||
.map(item -> new CourseResponse(
|
||||
item.getCourseName(),
|
||||
item.getTeacher(),
|
||||
item.getClassroom(),
|
||||
item.getDayOfWeek(),
|
||||
item.getStartTime(),
|
||||
item.getEndTime()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<GradeResponse> readSavedGrades(Long userId, String studentId) {
|
||||
return gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(userId, studentId)
|
||||
.stream()
|
||||
.map(item -> new GradeResponse(item.getCourseName(), item.getGrade(), item.getSemester()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void rememberLastSchoolQuery(User user, String studentId, String semester) {
|
||||
boolean changed = false;
|
||||
if (!semester.equals(user.getLastSchoolSemester())) {
|
||||
user.setLastSchoolSemester(semester);
|
||||
changed = true;
|
||||
}
|
||||
if (!studentId.equals(user.getLastSchoolStudentId())) {
|
||||
user.setLastSchoolStudentId(studentId);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
private QueryContext resolveLatestContext(User user) {
|
||||
if (hasText(user.getLastSchoolStudentId()) && hasText(user.getLastSchoolSemester())) {
|
||||
return new QueryContext(user.getLastSchoolStudentId(), user.getLastSchoolSemester());
|
||||
}
|
||||
|
||||
Optional<Course> latestCourse = courseRepository.findTopByUserIdOrderByCreatedAtDesc(user.getId());
|
||||
Optional<Grade> latestGrade = gradeRepository.findTopByUserIdOrderByCreatedAtDesc(user.getId());
|
||||
if (latestCourse.isEmpty() && latestGrade.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
QueryContext context;
|
||||
if (latestGrade.isEmpty()) {
|
||||
context = new QueryContext(latestCourse.get().getStudentId(), latestCourse.get().getSemester());
|
||||
} else if (latestCourse.isEmpty()) {
|
||||
context = new QueryContext(latestGrade.get().getStudentId(), latestGrade.get().getSemester());
|
||||
} else if (latestCourse.get().getCreatedAt().isAfter(latestGrade.get().getCreatedAt())) {
|
||||
context = new QueryContext(latestCourse.get().getStudentId(), latestCourse.get().getSemester());
|
||||
} else {
|
||||
context = new QueryContext(latestGrade.get().getStudentId(), latestGrade.get().getSemester());
|
||||
}
|
||||
|
||||
if (hasText(context.studentId()) && hasText(context.semester())) {
|
||||
user.setLastSchoolStudentId(context.studentId());
|
||||
user.setLastSchoolSemester(context.semester());
|
||||
userRepository.save(user);
|
||||
return context;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
|
||||
private CourseResponse toCourseResponse(Map<String, Object> source) {
|
||||
return new CourseResponse(
|
||||
stringValue(source, "courseName"),
|
||||
@@ -128,4 +236,7 @@ public class CquDataService {
|
||||
Object value = source.get(key);
|
||||
return value == null ? null : Double.parseDouble(value.toString());
|
||||
}
|
||||
|
||||
private record QueryContext(String studentId, String semester) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@ package com.yoyuzh.cqu;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GradeRepository extends JpaRepository<Grade, Long> {
|
||||
List<Grade> findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(Long userId, String studentId);
|
||||
|
||||
boolean existsByUserIdAndStudentIdAndSemester(Long userId, String studentId, String semester);
|
||||
|
||||
Optional<Grade> findTopByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
void deleteByUserIdAndStudentIdAndSemester(Long userId, String studentId, String semester);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.yoyuzh.cqu;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record LatestSchoolDataResponse(
|
||||
String studentId,
|
||||
String semester,
|
||||
List<CourseResponse> schedule,
|
||||
List<GradeResponse> grades
|
||||
) {
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.yoyuzh.cqu;
|
||||
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.config.CquApiProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -11,9 +12,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -28,6 +31,9 @@ class CquDataServiceTest {
|
||||
@Mock
|
||||
private GradeRepository gradeRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private CquDataService cquDataService;
|
||||
|
||||
@@ -35,7 +41,7 @@ class CquDataServiceTest {
|
||||
void shouldNormalizeScheduleFromRemoteApi() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(false);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, properties);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
when(cquApiClient.fetchSchedule("2025-2026-1", "20230001")).thenReturn(List.of(Map.of(
|
||||
"courseName", "Java",
|
||||
"teacher", "Zhang",
|
||||
@@ -56,7 +62,7 @@ class CquDataServiceTest {
|
||||
void shouldPersistGradesForLoggedInUserWhenAvailable() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, properties);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
@@ -83,4 +89,114 @@ class CquDataServiceTest {
|
||||
assertThat(response).hasSize(1);
|
||||
assertThat(response.get(0).grade()).isEqualTo(95D);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPersistedScheduleWithoutCallingRemoteApiWhenRefreshIsDisabled() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
|
||||
Course persisted = new Course();
|
||||
persisted.setUser(user);
|
||||
persisted.setCourseName("Java");
|
||||
persisted.setTeacher("Zhang");
|
||||
persisted.setClassroom("A101");
|
||||
persisted.setDayOfWeek(1);
|
||||
persisted.setStartTime(1);
|
||||
persisted.setEndTime(2);
|
||||
persisted.setSemester("2025-spring");
|
||||
persisted.setStudentId("20230001");
|
||||
|
||||
when(courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(1L, "20230001", "2025-spring"))
|
||||
.thenReturn(List.of(persisted));
|
||||
|
||||
List<CourseResponse> response = cquDataService.getSchedule(user, "2025-spring", "20230001", false);
|
||||
|
||||
assertThat(response).extracting(CourseResponse::courseName).containsExactly("Java");
|
||||
verifyNoInteractions(cquApiClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnLatestStoredSchoolDataFromPersistedUserContext() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setLastSchoolStudentId("20230001");
|
||||
user.setLastSchoolSemester("2025-spring");
|
||||
|
||||
Course course = new Course();
|
||||
course.setUser(user);
|
||||
course.setCourseName("Java");
|
||||
course.setTeacher("Zhang");
|
||||
course.setClassroom("A101");
|
||||
course.setDayOfWeek(1);
|
||||
course.setStartTime(1);
|
||||
course.setEndTime(2);
|
||||
course.setSemester("2025-spring");
|
||||
course.setStudentId("20230001");
|
||||
|
||||
Grade grade = new Grade();
|
||||
grade.setUser(user);
|
||||
grade.setCourseName("Java");
|
||||
grade.setGrade(95D);
|
||||
grade.setSemester("2025-spring");
|
||||
grade.setStudentId("20230001");
|
||||
|
||||
when(courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(1L, "20230001", "2025-spring"))
|
||||
.thenReturn(List.of(course));
|
||||
when(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(1L, "20230001"))
|
||||
.thenReturn(List.of(grade));
|
||||
|
||||
LatestSchoolDataResponse response = cquDataService.getLatest(user);
|
||||
|
||||
assertThat(response.studentId()).isEqualTo("20230001");
|
||||
assertThat(response.semester()).isEqualTo("2025-spring");
|
||||
assertThat(response.schedule()).extracting(CourseResponse::courseName).containsExactly("Java");
|
||||
assertThat(response.grades()).extracting(GradeResponse::courseName).containsExactly("Java");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToMostRecentStoredSchoolDataWhenUserContextIsEmpty() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
|
||||
Course latestCourse = new Course();
|
||||
latestCourse.setUser(user);
|
||||
latestCourse.setCourseName("Java");
|
||||
latestCourse.setTeacher("Zhang");
|
||||
latestCourse.setClassroom("A101");
|
||||
latestCourse.setDayOfWeek(1);
|
||||
latestCourse.setStartTime(1);
|
||||
latestCourse.setEndTime(2);
|
||||
latestCourse.setSemester("2025-spring");
|
||||
latestCourse.setStudentId("20230001");
|
||||
latestCourse.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
when(courseRepository.findTopByUserIdOrderByCreatedAtDesc(1L)).thenReturn(Optional.of(latestCourse));
|
||||
when(gradeRepository.findTopByUserIdOrderByCreatedAtDesc(1L)).thenReturn(Optional.empty());
|
||||
when(courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(1L, "20230001", "2025-spring"))
|
||||
.thenReturn(List.of(latestCourse));
|
||||
when(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(1L, "20230001"))
|
||||
.thenReturn(List.of());
|
||||
|
||||
LatestSchoolDataResponse response = cquDataService.getLatest(user);
|
||||
|
||||
assertThat(response.studentId()).isEqualTo("20230001");
|
||||
assertThat(response.semester()).isEqualTo("2025-spring");
|
||||
assertThat(user.getLastSchoolStudentId()).isEqualTo("20230001");
|
||||
assertThat(user.getLastSchoolSemester()).isEqualTo("2025-spring");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class CquDataServiceTransactionTest {
|
||||
)
|
||||
));
|
||||
|
||||
List<GradeResponse> response = cquDataService.getGrades(user, "2025-spring", "2023123456");
|
||||
List<GradeResponse> response = cquDataService.getGrades(user, "2025-spring", "2023123456", true);
|
||||
|
||||
assertThat(response).hasSize(1);
|
||||
assertThat(response.get(0).courseName()).isEqualTo("Java");
|
||||
@@ -74,5 +74,9 @@ class CquDataServiceTransactionTest {
|
||||
.first()
|
||||
.extracting(Grade::getCourseName)
|
||||
.isEqualTo("Java");
|
||||
assertThat(userRepository.findById(user.getId()))
|
||||
.get()
|
||||
.extracting(User::getLastSchoolStudentId, User::getLastSchoolSemester)
|
||||
.containsExactly("2023123456", "2025-spring");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user