first runnable version
This commit is contained in:
@@ -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 {
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
23
backend/src/main/java/com/yoyuzh/config/CorsProperties.java
Normal file
23
backend/src/main/java/com/yoyuzh/config/CorsProperties.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user