first runnable version

This commit is contained in:
yoyuzh
2026-03-14 12:28:46 +08:00
parent 8db2fa2aab
commit 6cff15f8dc
35 changed files with 2118 additions and 256 deletions

View File

@@ -1,6 +1,7 @@
package com.yoyuzh;
import com.yoyuzh.config.CquApiProperties;
import com.yoyuzh.config.CorsProperties;
import com.yoyuzh.config.FileStorageProperties;
import com.yoyuzh.config.JwtProperties;
import org.springframework.boot.SpringApplication;
@@ -11,7 +12,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
@EnableConfigurationProperties({
JwtProperties.class,
FileStorageProperties.class,
CquApiProperties.class
CquApiProperties.class,
CorsProperties.class
})
public class PortalBackendApplication {

View File

@@ -6,6 +6,7 @@ import com.yoyuzh.auth.dto.RegisterRequest;
import com.yoyuzh.auth.dto.UserProfileResponse;
import com.yoyuzh.common.BusinessException;
import com.yoyuzh.common.ErrorCode;
import com.yoyuzh.files.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
@@ -22,6 +23,7 @@ public class AuthService {
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final FileService fileService;
@Transactional
public AuthResponse register(RegisterRequest request) {
@@ -37,6 +39,7 @@ public class AuthService {
user.setEmail(request.email());
user.setPasswordHash(passwordEncoder.encode(request.password()));
User saved = userRepository.save(user);
fileService.ensureDefaultDirectories(saved);
return new AuthResponse(jwtTokenProvider.generateToken(saved.getId(), saved.getUsername()), toProfile(saved));
}
@@ -50,6 +53,7 @@ public class AuthService {
User user = userRepository.findByUsername(request.username())
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户不存在"));
fileService.ensureDefaultDirectories(user);
return new AuthResponse(jwtTokenProvider.generateToken(user.getId(), user.getUsername()), toProfile(user));
}
@@ -68,6 +72,7 @@ public class AuthService {
created.setPasswordHash(passwordEncoder.encode("1"));
return userRepository.save(created);
});
fileService.ensureDefaultDirectories(user);
return new AuthResponse(jwtTokenProvider.generateToken(user.getId(), user.getUsername()), toProfile(user));
}

View File

@@ -0,0 +1,148 @@
package com.yoyuzh.auth;
import com.yoyuzh.config.FileStorageProperties;
import com.yoyuzh.files.FileService;
import com.yoyuzh.files.StoredFile;
import com.yoyuzh.files.StoredFileRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@Component
@Profile("dev")
@RequiredArgsConstructor
public class DevBootstrapDataInitializer implements CommandLineRunner {
private static final List<DemoUserSpec> DEMO_USERS = List.of(
new DemoUserSpec(
"portal-demo",
"portal123456",
"portal-demo@example.com",
List.of(
new DemoFileSpec("/下载", "迎新资料.txt", "text/plain", "portal-demo 的下载目录示例文件。"),
new DemoFileSpec("/文档", "课程规划.md", "text/markdown", "# 课程规划\n- 高级 Java\n- 软件工程\n- 计算机网络"),
new DemoFileSpec("/图片", "campus-shot.png", "image/png", "PNG PLACEHOLDER FOR portal-demo")
)
),
new DemoUserSpec(
"portal-study",
"study123456",
"portal-study@example.com",
List.of(
new DemoFileSpec("/下载", "实验数据.csv", "text/csv", "week,score\n1,86\n2,91\n3,95"),
new DemoFileSpec("/文档", "论文草稿.md", "text/markdown", "# 论文草稿\n研究方向人机交互与数据分析。"),
new DemoFileSpec("/图片", "data-chart.png", "image/png", "PNG PLACEHOLDER FOR portal-study")
)
),
new DemoUserSpec(
"portal-design",
"design123456",
"portal-design@example.com",
List.of(
new DemoFileSpec("/下载", "素材清单.txt", "text/plain", "图标、插画、动效资源待确认。"),
new DemoFileSpec("/文档", "作品说明.md", "text/markdown", "# 作品说明\n本账号用于 UI 方案演示与交付。"),
new DemoFileSpec("/图片", "ui-mockup.png", "image/png", "PNG PLACEHOLDER FOR portal-design")
)
)
);
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final FileService fileService;
private final StoredFileRepository storedFileRepository;
private final FileStorageProperties fileStorageProperties;
@Override
@Transactional
public void run(String... args) {
for (DemoUserSpec spec : DEMO_USERS) {
User user = ensureUser(spec);
fileService.ensureDefaultDirectories(user);
ensureDemoFiles(user, spec.files());
}
}
private User ensureUser(DemoUserSpec spec) {
return userRepository.findByUsername(spec.username())
.map(existing -> updateExistingUser(existing, spec))
.orElseGet(() -> createUser(spec));
}
private User createUser(DemoUserSpec spec) {
User created = new User();
created.setUsername(spec.username());
created.setEmail(spec.email());
created.setPasswordHash(passwordEncoder.encode(spec.password()));
return userRepository.save(created);
}
private User updateExistingUser(User existing, DemoUserSpec spec) {
boolean changed = false;
if (!spec.email().equals(existing.getEmail())) {
existing.setEmail(spec.email());
changed = true;
}
if (!passwordEncoder.matches(spec.password(), existing.getPasswordHash())) {
existing.setPasswordHash(passwordEncoder.encode(spec.password()));
changed = true;
}
return changed ? userRepository.save(existing) : existing;
}
private void ensureDemoFiles(User user, List<DemoFileSpec> files) {
for (DemoFileSpec file : files) {
if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), file.path(), file.filename())) {
continue;
}
Path filePath = resolveFilePath(user.getId(), file.path(), file.filename());
try {
Files.createDirectories(filePath.getParent());
Files.writeString(filePath, file.content(), StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new IllegalStateException("无法初始化开发样例文件: " + file.filename(), ex);
}
StoredFile storedFile = new StoredFile();
storedFile.setUser(user);
storedFile.setFilename(file.filename());
storedFile.setPath(file.path());
storedFile.setStorageName(file.filename());
storedFile.setContentType(file.contentType());
storedFile.setSize((long) file.content().getBytes(StandardCharsets.UTF_8).length);
storedFile.setDirectory(false);
storedFileRepository.save(storedFile);
}
}
private Path resolveFilePath(Long userId, String path, String filename) {
Path rootPath = Path.of(fileStorageProperties.getRootDir()).toAbsolutePath().normalize();
String normalizedPath = path.startsWith("/") ? path.substring(1) : path;
return rootPath.resolve(userId.toString()).resolve(normalizedPath).resolve(filename).normalize();
}
private record DemoUserSpec(
String username,
String password,
String email,
List<DemoFileSpec> files
) {
}
private record DemoFileSpec(
String path,
String filename,
String contentType,
String content
) {
}
}

