Merge remote-tracking branch 'origin/Chester' into merge/1

# Conflicts:
#	src/main/java/org/cmh/backend/authentication/controller/AuthenticationController.java
#	src/main/java/org/cmh/backend/authentication/controller/UserController.java
#	src/main/java/org/cmh/backend/authentication/dto/LoginRequest.java
#	src/main/java/org/cmh/backend/authentication/dto/RegisterResponse.java
#	src/main/java/org/cmh/backend/authentication/model/User.java
#	src/main/java/org/cmh/backend/authentication/repository/UserRepository.java
#	src/main/java/org/cmh/backend/authentication/service/UserService.java
#	src/main/resources/application.properties
This commit is contained in:
高子兴 2024-07-05 23:59:21 +08:00
commit 8728f7f454
19 changed files with 476 additions and 146 deletions

View File

@ -93,11 +93,6 @@
<scope>runtime</scope> <scope>runtime</scope>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- <dependency>-->
<!-- <groupId>com.h2database</groupId>-->
<!-- <artifactId>h2</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>

View File

@ -0,0 +1,138 @@
package org.cmh.backend.CourseManagement.controller;
import org.cmh.backend.CourseManagement.dto.GetCourseListResponse;
import org.cmh.backend.CourseManagement.dto.MessageResponse;
import org.cmh.backend.CourseManagement.dto.CourseRequest;
import org.cmh.backend.CourseManagement.dto.SearchCourseRequest;
import org.cmh.backend.CourseManagement.model.Course;
import org.cmh.backend.CourseManagement.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import jakarta.persistence.EntityNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/courses")
public class CourseController {
private static final Logger logger = LoggerFactory.getLogger(CourseController.class);
@Autowired
private CourseService courseService;
@GetMapping
public ResponseEntity<GetCourseListResponse> getCoursesByRange(@RequestParam Integer start, @RequestParam Integer end, @RequestParam String token) {
if (start >= end) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
long courseCount = courseService.getCourseCount();
return new ResponseEntity<>(new GetCourseListResponse(courseCount, courseService.getCoursesByRange(start, end)), HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<Course> getCoursePage(@PathVariable Long id, @RequestParam String token) {
try {
return new ResponseEntity<>(courseService.getCourseById(id), HttpStatus.OK);
} catch (EntityNotFoundException e) {
logger.error("Course not found with id: {}", id, e);
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@PostMapping
public ResponseEntity<MessageResponse> createCourse(@RequestBody CourseRequest request) {
try {
courseService.createCourse(request);
} catch (DataIntegrityViolationException e) {
logger.error("Create course failed: Data integrity violation", e);
return new ResponseEntity<>(new MessageResponse("创建失败,课程已存在或缺少字段"), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
logger.error("Create course failed: {}", e.getMessage(), e);
return new ResponseEntity<>(new MessageResponse("创建失败:" + e.getMessage()), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(new MessageResponse("创建成功"), HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<MessageResponse> updateCourse(@PathVariable Long id, @RequestBody CourseRequest request) {
try {
courseService.updateCourse(id, request);
} catch (DataIntegrityViolationException e) {
logger.error("Update course failed: Data integrity violation", e);
return new ResponseEntity<>(new MessageResponse("修改失败,新标题已存在或缺少字段"), HttpStatus.BAD_REQUEST);
} catch (EntityNotFoundException e) {
logger.error("Course not found with id: {}", id, e);
return new ResponseEntity<>(new MessageResponse("修改失败: 课程不存在"), HttpStatus.NOT_FOUND);
} catch (Exception e) {
logger.error("Update course failed: {}", e.getMessage(), e);
return new ResponseEntity<>(new MessageResponse("修改失败:" + e.getMessage()), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(new MessageResponse("修改成功"), HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<MessageResponse> deleteCourse(@PathVariable Long id, @RequestParam String token) {
try {
courseService.deleteCourse(id);
} catch (EntityNotFoundException e) {
logger.error("Course not found with id: {}", id, e);
return new ResponseEntity<>(new MessageResponse("删除失败,课程不存在"), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
logger.error("Delete course failed: {}", e.getMessage(), e);
return new ResponseEntity<>(new MessageResponse("删除失败:" + e.getMessage()), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(new MessageResponse("删除成功"), HttpStatus.OK);
}
@GetMapping("/search")
public ResponseEntity<GetCourseListResponse> searchCourses(@RequestParam String token,
@RequestParam(required = false) String title,
@RequestParam(required = false) String author,
@RequestParam(required = false) String description,
@RequestParam(required = false) String sortOrder,
@RequestParam Integer start,
@RequestParam Integer end) {
if (start >= end) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
try {
SearchCourseRequest request = new SearchCourseRequest(token, title, author, description, sortOrder, start, end);
List<Course> courses = courseService.searchCourses(request);
long courseCount = courseService.getCourseCountByCriteria(title, author, description, sortOrder);
return new ResponseEntity<>(new GetCourseListResponse(courseCount, courses), HttpStatus.OK);
} catch (Exception e) {
logger.error("Search courses failed with parameters: title={}, author={}, description={}, sortOrder={}, start={}, end={}",
title, author, description, sortOrder, start, end, e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/sort")
public ResponseEntity<GetCourseListResponse> sortCourses(@RequestParam String token,
@RequestParam String sortField,
@RequestParam String sortDirection,
@RequestParam Integer start,
@RequestParam Integer end) {
if (start >= end) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
try {
long courseCount = courseService.getCourseCount();
List<Course> sortedCourses = courseService.sortCourses(sortField, sortDirection, start, end);
return new ResponseEntity<>(new GetCourseListResponse(courseCount, sortedCourses), HttpStatus.OK);
} catch (Exception e) {
logger.error("Sort courses failed with parameters: sortField={}, sortDirection={}, start={}, end={}",
sortField, sortDirection, start, end, e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -0,0 +1,64 @@
package org.cmh.backend.CourseManagement.controller;
import org.cmh.backend.CourseManagement.dto.UploadFileResponse;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
@RequestMapping("/courses")
public class FileController {
private static final String UPLOAD_DIR = "uploads/";
@PostMapping("/upload")
public ResponseEntity<UploadFileResponse> uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return new ResponseEntity<>(new UploadFileResponse("文件不能为空", null), HttpStatus.BAD_REQUEST);
}
try {
// 确保上传目录存在
Path uploadDirPath = Paths.get(UPLOAD_DIR);
if (!Files.exists(uploadDirPath)) {
Files.createDirectories(uploadDirPath);
}
// 生成文件路径
byte[] bytes = file.getBytes();
Path path = Paths.get(UPLOAD_DIR + file.getOriginalFilename());
Files.write(path, bytes);
// 返回成功信息
return new ResponseEntity<>(new UploadFileResponse("文件上传成功", "/api/courses/files/" + file.getOriginalFilename()), HttpStatus.OK);
} catch (IOException e) {
return new ResponseEntity<>(new UploadFileResponse("文件上传失败", null), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/files/{filename}")
public ResponseEntity<Resource> getFile(@PathVariable String filename) {
try {
Path filePath = Paths.get(UPLOAD_DIR).resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists() && resource.isReadable()) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -0,0 +1,16 @@
package org.cmh.backend.CourseManagement.dto;
import lombok.Getter;
import lombok.Setter;
import org.cmh.backend.Utils.JwtRequest;
@Getter
@Setter
public class CourseRequest extends JwtRequest {
private String title;
private String description;
private String orderNo;
private String author;
private String videoPath;
private String imagePath;
}

View File

@ -0,0 +1,16 @@
package org.cmh.backend.CourseManagement.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.cmh.backend.CourseManagement.model.Course;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
public class GetCourseListResponse {
Long courseCount;
List<Course> courseList;
}

View File

@ -0,0 +1,12 @@
package org.cmh.backend.CourseManagement.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class MessageResponse {
String message;
}

View File

@ -0,0 +1,18 @@
package org.cmh.backend.CourseManagement.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class SearchCourseRequest {
private String token;
private String title;
private String author;
private String description;
private String sortOrder;
private int start;
private int end;
}

View File

@ -0,0 +1,15 @@
package org.cmh.backend.CourseManagement.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UploadFileResponse extends MessageResponse {
private String url;
public UploadFileResponse(String message, String url) {
super(message);
this.url = url;
}
}

View File

@ -0,0 +1,21 @@
package org.cmh.backend.CourseManagement.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
private String orderNo;
private String author;
private String videoPath;
private String imagePath;
}

View File

@ -0,0 +1,13 @@
package org.cmh.backend.CourseManagement.repository;
import org.cmh.backend.CourseManagement.model.Course;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long>, JpaSpecificationExecutor<Course> {
Page<Course> findAllByOrderByIdDesc(Pageable pageable);
}

View File

@ -0,0 +1,82 @@
package org.cmh.backend.CourseManagement.service;
import org.cmh.backend.CourseManagement.dto.CourseRequest;
import org.cmh.backend.CourseManagement.dto.SearchCourseRequest;
import org.cmh.backend.CourseManagement.model.Course;
import org.cmh.backend.CourseManagement.repository.CourseRepository;
import org.cmh.backend.CourseManagement.specification.CourseSpecification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
@Service
public class CourseService {
@Autowired
private CourseRepository courseRepository;
public long getCourseCount() {
return courseRepository.count();
}
public List<Course> getCoursesByRange(int start, int end) {
Pageable pageable = PageRequest.of(start, end - start);
return courseRepository.findAll(pageable).getContent();
}
public Course getCourseById(Long id) {
return courseRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Course not found"));
}
public void createCourse(CourseRequest request) {
Course course = new Course();
course.setTitle(request.getTitle());
course.setAuthor(request.getAuthor());
course.setDescription(request.getDescription());
course.setOrderNo(request.getOrderNo());
course.setVideoPath(request.getVideoPath()); // 确保保存视频路径
course.setImagePath(request.getImagePath()); // 确保保存图片路径
courseRepository.save(course);
}
public void updateCourse(Long id, CourseRequest request) {
Course course = courseRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Course not found"));
course.setTitle(request.getTitle());
course.setAuthor(request.getAuthor());
course.setDescription(request.getDescription());
course.setOrderNo(request.getOrderNo());
course.setVideoPath(request.getVideoPath()); // 确保更新视频路径
course.setImagePath(request.getImagePath()); // 确保更新图片路径
courseRepository.save(course);
}
public void deleteCourse(Long id) {
courseRepository.deleteById(id);
}
public List<Course> searchCourses(SearchCourseRequest request) {
Pageable pageable = PageRequest.of(request.getStart(), request.getEnd() - request.getStart());
return courseRepository.findAll(
CourseSpecification.searchCourses(
request.getTitle(),
request.getAuthor(),
request.getDescription(),
request.getSortOrder()
),
pageable
).getContent();
}
public long getCourseCountByCriteria(String title, String author, String description, String sortOrder) {
return courseRepository.count(CourseSpecification.searchCourses(title, author, description, sortOrder));
}
public List<Course> sortCourses(String sortField, String sortDirection, int start, int end) {
Pageable pageable = PageRequest.of(start, end - start);
return courseRepository.findAll(
CourseSpecification.sortCourses(sortField, sortDirection),
pageable
).getContent();
}
}

View File

@ -0,0 +1,44 @@
package org.cmh.backend.CourseManagement.specification;
import org.cmh.backend.CourseManagement.model.Course;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
public class CourseSpecification {
public static Specification<Course> searchCourses(
String title, String author, String description, String sortOrder) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (title != null && !title.isEmpty()) {
predicates.add(criteriaBuilder.like(root.get("title"), "%" + title + "%"));
}
if (author != null && !author.isEmpty()) {
predicates.add(criteriaBuilder.like(root.get("author"), "%" + author + "%"));
}
if (description != null && !description.isEmpty()) {
predicates.add(criteriaBuilder.like(root.get("description"), "%" + description + "%"));
}
if (sortOrder != null && !sortOrder.isEmpty()) {
predicates.add(criteriaBuilder.like(root.get("orderNo"), "%" + sortOrder+ "%"));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
public static Specification<Course> sortCourses(String sortField, String sortDirection) {
return (root, query, cb) -> {
if ("asc".equalsIgnoreCase(sortDirection)) {
query.orderBy(cb.asc(root.get(sortField)));
} else if ("desc".equalsIgnoreCase(sortDirection)) {
query.orderBy(cb.desc(root.get(sortField)));
}
return cb.conjunction();
};
}
}

View File

@ -0,0 +1,19 @@
package org.cmh.backend.authentication.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
public class ProfileResponse {
private String username;
private String email;
private String role;
private String phoneNumber;
private String company;
private LocalDateTime createdDate;
}

View File

@ -0,0 +1,15 @@
package org.cmh.backend.authentication.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class UpdateRequest {
private String username;
private String email;
private String phoneNumber;
private String company;
}

View File

@ -23,7 +23,8 @@ server.servlet.encoding.charset=utf-8
# verificationCode # verificationCode
verification.code.images.path=src/main/resources/static/verificationCodeImages verification.code.images.path=src/main/resources/static/verificationCodeImages
# set the max size of a single file # set the max size of a single file
spring.servlet.multipart.max-file-size=50MB spring.servlet.multipart.max-file-size=500MB
# set the max size of the total request # set the max size of the total request
spring.servlet.multipart.max-request-size=50MB spring.servlet.multipart.max-request-size=500MB

View File

@ -1,15 +0,0 @@
package org.cmh.backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@Import(TestcontainersConfiguration.class)
@SpringBootTest
class BackendApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -1,11 +0,0 @@
package org.cmh.backend;
import org.springframework.boot.SpringApplication;
public class TestBackendApplication {
public static void main(String[] args) {
SpringApplication.from(BackendApplication::main).with(TestcontainersConfiguration.class).run(args);
}
}

View File

@ -1,25 +0,0 @@
package org.cmh.backend;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
MariaDBContainer<?> mariaDbContainer() {
return new MariaDBContainer<>(DockerImageName.parse("mariadb:latest"));
}
@Bean
@ServiceConnection
MySQLContainer<?> mysqlContainer() {
return new MySQLContainer<>(DockerImageName.parse("mysql:latest"));
}
}

View File

@ -1,88 +0,0 @@
package org.cmh.backend.Utils;
import org.cmh.backend.authentication.service.UserService;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
public class JwtVerifyAspectTest {
@Configuration
@EnableAspectJAutoProxy
@Import({JwtVerifyAspect.class})
static class Config {
@Bean
public JwtUtil jwtUtil() {
return Mockito.mock(JwtUtil.class);
}
@Bean
public UserService userService() {
return Mockito.mock(UserService.class);
}
}
private JwtUtil jwtUtil = new JwtUtil();
@InjectMocks
private JwtVerifyAspect jwtVerifyAspect;
@BeforeClass
public static void setUpClass() {
// Static setup if needed
}
@Before
public void setUp() {
Mockito.when(jwtUtil.isTokenValid("validToken")).thenReturn(true);
Mockito.when(jwtUtil.isTokenValid("invalidToken")).thenReturn(false);
}
// TODO:这个测试跑不动有问题先取消掉
// @Test
// public void testVerify() {
// SomeController validTokenController = new SomeController("validToken");
// SomeController invalidTokenController = new SomeController("invalidToken");
//
// Assert.assertTrue("Valid token should pass verification", validTokenController.run());
// Assert.assertFalse("Invalid token should fail verification", invalidTokenController.run());
// }
}
class SomeController {
private SomeJwtRequest request;
SomeController(String token) {
this.request = new SomeJwtRequest(token, "test");
}
public boolean run() {
try {
return verify(request);
} catch (JwtValidationException e) {
return false;
}
}
@JwtVerify
public boolean verify(SomeJwtRequest request) {
return true;
}
}
class SomeJwtRequest extends JwtRequest {
String msg;
public SomeJwtRequest(String token, String msg) {
super.setToken(token);
this.msg = msg;
}
}