View File

@@ -0,0 +1,23 @@
package com.yoyuzh.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
@ConfigurationProperties(prefix = "app.cors")
public class CorsProperties {
private List<String> allowedOrigins = new ArrayList<>(List.of(
"http://localhost:3000",
"http://127.0.0.1:3000"
));
public List<String> getAllowedOrigins() {
return allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
}

View File

@@ -21,6 +21,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
@@ -31,6 +36,7 @@ public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailsService userDetailsService;
private final ObjectMapper objectMapper;
private final CorsProperties corsProperties;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@@ -41,7 +47,7 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
.permitAll()
.requestMatchers("/api/files/**", "/api/user/**")
.requestMatchers("/api/files/**", "/api/user/**", "/api/cqu/**")
.authenticated()
.anyRequest()
.permitAll())
@@ -80,4 +86,18 @@ public class SecurityConfig {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(corsProperties.getAllowedOrigins());
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@@ -20,6 +20,7 @@ public class CquDataService {
private final GradeRepository gradeRepository;
private final CquApiProperties cquApiProperties;
@Transactional
public List<CourseResponse> getSchedule(User user, String semester, String studentId) {
requireLoginIfNecessary(user);
List<CourseResponse> responses = cquApiClient.fetchSchedule(semester, studentId).stream()
@@ -42,6 +43,7 @@ public class CquDataService {
return responses;
}
@Transactional
public List<GradeResponse> getGrades(User user, String semester, String studentId) {
requireLoginIfNecessary(user);
List<GradeResponse> responses = cquApiClient.fetchGrades(semester, studentId).stream()

View File

@@ -9,60 +9,101 @@ public final class CquMockDataFactory {
}
public static List<Map<String, Object>> createSchedule(String semester, String studentId) {
return List.of(
Map.of(
StudentProfile profile = StudentProfile.fromStudentId(studentId);
return profile.schedule().stream()
.map(item -> Map.<String, Object>of(
"studentId", studentId,
"semester", semester,
"courseName", "高级 Java 程序设计",
"teacher", "李老师",
"classroom", "D1131",
"dayOfWeek", 1,
"startTime", 1,
"endTime", 2
),
Map.of(
"studentId", studentId,
"semester", semester,
"courseName", "计算机网络",
"teacher", "王老师",
"classroom", "A2204",
"dayOfWeek", 3,
"startTime", 3,
"endTime", 4
),
Map.of(
"studentId", studentId,
"semester", semester,
"courseName", "软件工程",
"teacher", "周老师",
"classroom", "B3102",
"dayOfWeek", 5,
"startTime", 5,
"endTime", 6
)
);
"courseName", item.courseName(),
"teacher", item.teacher(),
"classroom", item.classroom(),
"dayOfWeek", item.dayOfWeek(),
"startTime", item.startTime(),
"endTime", item.endTime()
))
.toList();
}
public static List<Map<String, Object>> createGrades(String semester, String studentId) {
return List.of(
Map.of(
StudentProfile profile = StudentProfile.fromStudentId(studentId);
return profile.grades().stream()
.map(item -> Map.<String, Object>of(
"studentId", studentId,
"semester", semester,
"courseName", "高级 Java 程序设计",
"grade", 92.0
),
Map.of(
"studentId", studentId,
"semester", semester,
"courseName", "计算机网络",
"grade", 88.5
),
Map.of(
"studentId", studentId,
"semester", semester,
"courseName", "软件工程",
"grade", 90.0
)
);
"courseName", item.courseName(),
"grade", item.score()
))
.toList();
}
private record ScheduleEntry(
String courseName,
String teacher,
String classroom,
Integer dayOfWeek,
Integer startTime,
Integer endTime
) {
}
private record GradeEntry(String courseName, Double score) {
}
private record StudentProfile(
List<ScheduleEntry> schedule,
List<GradeEntry> grades
) {
private static StudentProfile fromStudentId(String studentId) {
return switch (studentId) {
case "2023123456" -> new StudentProfile(
List.of(
new ScheduleEntry("高级 Java 程序设计", "李老师", "D1131", 1, 1, 2),
new ScheduleEntry("计算机网络", "王老师", "A2204", 3, 3, 4),
new ScheduleEntry("软件工程", "周老师", "B3102", 5, 5, 6)
),
List.of(
new GradeEntry("高级 Java 程序设计", 92.0),
new GradeEntry("计算机网络", 88.5),
new GradeEntry("软件工程", 90.0)
)
);
case "2022456789" -> new StudentProfile(
List.of(
new ScheduleEntry("数据挖掘", "陈老师", "A1408", 2, 1, 2),
new ScheduleEntry("机器学习基础", "赵老师", "B2201", 4, 3, 4),
new ScheduleEntry("信息检索", "孙老师", "C1205", 5, 7, 8)
),
List.of(
new GradeEntry("数据挖掘", 94.0),
new GradeEntry("机器学习基础", 91.0),
new GradeEntry("信息检索", 89.0)
)
);
case "2021789012" -> new StudentProfile(
List.of(
new ScheduleEntry("交互设计", "刘老师", "艺设楼201", 1, 3, 4),
new ScheduleEntry("视觉传达专题", "黄老师", "艺设楼305", 3, 5, 6),
new ScheduleEntry("数字媒体项目实践", "许老师", "创意工坊101", 4, 7, 8)
),
List.of(
new GradeEntry("交互设计", 96.0),
new GradeEntry("视觉传达专题", 93.0),
new GradeEntry("数字媒体项目实践", 97.0)
)
);
default -> new StudentProfile(
List.of(
new ScheduleEntry("高级 Java 程序设计", "李老师", "D1131", 1, 1, 2),
new ScheduleEntry("计算机网络", "王老师", "A2204", 3, 3, 4),
new ScheduleEntry("软件工程", "周老师", "B3102", 5, 5, 6)
),
List.of(
new GradeEntry("高级 Java 程序设计", 92.0),
new GradeEntry("计算机网络", 88.5),
new GradeEntry("软件工程", 90.0)
)
);
};
}
}
}

View File

@@ -25,6 +25,7 @@ import java.util.List;
@Service
public class FileService {
private static final List<String> DEFAULT_DIRECTORIES = List.of("下载", "文档", "图片");
private final StoredFileRepository storedFileRepository;
private final Path rootPath;
@@ -118,6 +119,31 @@ public class FileService {
.toList();
}
@Transactional
public void ensureDefaultDirectories(User user) {
for (String directoryName : DEFAULT_DIRECTORIES) {
if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), "/", directoryName)) {
continue;
}
try {
Files.createDirectories(resolveUserPath(user.getId(), "/").resolve(directoryName));
} catch (IOException ex) {
throw new BusinessException(ErrorCode.UNKNOWN, "默认目录初始化失败");
}
StoredFile storedFile = new StoredFile();
storedFile.setUser(user);
storedFile.setFilename(directoryName);
storedFile.setPath("/");
storedFile.setStorageName(directoryName);
storedFile.setContentType("directory");
storedFile.setSize(0L);
storedFile.setDirectory(true);
storedFileRepository.save(storedFile);
}
}
@Transactional
public void delete(User user, Long fileId) {
StoredFile storedFile = storedFileRepository.findById(fileId)

View File

@@ -30,8 +30,12 @@ app:
max-file-size: 52428800
cqu:
base-url: https://example-cqu-api.local
require-login: false
require-login: true
mock-enabled: false
cors:
allowed-origins:
- http://localhost:3000
- http://127.0.0.1:3000
springdoc:
swagger-ui:

View File

@@ -4,6 +4,7 @@ import com.yoyuzh.auth.dto.AuthResponse;
import com.yoyuzh.auth.dto.LoginRequest;
import com.yoyuzh.auth.dto.RegisterRequest;
import com.yoyuzh.common.BusinessException;
import com.yoyuzh.files.FileService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -38,6 +39,9 @@ class AuthServiceTest {
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private FileService fileService;
@InjectMocks
private AuthService authService;
@@ -60,6 +64,7 @@ class AuthServiceTest {
assertThat(response.token()).isEqualTo("jwt-token");
assertThat(response.user().username()).isEqualTo("alice");
verify(passwordEncoder).encode("plain-password");
verify(fileService).ensureDefaultDirectories(any(User.class));
}
@Test
@@ -90,6 +95,7 @@ class AuthServiceTest {
new UsernamePasswordAuthenticationToken("alice", "plain-password"));
assertThat(response.token()).isEqualTo("jwt-token");
assertThat(response.user().email()).isEqualTo("alice@example.com");
verify(fileService).ensureDefaultDirectories(user);
}
@Test
@@ -102,4 +108,22 @@ class AuthServiceTest {
.isInstanceOf(BusinessException.class)
.hasMessageContaining("用户名或密码错误");
}
@Test
void shouldCreateDefaultDirectoriesForDevLoginUser() {
when(userRepository.findByUsername("demo")).thenReturn(Optional.empty());
when(passwordEncoder.encode("1")).thenReturn("encoded-password");
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(9L);
user.setCreatedAt(LocalDateTime.now());
return user;
});
when(jwtTokenProvider.generateToken(9L, "demo")).thenReturn("jwt-token");
AuthResponse response = authService.devLogin("demo");
assertThat(response.user().username()).isEqualTo("demo");
verify(fileService).ensureDefaultDirectories(any(User.class));
}
}

View File

@@ -0,0 +1,79 @@
package com.yoyuzh.auth;
import com.yoyuzh.config.FileStorageProperties;
import com.yoyuzh.files.FileService;
import com.yoyuzh.files.StoredFileRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DevBootstrapDataInitializerTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private FileService fileService;
@Mock
private StoredFileRepository storedFileRepository;
@Mock
private FileStorageProperties fileStorageProperties;
@InjectMocks
private DevBootstrapDataInitializer initializer;
@TempDir
Path tempDir;
@Test
void shouldCreateInitialDevUsersWhenMissing() throws Exception {
when(userRepository.findByUsername("portal-demo")).thenReturn(Optional.empty());
when(userRepository.findByUsername("portal-study")).thenReturn(Optional.empty());
when(userRepository.findByUsername("portal-design")).thenReturn(Optional.empty());
when(passwordEncoder.encode("portal123456")).thenReturn("encoded-demo-password");
when(passwordEncoder.encode("study123456")).thenReturn("encoded-study-password");
when(passwordEncoder.encode("design123456")).thenReturn("encoded-design-password");
when(storedFileRepository.existsByUserIdAndPathAndFilename(anyLong(), anyString(), anyString())).thenReturn(false);
when(fileStorageProperties.getRootDir()).thenReturn(tempDir.toString());
List<User> savedUsers = new ArrayList<>();
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId((long) (savedUsers.size() + 1));
user.setCreatedAt(LocalDateTime.now());
savedUsers.add(user);
return user;
});
initializer.run();
verify(userRepository, times(3)).save(any(User.class));
verify(fileService, times(3)).ensureDefaultDirectories(any(User.class));
org.assertj.core.api.Assertions.assertThat(savedUsers)
.extracting(User::getUsername)
.containsExactly("portal-demo", "portal-study", "portal-design");
verify(storedFileRepository, times(9)).save(any());
}
}

View File

@@ -0,0 +1,78 @@
package com.yoyuzh.cqu;
import com.yoyuzh.PortalBackendApplication;
import com.yoyuzh.auth.User;
import com.yoyuzh.auth.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@SpringBootTest(
classes = PortalBackendApplication.class,
properties = {
"spring.datasource.url=jdbc:h2:mem:cqu_tx_test;MODE=MySQL;DB_CLOSE_DELAY=-1",
"spring.datasource.driver-class-name=org.h2.Driver",
"spring.datasource.username=sa",
"spring.datasource.password=",
"spring.jpa.hibernate.ddl-auto=create-drop",
"app.cqu.require-login=true",
"app.cqu.mock-enabled=false"
}
)
class CquDataServiceTransactionTest {
@Autowired
private CquDataService cquDataService;
@Autowired
private UserRepository userRepository;
@Autowired
private GradeRepository gradeRepository;
@MockBean
private CquApiClient cquApiClient;
@Test
void shouldPersistGradesInsideTransactionForLoggedInUser() {
User user = new User();
user.setUsername("portal-demo");
user.setEmail("portal-demo@example.com");
user.setPasswordHash("encoded");
user = userRepository.save(user);
Grade existing = new Grade();
existing.setUser(user);
existing.setCourseName("Old Java");
existing.setGrade(60D);
existing.setSemester("2025-spring");
existing.setStudentId("2023123456");
gradeRepository.save(existing);
when(cquApiClient.fetchGrades("2025-spring", "2023123456")).thenReturn(List.of(
Map.of(
"courseName", "Java",
"grade", 95,
"semester", "2025-spring"
)
));
List<GradeResponse> response = cquDataService.getGrades(user, "2025-spring", "2023123456");
assertThat(response).hasSize(1);
assertThat(response.get(0).courseName()).isEqualTo("Java");
assertThat(response.get(0).grade()).isEqualTo(95D);
assertThat(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(user.getId(), "2023123456"))
.hasSize(1)
.first()
.extracting(Grade::getCourseName)
.isEqualTo("Java");
}
}

View File

@@ -26,4 +26,17 @@ class CquMockDataFactoryTest {
assertThat(result.get(0)).containsEntry("studentId", "20230001");
assertThat(result.get(0)).containsKey("grade");
}
@Test
void shouldReturnDifferentMockDataForDifferentStudents() {
List<Map<String, Object>> firstSchedule = CquMockDataFactory.createSchedule("2025-2026-1", "2023123456");
List<Map<String, Object>> secondSchedule = CquMockDataFactory.createSchedule("2025-2026-1", "2022456789");
List<Map<String, Object>> firstGrades = CquMockDataFactory.createGrades("2025-2026-1", "2023123456");
List<Map<String, Object>> secondGrades = CquMockDataFactory.createGrades("2025-2026-1", "2022456789");
assertThat(firstSchedule).extracting(item -> item.get("courseName"))
.isNotEqualTo(secondSchedule.stream().map(item -> item.get("courseName")).toList());
assertThat(firstGrades).extracting(item -> item.get("grade"))
.isNotEqualTo(secondGrades.stream().map(item -> item.get("grade")).toList());
}
}

View File

@@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageImpl;
@@ -22,6 +21,8 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -89,6 +90,25 @@ class FileServiceTest {
assertThat(result.items().get(0).filename()).isEqualTo("notes.txt");
}
@Test
void shouldCreateDefaultDirectoriesForUserWorkspace() {
User user = createUser(7L);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "下载")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "文档")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "图片")).thenReturn(false);
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
fileService.ensureDefaultDirectories(user);
assertThat(tempDir.resolve("7/下载")).exists();
assertThat(tempDir.resolve("7/文档")).exists();
assertThat(tempDir.resolve("7/图片")).exists();
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "下载");
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "文档");
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "图片");
verify(storedFileRepository, times(3)).save(any(StoredFile.class));
}
private User createUser(Long id) {
User user = new User();
user.setId(id);