Răsfoiți Sursa

Merge branch 'dev' of Yumin/EDUServer into master

王育民 6 ani în urmă
părinte
comite
ece984167f
74 a modificat fișierele cu 3132 adăugiri și 64 ștergeri
  1. 19 0
      pom.xml
  2. 14 0
      src/main/java/cn/minbb/edu/EDUApplication.java
  3. 36 0
      src/main/java/cn/minbb/edu/config/AppInterceptor.java
  4. 38 0
      src/main/java/cn/minbb/edu/config/WebMvcConfig.java
  5. 99 0
      src/main/java/cn/minbb/edu/config/WebSecurityConfig.java
  6. 13 0
      src/main/java/cn/minbb/edu/controller/UniqueNameGenerator.java
  7. 47 0
      src/main/java/cn/minbb/edu/controller/rest/CourseController.java
  8. 28 0
      src/main/java/cn/minbb/edu/controller/rest/MainController.java
  9. 21 11
      src/main/java/cn/minbb/edu/controller/rest/SignController.java
  10. 28 0
      src/main/java/cn/minbb/edu/controller/web/AdminController.java
  11. 107 0
      src/main/java/cn/minbb/edu/controller/web/CourseController.java
  12. 47 0
      src/main/java/cn/minbb/edu/controller/web/FileController.java
  13. 49 6
      src/main/java/cn/minbb/edu/controller/web/MainController.java
  14. 28 0
      src/main/java/cn/minbb/edu/controller/web/UserController.java
  15. 66 1
      src/main/java/cn/minbb/edu/data/ResponseResult.java
  16. 24 0
      src/main/java/cn/minbb/edu/handler/AccessDenied.java
  17. 34 0
      src/main/java/cn/minbb/edu/handler/AppExceptionHandler.java
  18. 48 0
      src/main/java/cn/minbb/edu/handler/LoginAuthenticationFailureHandler.java
  19. 59 0
      src/main/java/cn/minbb/edu/handler/LoginAuthenticationSuccessHandler.java
  20. 19 0
      src/main/java/cn/minbb/edu/handler/LoginUrlEntryPoint.java
  21. 47 0
      src/main/java/cn/minbb/edu/handler/LogoutSuccessHandler.java
  22. 86 0
      src/main/java/cn/minbb/edu/model/Banner.java
  23. 86 0
      src/main/java/cn/minbb/edu/model/Course.java
  24. 62 0
      src/main/java/cn/minbb/edu/model/Homework.java
  25. 72 0
      src/main/java/cn/minbb/edu/model/Study.java
  26. 124 5
      src/main/java/cn/minbb/edu/model/User.java
  27. 57 0
      src/main/java/cn/minbb/edu/model/UserRole.java
  28. 11 0
      src/main/java/cn/minbb/edu/model/repository/BannerRepository.java
  29. 7 0
      src/main/java/cn/minbb/edu/model/repository/CourseRepository.java
  30. 7 0
      src/main/java/cn/minbb/edu/model/repository/HomeworkRepository.java
  31. 7 0
      src/main/java/cn/minbb/edu/model/repository/StudyRepository.java
  32. 14 0
      src/main/java/cn/minbb/edu/model/repository/UserRoleRepository.java
  33. 11 0
      src/main/java/cn/minbb/edu/service/BannerService.java
  34. 15 0
      src/main/java/cn/minbb/edu/service/CourseService.java
  35. 7 0
      src/main/java/cn/minbb/edu/service/HomeworkService.java
  36. 7 0
      src/main/java/cn/minbb/edu/service/StudyService.java
  37. 16 0
      src/main/java/cn/minbb/edu/service/UserRoleService.java
  38. 4 1
      src/main/java/cn/minbb/edu/service/UserService.java
  39. 32 0
      src/main/java/cn/minbb/edu/service/impl/BannerServiceImpl.java
  40. 48 0
      src/main/java/cn/minbb/edu/service/impl/CourseServiceImpl.java
  41. 24 0
      src/main/java/cn/minbb/edu/service/impl/HomeworkServiceImpl.java
  42. 24 0
      src/main/java/cn/minbb/edu/service/impl/StudyServiceImpl.java
  43. 42 0
      src/main/java/cn/minbb/edu/service/impl/UserRoleServiceImpl.java
  44. 51 0
      src/main/java/cn/minbb/edu/service/impl/UserServiceImpl.java
  45. 22 0
      src/main/java/cn/minbb/edu/storage/FileService.java
  46. 80 0
      src/main/java/cn/minbb/edu/storage/FileServiceImpl.java
  47. 12 0
      src/main/java/cn/minbb/edu/storage/StorageException.java
  48. 12 0
      src/main/java/cn/minbb/edu/storage/StorageFileNotFoundException.java
  49. 57 0
      src/main/java/cn/minbb/edu/storage/StorageProperties.java
  50. 24 0
      src/main/java/cn/minbb/edu/storage/StorageService.java
  51. 184 0
      src/main/java/cn/minbb/edu/storage/StorageServiceFileSystem.java
  52. 10 0
      src/main/java/cn/minbb/edu/system/Const.java
  53. 74 0
      src/main/java/cn/minbb/edu/system/UserSession.java
  54. 74 0
      src/main/java/cn/minbb/edu/task/InitDataRunner.java
  55. 16 0
      src/main/java/cn/minbb/edu/util/RequestUtil.java
  56. 50 0
      src/main/java/cn/minbb/edu/util/SortTools.java
  57. 1 1
      src/main/resources/application-dev.properties
  58. 1 1
      src/main/resources/application-pro.properties
  59. 6 1
      src/main/resources/application.properties
  60. 60 0
      src/main/resources/static/css/user-sign.css
  61. BIN
      src/main/resources/static/favicon.ico
  62. 17 0
      src/main/resources/templates/admin-config.html
  63. 59 0
      src/main/resources/templates/course-center.html
  64. 56 0
      src/main/resources/templates/course-create.html
  65. 46 0
      src/main/resources/templates/course-player.html
  66. 17 0
      src/main/resources/templates/course-square.html
  67. 17 0
      src/main/resources/templates/fragments/footer.html
  68. 70 0
      src/main/resources/templates/fragments/header.html
  69. 145 0
      src/main/resources/templates/index-about.html
  70. 138 2
      src/main/resources/templates/index.html
  71. 45 0
      src/main/resources/templates/layouts/layout.html
  72. 48 35
      src/main/resources/templates/sign-in.html
  73. 89 0
      src/main/resources/templates/sign-up.html
  74. 17 0
      src/main/resources/templates/user-center.html

+ 19 - 0
pom.xml

@@ -50,6 +50,25 @@
             <artifactId>spring-boot-starter-thymeleaf</artifactId>
         </dependency>
 
+        <!-- 避免 layout:fragment 失效 -->
+        <dependency>
+            <groupId>nz.net.ultraq.thymeleaf</groupId>
+            <artifactId>thymeleaf-layout-dialect</artifactId>
+            <version>2.4.1</version>
+        </dependency>
+
+        <!-- Spring Security -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <!-- MySQL 数据库依赖 -->
         <dependency>
             <groupId>mysql</groupId>

+ 14 - 0
src/main/java/cn/minbb/edu/EDUApplication.java

@@ -1,12 +1,21 @@
 package cn.minbb.edu;
 
+import cn.minbb.edu.controller.UniqueNameGenerator;
+import cn.minbb.edu.storage.StorageProperties;
+import cn.minbb.edu.storage.StorageService;
 import org.springframework.boot.Banner;
+import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
 
 @SpringBootApplication
+@EnableConfigurationProperties(StorageProperties.class)
+@ComponentScan(nameGenerator = UniqueNameGenerator.class)
 public class EDUApplication extends SpringBootServletInitializer {
 
     public static void main(String[] args) {
@@ -19,4 +28,9 @@ public class EDUApplication extends SpringBootServletInitializer {
     protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
         return builder.sources(EDUApplication.class);
     }
+
+    @Bean
+    CommandLineRunner init(StorageService storageService) {
+        return args -> storageService.init();
+    }
 }

+ 36 - 0
src/main/java/cn/minbb/edu/config/AppInterceptor.java

@@ -0,0 +1,36 @@
+package cn.minbb.edu.config;
+
+import cn.minbb.edu.model.User;
+import cn.minbb.edu.system.UserSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// 自定义拦截器
+public class AppInterceptor extends HandlerInterceptorAdapter {
+
+    private Logger logger = LoggerFactory.getLogger(AppInterceptor.class);
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        return super.preHandle(request, response, handler);
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+        super.postHandle(request, response, handler, modelAndView);
+        if (null != modelAndView) {
+            User user = UserSession.getUserAuthentication();
+            if (null != user) modelAndView.addObject("user", user);
+        }
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        super.afterCompletion(request, response, handler, ex);
+    }
+}

+ 38 - 0
src/main/java/cn/minbb/edu/config/WebMvcConfig.java

@@ -0,0 +1,38 @@
+package cn.minbb.edu.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 多个拦截器组成一个拦截器链
+        registry.addInterceptor(appInterceptor());
+    }
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        // 和页面有关的静态目录都放在项目的 static 目录下
+        registry.addResourceHandler("/**")
+                .addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/")
+                .setCachePeriod(0);
+    }
+
+    @Override
+    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
+    }
+
+    // 自定义拦截器
+    @Bean
+    public AppInterceptor appInterceptor() {
+        return new AppInterceptor();
+    }
+}

+ 99 - 0
src/main/java/cn/minbb/edu/config/WebSecurityConfig.java

@@ -0,0 +1,99 @@
+package cn.minbb.edu.config;
+
+import cn.minbb.edu.handler.*;
+import cn.minbb.edu.model.repository.UserRepository;
+import cn.minbb.edu.service.impl.UserServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    private Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
+
+    private UserRepository userRepository;
+    private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;
+    private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;
+    private LogoutSuccessHandler logoutSuccessHandler;
+
+    @Autowired
+    public WebSecurityConfig(
+            UserRepository userRepository,
+            LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler,
+            LoginAuthenticationFailureHandler loginAuthenticationFailureHandler,
+            LogoutSuccessHandler logoutSuccessHandler) {
+        this.userRepository = userRepository;
+        this.loginAuthenticationSuccessHandler = loginAuthenticationSuccessHandler;
+        this.loginAuthenticationFailureHandler = loginAuthenticationFailureHandler;
+        this.logoutSuccessHandler = logoutSuccessHandler;
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http.exceptionHandling()
+                .authenticationEntryPoint(new LoginUrlEntryPoint())
+                .accessDeniedHandler(new AccessDenied())
+                .and().csrf().disable().authorizeRequests()
+                .antMatchers("/user/**").access("hasRole('USER')")
+                .antMatchers("/course/**").access("hasRole('USER') and hasRole('TEACHER')")
+                .antMatchers("/admin/**").access("hasRole('USER') and  hasRole('ADMIN')")
+                .antMatchers("/swagger-ui.html").access("hasRole('ADMIN')")
+                .antMatchers("/css/**", "/js/**", "/images/**", "/fonts/*").permitAll()
+                .and().formLogin().loginPage("/sign-in").loginProcessingUrl("/sign-in")
+                // 用户名字段和密码字段
+                .usernameParameter("username").passwordParameter("password")
+                // 登陆成功后跳转的请求和验证失败后跳转的请求
+                .defaultSuccessUrl("/admin").failureUrl("/sign-in?error=true").permitAll()
+                .successHandler(loginAuthenticationSuccessHandler)
+                .failureHandler(loginAuthenticationFailureHandler)
+                .and().logout().logoutUrl("/sign-out").logoutSuccessUrl("").logoutSuccessHandler(logoutSuccessHandler)
+                // 使 Session 失效
+                .invalidateHttpSession(true)
+                // 清除认证信息
+                .clearAuthentication(true).permitAll()
+                // 开启 cookie 存储用户信息
+                .and().rememberMe().rememberMeParameter("remember").rememberMeCookieName("remember")
+                // cookie 有效期为两个星期(秒)
+                .tokenValiditySeconds(1209600)
+                .and().authorizeRequests()
+                .anyRequest().permitAll()
+                // Session 管理
+                .and().sessionManagement()
+                // 系统中同一个账号的登陆数量限制
+                .maximumSessions(1).sessionRegistry(new SessionRegistryImpl())
+                .and().and().headers().frameOptions().disable()
+                .and().authorizeRequests();
+    }
+
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+        auth.userDetailsService(new UserServiceImpl(userRepository)).passwordEncoder(new BCryptPasswordEncoder() {
+            @Override
+            public String encode(CharSequence rawPassword) {
+                logger.info("加密时待加密的密码 1 = {}", rawPassword);
+                logger.info("加密原始密码 2 = {}", new BCryptPasswordEncoder().encode(rawPassword));
+                return new BCryptPasswordEncoder().encode(rawPassword);
+            }
+
+            @Override
+            public boolean matches(CharSequence rawPassword, String encodedPassword) {
+                logger.info("用户输入密码 = {}", rawPassword);
+                logger.info("数据库密码 = {}", encodedPassword);
+                logger.info("用户输入密码加密 = {}", new BCryptPasswordEncoder().encode(rawPassword));
+                if (!encodedPassword.contentEquals(rawPassword))
+                    throw new BadCredentialsException("密码错误!");
+                return true;
+            }
+        });
+    }
+}

+ 13 - 0
src/main/java/cn/minbb/edu/controller/UniqueNameGenerator.java

@@ -0,0 +1,13 @@
+package cn.minbb.edu.controller;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.annotation.AnnotationBeanNameGenerator;
+
+public class UniqueNameGenerator extends AnnotationBeanNameGenerator {
+    @Override
+    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
+        // 全限定类名
+        return definition.getBeanClassName();
+    }
+}

+ 47 - 0
src/main/java/cn/minbb/edu/controller/rest/CourseController.java

@@ -0,0 +1,47 @@
+package cn.minbb.edu.controller.rest;
+
+import cn.minbb.edu.data.ResponseResult;
+import cn.minbb.edu.model.Course;
+import cn.minbb.edu.service.CourseService;
+import cn.minbb.edu.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("app/course")
+public class CourseController {
+
+    private UserService userService;
+    private CourseService courseService;
+
+    @Autowired
+    public CourseController(UserService userService, CourseService courseService) {
+        this.userService = userService;
+        this.courseService = courseService;
+    }
+
+    @GetMapping("")
+    public ResponseResult<Course> course(@RequestParam("id") Integer id) {
+        Course course = courseService.findOneById(id);
+        course.setVoTeacher(userService.findUserById(course.getTeacherId()));
+        return ResponseResult.ok(true).code(1).data(course);
+    }
+
+    @PostMapping("hot")
+    public ResponseResult<Course> courseHot() {
+        return ResponseResult.ok(true).code(1).dataset(courseService.findAll());
+    }
+
+    @PostMapping("excellent")
+    public ResponseResult<Course> courseExcellent() {
+        List<Course> courseList = courseService.findAll();
+        return ResponseResult.ok(true).code(1).message(courseList.size() + "").dataset(courseList);
+    }
+
+    @PostMapping("all")
+    public ResponseResult<Course> courseAll() {
+        return ResponseResult.ok(true).code(1).dataset(courseService.findAll());
+    }
+}

+ 28 - 0
src/main/java/cn/minbb/edu/controller/rest/MainController.java

@@ -0,0 +1,28 @@
+package cn.minbb.edu.controller.rest;
+
+import cn.minbb.edu.data.ResponseResult;
+import cn.minbb.edu.model.Banner;
+import cn.minbb.edu.service.BannerService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("")
+public class MainController {
+
+    private BannerService bannerService;
+
+    public MainController(BannerService bannerService) {
+        this.bannerService = bannerService;
+    }
+
+    @GetMapping("banners")
+    public ResponseResult<Banner> banners() {
+        ResponseResult<Banner> result = new ResponseResult<>();
+        result.setSuccess(true);
+        result.setDataset(bannerService.findAll());
+        result.setMessage("获取成功");
+        return result;
+    }
+}

+ 21 - 11
src/main/java/cn/minbb/edu/controller/rest/UserController.java → src/main/java/cn/minbb/edu/controller/rest/SignController.java

@@ -2,22 +2,28 @@ package cn.minbb.edu.controller.rest;
 
 import cn.minbb.edu.data.ResponseResult;
 import cn.minbb.edu.model.User;
+import cn.minbb.edu.model.UserRole;
+import cn.minbb.edu.service.UserRoleService;
 import cn.minbb.edu.service.UserService;
 import com.alibaba.fastjson.JSONObject;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.HashSet;
+import java.util.Set;
 
 @RestController
-@RequestMapping(value = "user")
-public class UserController {
+@RequestMapping("app/user")
+public class SignController {
 
     private UserService userService;
+    private UserRoleService userRoleService;
 
     @Autowired
-    public UserController(UserService userService) {
+    public SignController(UserService userService, UserRoleService userRoleService) {
         this.userService = userService;
+        this.userRoleService = userRoleService;
     }
 
     @PostMapping(value = "login")
@@ -30,19 +36,19 @@ public class UserController {
         if (user == null) {
             // 用户不存在
             result.setCode(0);
-            result.setStatus(false);
+            result.setSuccess(false);
             result.setMessage("用户不存在");
         } else {
             if (password.equals(user.getPassword())) {
                 // OK
                 result.setCode(1);
-                result.setStatus(true);
+                result.setSuccess(true);
                 result.setMessage("登录成功");
                 result.setData(user);
             } else {
                 // 密码错误
                 result.setCode(0);
-                result.setStatus(false);
+                result.setSuccess(false);
                 result.setMessage("密码错误");
             }
         }
@@ -57,29 +63,33 @@ public class UserController {
             String username = userClient.getUsername();
             if (userService.findUserByUsername(username) == null) {
                 // 用户不存在 - 可以注册
-                User user = userService.save(new User(username, userClient.getPassword()));
+                User u = new User(userClient.getName(), username, userClient.getPassword());
+                Set<UserRole> userRoleSet = new HashSet<>();
+                userRoleSet.add(userRoleService.findOneByRole(UserRole.Role.USER));
+                u.setUserRoleSet(userRoleSet);
+                User user = userService.save(u);
                 if (user != null) {
                     // 注册成功
                     result.setCode(1);
-                    result.setStatus(true);
+                    result.setSuccess(true);
                     result.setMessage("注册成功");
                     result.setData(user);
                 } else {
                     // 注册失败
                     result.setCode(0);
-                    result.setStatus(false);
+                    result.setSuccess(false);
                     result.setMessage("注册失败");
                 }
             } else {
                 // 用户存在 - 不可以注册
                 result.setCode(0);
-                result.setStatus(false);
+                result.setSuccess(false);
                 result.setMessage("用户已存在");
             }
         } else {
             // 错误的注册请求
             result.setCode(0);
-            result.setStatus(false);
+            result.setSuccess(false);
             result.setMessage("错误的注册请求");
         }
         return result;

+ 28 - 0
src/main/java/cn/minbb/edu/controller/web/AdminController.java

@@ -0,0 +1,28 @@
+package cn.minbb.edu.controller.web;
+
+import cn.minbb.edu.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Controller
+@RequestMapping("admin")
+public class AdminController {
+
+    private UserService userService;
+
+    @Autowired
+    public AdminController(UserService userService) {
+        this.userService = userService;
+    }
+
+    @GetMapping(value = "config")
+    public ModelAndView configPage(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("admin-config");
+        return modelAndView;
+    }
+}

+ 107 - 0
src/main/java/cn/minbb/edu/controller/web/CourseController.java

@@ -0,0 +1,107 @@
+package cn.minbb.edu.controller.web;
+
+import cn.minbb.edu.model.Course;
+import cn.minbb.edu.model.User;
+import cn.minbb.edu.service.CourseService;
+import cn.minbb.edu.service.UserService;
+import cn.minbb.edu.storage.FileService;
+import cn.minbb.edu.storage.StorageProperties;
+import cn.minbb.edu.system.Const;
+import cn.minbb.edu.system.UserSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+@Controller
+@RequestMapping("course")
+public class CourseController {
+
+    private UserService userService;
+    private CourseService courseService;
+    private FileService fileService;
+
+    @Autowired
+    public CourseController(UserService userService, CourseService courseService, FileService fileService) {
+        this.userService = userService;
+        this.courseService = courseService;
+        this.fileService = fileService;
+    }
+
+    @GetMapping(value = "create")
+    public ModelAndView courseCreatePage(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("course-create");
+        return modelAndView;
+    }
+
+    @PostMapping(value = "create")
+    public ModelAndView courseCreate(
+            @RequestParam("name") String name,
+            @RequestParam("introduction") String introduction,
+            @RequestParam("remark") String remark,
+            @RequestParam("cover") MultipartFile cover,
+            @RequestParam("video") MultipartFile video,
+            ModelAndView modelAndView, HttpServletRequest request) {
+        User user = UserSession.getUserAuthentication();
+        if (null != user) {
+            int userId = user.getId();
+            String coverFilename = cover.getOriginalFilename();
+            String videoFilename = video.getOriginalFilename();
+            String filenamePrefix = userId + "-" + System.currentTimeMillis();
+            if (null != coverFilename && null != videoFilename) {
+                String coverFilenameStore = filenamePrefix + coverFilename.substring(coverFilename.lastIndexOf("."));
+                String videoFilenameStore = filenamePrefix + videoFilename.substring(videoFilename.lastIndexOf("."));
+                fileService.storeToFolder(cover, coverFilenameStore, StorageProperties.Folder.IMAGES);
+                fileService.storeToFolder(video, videoFilenameStore, StorageProperties.Folder.VIDEOS);
+                Course course = new Course();
+                course.setName(name);
+                course.setIntroduction(introduction);
+                course.setRemark(remark);
+                course.setCover(Const.STORAGE_HOST + "images/" + coverFilenameStore);
+                course.setVideo(Const.STORAGE_HOST + "videos/" + videoFilenameStore);
+                course.setTeacherId(userId);
+                courseService.save(course);
+            }
+        }
+        modelAndView.setViewName("redirect:/course/center");
+        return modelAndView;
+    }
+
+    @GetMapping(value = "square")
+    public ModelAndView courseSquarePage(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("course-square");
+        return modelAndView;
+    }
+
+    @GetMapping(value = "center")
+    public ModelAndView courseCenterPage(ModelAndView modelAndView, HttpServletRequest request) {
+        User user = UserSession.getUserAuthentication();
+        if (null != user) {
+            List<Course> courseList = courseService.findAllByUserId(user.getId());
+            modelAndView.addObject("courseList", courseList);
+            modelAndView.setViewName("course-center");
+        } else {
+            modelAndView.setViewName("redirect:/");
+        }
+        return modelAndView;
+    }
+
+    @GetMapping(value = "player")
+    public ModelAndView coursePlayerPage(
+            @RequestParam(value = "id", required = false) Integer id,
+            ModelAndView modelAndView, HttpServletRequest request) {
+        if (null != id) {
+            Course course = courseService.findOneById(id);
+            modelAndView.addObject("course", course);
+        }
+        modelAndView.setViewName("course-player");
+        return modelAndView;
+    }
+}

+ 47 - 0
src/main/java/cn/minbb/edu/controller/web/FileController.java

@@ -0,0 +1,47 @@
+package cn.minbb.edu.controller.web;
+
+import cn.minbb.edu.storage.FileService;
+import cn.minbb.edu.storage.StorageFileNotFoundException;
+import cn.minbb.edu.storage.StorageProperties;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+@RequestMapping("file")
+public class FileController {
+
+    private FileService fileService;
+
+    public FileController(FileService fileService) {
+        this.fileService = fileService;
+    }
+
+    @GetMapping(value = "/images/{filename:.+}")
+    public ResponseEntity<Resource> loadImages(@PathVariable String filename) {
+        Resource file = fileService.loadAsResourceFromFolder(filename, StorageProperties.Folder.IMAGES);
+        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
+    }
+
+    @GetMapping(value = "/videos/{filename:.+}")
+    public ResponseEntity<Resource> loadVideos(@PathVariable String filename) {
+        Resource file = fileService.loadAsResourceFromFolder(filename, StorageProperties.Folder.VIDEOS);
+        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
+    }
+
+    @GetMapping(value = "/download/videos/{filename:.+}")
+    public ResponseEntity<Resource> downloadVideos(@PathVariable String filename) {
+        Resource file = fileService.loadAsResourceFromFolder(filename, StorageProperties.Folder.VIDEOS);
+        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
+    }
+
+    @ExceptionHandler(value = StorageFileNotFoundException.class)
+    public ResponseEntity handleStorageFileNotFound(StorageFileNotFoundException e) {
+        return ResponseEntity.notFound().build();
+    }
+}

+ 49 - 6
src/main/java/cn/minbb/edu/controller/web/MainController.java

@@ -1,30 +1,73 @@
 package cn.minbb.edu.controller.web;
 
+import cn.minbb.edu.model.Banner;
+import cn.minbb.edu.service.BannerService;
 import cn.minbb.edu.service.UserService;
+import cn.minbb.edu.system.Const;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.servlet.ModelAndView;
 
 import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.List;
 
 @Controller
-@RequestMapping(value = "")
+@RequestMapping("")
 public class MainController {
 
     private UserService userService;
+    private BannerService bannerService;
 
     @Autowired
-    public MainController(UserService userService) {
+    public MainController(UserService userService, BannerService bannerService) {
         this.userService = userService;
+        this.bannerService = bannerService;
     }
 
     @GetMapping(value = "")
-    @ResponseBody
-    public String main() {
-        return "EDUServer Running...";
+    public ModelAndView indexPage(ModelAndView modelAndView, HttpServletRequest request) {
+        List<Banner> bannerList = bannerService.findAll();
+        modelAndView.addObject("bannerList", bannerList);
+        modelAndView.addObject(Const.ACTIVE, "index");
+        modelAndView.setViewName("index");
+        return modelAndView;
+    }
+
+    @GetMapping(value = "about")
+    public ModelAndView aboutPage(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.addObject(Const.ACTIVE, "about");
+        modelAndView.setViewName("index-about");
+        return modelAndView;
+    }
+
+    @GetMapping(value = "download")
+    public ResponseEntity<InputStreamResource> downloadPage(HttpServletRequest request) throws IOException {
+        // String apkPath = "C:\\Evaluation\\download\\apk\\";
+        // FileSystemResource file = new FileSystemResource(apkPath);
+        ClassPathResource file = new ClassPathResource("/static/favicon.ico", this.getClass());
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
+        headers.add("Content-Disposition", String.format("attachment; filename=\"%s\"", file.getFilename()));
+        headers.add("Pragma", "no-cache");
+        headers.add("Expires", "0");
+        return ResponseEntity.ok().headers(headers).contentLength(file.contentLength())
+                .contentType(MediaType.parseMediaType("application/octet-stream"))
+                .contentType(MediaType.parseMediaType("application/x-shockwave-flash"))
+                .body(new InputStreamResource(file.getInputStream()));
+    }
+
+    @GetMapping(value = {"register", "sign-up"})
+    public ModelAndView signUp(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("sign-up");
+        return modelAndView;
     }
 
     @GetMapping(value = {"login", "sign-in"})

+ 28 - 0
src/main/java/cn/minbb/edu/controller/web/UserController.java

@@ -0,0 +1,28 @@
+package cn.minbb.edu.controller.web;
+
+import cn.minbb.edu.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Controller
+@RequestMapping("user")
+public class UserController {
+
+    private UserService userService;
+
+    @Autowired
+    public UserController(UserService userService) {
+        this.userService = userService;
+    }
+
+    @GetMapping(value = "center")
+    public ModelAndView userCenterPage(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("user-center");
+        return modelAndView;
+    }
+}

+ 66 - 1
src/main/java/cn/minbb/edu/data/ResponseResult.java

@@ -1,20 +1,85 @@
 package cn.minbb.edu.data;
 
+import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
 import java.util.Collection;
 
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class ResponseResult<T extends Serializable> implements Serializable {
 
     private Integer code = -1;
 
-    private Boolean status = false;
+    private Boolean success = false;
 
     private String message = "";
 
     private T data;
 
     private Collection<T> dataset;
+
+    public static ResponseResult.Builder ok(Boolean success) {
+        return new ResultBuilder(success);
+    }
+
+    private static class ResultBuilder implements Builder {
+
+        private Integer code;
+        private Boolean success;
+        private String message;
+
+        ResultBuilder(boolean success) {
+            this.success = success;
+        }
+
+        @Override
+        public Builder code(Integer code) {
+            this.code = code;
+            return this;
+        }
+
+        @Override
+        public Builder message(String message) {
+            this.message = message;
+            return this;
+        }
+
+        @Override
+        public <T extends Serializable> ResponseResult<T> data(T data) {
+            return new ResponseResult<>(code, success, message, data, null);
+        }
+
+        @Override
+        public <T extends Serializable> ResponseResult<T> dataset(Collection<T> dataset) {
+            return new ResponseResult<>(code, success, message, null, dataset);
+        }
+
+        @Override
+        public <T extends Serializable> ResponseResult<T> dataAll(T data, Collection<T> dataset) {
+            return new ResponseResult<>(code, success, message, data, dataset);
+        }
+
+        @Override
+        public <T extends Serializable> ResponseResult<T> dataNone() {
+            return new ResponseResult<>(code, success, message, null, null);
+        }
+    }
+
+    public interface Builder {
+        Builder code(Integer code);
+
+        Builder message(String message);
+
+        <T extends Serializable> ResponseResult<T> data(T data);
+
+        <T extends Serializable> ResponseResult<T> dataset(Collection<T> dataset);
+
+        <T extends Serializable> ResponseResult<T> dataAll(T data, Collection<T> dataset);
+
+        <T extends Serializable> ResponseResult<T> dataNone();
+    }
 }

+ 24 - 0
src/main/java/cn/minbb/edu/handler/AccessDenied.java

@@ -0,0 +1,24 @@
+package cn.minbb.edu.handler;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 当一个已授权(或已登陆)的用户请求访问他权限之外的资源时, handle 方法将会被调用
+ */
+public class AccessDenied implements AccessDeniedHandler {
+
+    @Override
+    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
+        httpServletRequest.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new Exception("访问权限不足"));
+        httpServletResponse.setStatus(HttpStatus.OK.value());
+        httpServletResponse.sendRedirect("/sign-in?error=true");
+    }
+}

+ 34 - 0
src/main/java/cn/minbb/edu/handler/AppExceptionHandler.java

@@ -0,0 +1,34 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.data.ResponseResult;
+import cn.minbb.edu.util.RequestUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RestControllerAdvice
+public class AppExceptionHandler {
+
+    private Logger logger = LoggerFactory.getLogger(AppExceptionHandler.class);
+
+    @ExceptionHandler(value = Exception.class)
+    public Object errorHandler(HttpServletRequest request, HttpServletResponse response, Exception e) throws Exception {
+        e.printStackTrace();
+        String error = "全局捕获异常\n" + e.getMessage();
+        logger.error(error);
+        if (RequestUtil.isAjax(request)) {
+            return ResponseResult.ok(false).code(-1).message(e.getMessage()).dataNone();
+        } else {
+            ModelAndView modelAndView = new ModelAndView();
+            modelAndView.addObject("exception", e);
+            modelAndView.addObject("url", request.getRequestURL());
+            modelAndView.setViewName("app-error");
+            return modelAndView;
+        }
+    }
+}

+ 48 - 0
src/main/java/cn/minbb/edu/handler/LoginAuthenticationFailureHandler.java

@@ -0,0 +1,48 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.data.ResponseResult;
+import cn.minbb.edu.util.RequestUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class LoginAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
+
+    private Logger logger = LoggerFactory.getLogger(LoginAuthenticationFailureHandler.class);
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public LoginAuthenticationFailureHandler(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
+        String message = objectMapper.writeValueAsString(exception.getMessage());
+        logger.info("用户登录失败 = {}", message);
+        if (RequestUtil.isAjax(request)) {
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write(objectMapper.writeValueAsString(
+                    ResponseResult.ok(false).code(-1).message(exception.getMessage()).dataNone()));
+        } else {
+            // 异常写入 Session
+            request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
+            response.setStatus(HttpStatus.OK.value());
+            response.sendRedirect("/sign-in?error=true");
+        }
+    }
+}

+ 59 - 0
src/main/java/cn/minbb/edu/handler/LoginAuthenticationSuccessHandler.java

@@ -0,0 +1,59 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.util.RequestUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.SavedRequest;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class LoginAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
+
+    private Logger logger = LoggerFactory.getLogger(LoginAuthenticationSuccessHandler.class);
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public LoginAuthenticationSuccessHandler(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    // Authentication 封装认证信息
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
+        String auth = objectMapper.writeValueAsString(authentication);
+        logger.info("用户登陆成功 = {}", auth);
+        // 登录方式不同,Authentication 不同
+        if (RequestUtil.isAjax(request)) {
+            response.setContentType("application/json;charset=UTF-8");
+            // 把 authentication 对象转成 json 格式字符串通过 response 以 application/json;charset=UTF-8 格式写到响应里面去
+            response.getWriter().write(objectMapper.writeValueAsString(authentication));
+        } else {
+            // 跳转到登录之前访问的页面
+            response.setStatus(HttpStatus.OK.value());
+            SavedRequest savedRequest = (SavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
+            if (savedRequest != null) {
+                response.sendRedirect(savedRequest.getRedirectUrl());
+                request.getSession().removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
+            } else {
+                // 重定向页面需要在登录页设置路径参数
+                String redirect = (String) request.getSession().getAttribute("REDIRECT");
+                if (null != redirect) {
+                    response.sendRedirect(redirect);
+                } else {
+                    response.sendRedirect("/");
+                }
+            }
+        }
+    }
+}

+ 19 - 0
src/main/java/cn/minbb/edu/handler/LoginUrlEntryPoint.java

@@ -0,0 +1,19 @@
+package cn.minbb.edu.handler;
+
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 当一个未授权的用户请求非公有资源时, commence 方法将会被调用
+ */
+public class LoginUrlEntryPoint implements AuthenticationEntryPoint {
+    @Override
+    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
+        httpServletResponse.sendRedirect("/sign-in");
+    }
+}

+ 47 - 0
src/main/java/cn/minbb/edu/handler/LogoutSuccessHandler.java

@@ -0,0 +1,47 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.util.RequestUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * SimpleUrlLogoutSuccessHandler - 重定向到设置的URL地址。默认的地址是/login?logout
+ * HttpStatusReturningLogoutSuccessHandler - REST API场景,允许设置一个返回给客户端的HTTP状态码(默认返回200)来替换重定向到URL这个动作
+ */
+@Component
+public class LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
+
+    private Logger logger = LoggerFactory.getLogger(LogoutSuccessHandler.class);
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public LogoutSuccessHandler(  ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        String auth = objectMapper.writeValueAsString(authentication);
+        logger.info("用户登出成功 = {}", auth);
+        if (RequestUtil.isAjax(request)) {
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write(objectMapper.writeValueAsString(authentication));
+        } else {
+            response.setStatus(HttpStatus.OK.value());
+            response.sendRedirect("/sign-in?logout=true");
+            super.onLogoutSuccess(request, response, authentication);
+        }
+    }
+}

+ 86 - 0
src/main/java/cn/minbb/edu/model/Banner.java

@@ -0,0 +1,86 @@
+package cn.minbb.edu.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Date;
+
+@Entity
+@Table(name = "banner")
+@NoArgsConstructor
+@AllArgsConstructor
+public class Banner implements Serializable {
+
+    @Getter
+    @Setter
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '轮播实体'")
+    private Integer id;
+
+    @Getter
+    @Setter
+    @Column(name = "title", nullable = false, columnDefinition = "VARCHAR(64) COMMENT '标题'")
+    private String title;
+
+    @Getter
+    @Setter
+    @Column(name = "subtitle", nullable = true, columnDefinition = "VARCHAR(255) COMMENT '子标题'")
+    private String subtitle;
+
+    @Getter
+    @Setter
+    @Column(name = "action", nullable = false, columnDefinition = "VARCHAR(16) COMMENT '动作提示'")
+    private String action;
+
+    @Getter
+    @Setter
+    @Column(name = "cover", columnDefinition = "VARCHAR(255) COMMENT '封面'")
+    private String cover;
+
+    @Getter
+    @Setter
+    @Column(name = "path", columnDefinition = "VARCHAR(255) COMMENT '路径'")
+    private String path;
+
+    @Getter
+    @Setter
+    @Column(name = "remark", columnDefinition = "VARCHAR(255) COMMENT '备注'")
+    private String remark;
+
+    @Getter
+    @Setter
+    @Column(name = "user_id", nullable = false, columnDefinition = "INT COMMENT '关联用户'")
+    private Integer userId;
+
+    @Getter
+    @Setter
+    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '已启用'")
+    private Boolean isEnabled;
+
+    @Getter
+    @Setter
+    @Column(name = "created_at", columnDefinition = "DATETIME COMMENT '创建时间'")
+    @CreationTimestamp
+    private Date createdAt;
+
+    @Getter
+    @Setter
+    @Column(name = "updated_at", columnDefinition = "DATETIME COMMENT '更新时间'")
+    @UpdateTimestamp
+    private Date updatedAt;
+
+    @Version
+    @Column(name = "version", columnDefinition = "INTEGER COMMENT '版本号'")
+    public Integer version;
+
+    public Banner(Boolean isEnabled) {
+        this.isEnabled = isEnabled;
+    }
+}

+ 86 - 0
src/main/java/cn/minbb/edu/model/Course.java

@@ -0,0 +1,86 @@
+package cn.minbb.edu.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Date;
+
+@Entity
+@Table(name = "course")
+@NoArgsConstructor
+@AllArgsConstructor
+public class Course implements Serializable {
+
+    @Getter
+    @Setter
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '课程实体'")
+    private Integer id;
+
+    @Getter
+    @Setter
+    @Column(name = "name", nullable = false, columnDefinition = "VARCHAR(64) COMMENT '名称'")
+    private String name;
+
+    @Getter
+    @Setter
+    @Column(name = "introduction", nullable = false, columnDefinition = "VARCHAR(255) COMMENT '介绍'")
+    private String introduction;
+
+    @Getter
+    @Setter
+    @Column(name = "cover", columnDefinition = "VARCHAR(255) COMMENT '封面'")
+    private String cover;
+
+    @Getter
+    @Setter
+    @Column(name = "video", columnDefinition = "VARCHAR(255) COMMENT '视频'")
+    private String video;
+
+    @Getter
+    @Setter
+    @Column(name = "remark", columnDefinition = "VARCHAR(255) COMMENT '备注'")
+    private String remark;
+
+    @Getter
+    @Setter
+    @Column(name = "teacher_id", nullable = false, columnDefinition = "INT COMMENT '关联用户(教师)'")
+    private Integer teacherId;
+
+    @Getter
+    @Setter
+    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '已启用'")
+    private Boolean isEnabled;
+
+    @Getter
+    @Setter
+    @Column(name = "created_at", columnDefinition = "DATETIME COMMENT '创建时间'")
+    @CreationTimestamp
+    private Date createdAt;
+
+    @Getter
+    @Setter
+    @Column(name = "updated_at", columnDefinition = "DATETIME COMMENT '更新时间'")
+    @UpdateTimestamp
+    private Date updatedAt;
+
+    @Version
+    @Column(name = "version", columnDefinition = "INTEGER COMMENT '版本号'")
+    public Integer version;
+
+    @Getter
+    @Setter
+    @Transient
+    private User voTeacher;
+
+    public Course(Boolean isEnabled) {
+        this.isEnabled = isEnabled;
+    }
+}

+ 62 - 0
src/main/java/cn/minbb/edu/model/Homework.java

@@ -0,0 +1,62 @@
+package cn.minbb.edu.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Date;
+
+@Entity
+@Table(name = "homework")
+@NoArgsConstructor
+@AllArgsConstructor
+public class Homework implements Serializable {
+
+    @Getter
+    @Setter
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '作业实体'")
+    private Integer id;
+
+    @Getter
+    @Setter
+    @Column(name = "name", nullable = false, columnDefinition = "VARCHAR(64) COMMENT '名称'")
+    private String name;
+
+    @Getter
+    @Setter
+    @Column(name = "introduction", nullable = false, columnDefinition = "VARCHAR(255) COMMENT '介绍'")
+    private String introduction;
+
+    @Getter
+    @Setter
+    @Column(name = "remark", columnDefinition = "VARCHAR(255) COMMENT '备注'")
+    private String remark;
+
+    @Getter
+    @Setter
+    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '已启用'")
+    private Boolean isEnabled;
+
+    @Getter
+    @Setter
+    @Column(name = "created_at", columnDefinition = "DATETIME COMMENT '创建时间'")
+    @CreationTimestamp
+    private Date createdAt;
+
+    @Getter
+    @Setter
+    @Column(name = "updated_at", columnDefinition = "DATETIME COMMENT '更新时间'")
+    @UpdateTimestamp
+    private Date updatedAt;
+
+    @Version
+    @Column(name = "version", columnDefinition = "INTEGER COMMENT '版本号'")
+    public Integer version;
+}

+ 72 - 0
src/main/java/cn/minbb/edu/model/Study.java

@@ -0,0 +1,72 @@
+package cn.minbb.edu.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Date;
+
+@Entity
+@Table(name = "study")
+@NoArgsConstructor
+@AllArgsConstructor
+public class Study implements Serializable {
+
+    @Getter
+    @Setter
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '用户课程实体'")
+    private Integer id;
+
+    @Getter
+    @Setter
+    @Column(name = "user_id", nullable = false, columnDefinition = "INT COMMENT '关联用户'")
+    private Integer userId;
+
+    @Getter
+    @Setter
+    @Column(name = "course_id", nullable = false, columnDefinition = "INT COMMENT '关联课程'")
+    private Integer courseId;
+
+    @Getter
+    @Setter
+    @Column(name = "remark", columnDefinition = "VARCHAR(255) COMMENT '备注'")
+    private String remark;
+
+    @Getter
+    @Setter
+    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '已启用'")
+    private Boolean isEnabled;
+
+    @Getter
+    @Setter
+    @Column(name = "created_at", columnDefinition = "DATETIME COMMENT '创建时间'")
+    @CreationTimestamp
+    private Date createdAt;
+
+    @Getter
+    @Setter
+    @Column(name = "updated_at", columnDefinition = "DATETIME COMMENT '更新时间'")
+    @UpdateTimestamp
+    private Date updatedAt;
+
+    @Version
+    @Column(name = "version", columnDefinition = "INTEGER COMMENT '版本号'")
+    public Integer version;
+
+    @Getter
+    @Setter
+    @Transient
+    private User voUser;
+
+    @Getter
+    @Setter
+    @Transient
+    private Course voCourse;
+}

+ 124 - 5
src/main/java/cn/minbb/edu/model/User.java

@@ -4,22 +4,32 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
 
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;
-import java.io.Serializable;
+import java.util.*;
 
 @Entity
-@Table(name = "user")
+@Table(name = "user",
+        uniqueConstraints = {@UniqueConstraint(name = "unique_username", columnNames = {"username"})},
+        indexes = {
+                @Index(name = "index_id", columnList = "id"),
+                @Index(name = "index_username", columnList = "username")
+        })
 @NoArgsConstructor
 @AllArgsConstructor
-public class User implements Serializable {
+public class User implements UserDetails {
 
     @Getter
     @Setter
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
-    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '用户实体ID'")
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '用户实体'")
     private Integer id;
 
     @Getter
@@ -37,6 +47,31 @@ public class User implements Serializable {
     @Column(name = "name", columnDefinition = "VARCHAR(64) COMMENT '姓名'")
     private String name;
 
+    @Getter
+    @Setter
+    @Column(name = "nickname", columnDefinition = "VARCHAR(32) COMMENT '昵称'")
+    private String nickname;
+
+    @Getter
+    @Setter
+    @Column(name = "signature", columnDefinition = "VARCHAR(255) COMMENT '签名'")
+    private String signature;
+
+    @Getter
+    @Setter
+    @Column(name = "email", columnDefinition = "VARCHAR(255) COMMENT '电子邮箱'")
+    private String email;
+
+    @Getter
+    @Setter
+    @Column(name = "phone", columnDefinition = "VARCHAR(16) COMMENT '电话'")
+    private String phone;
+
+    @Getter
+    @Setter
+    @Column(name = "qq", columnDefinition = "VARCHAR(16) COMMENT 'QQ'")
+    private String qq;
+
     @Getter
     @Setter
     @Column(name = "avatar", columnDefinition = "VARCHAR(255) COMMENT '头像'")
@@ -47,8 +82,92 @@ public class User implements Serializable {
     @Column(name = "school", columnDefinition = "VARCHAR(64) COMMENT '学校'")
     private String school;
 
-    public User(@NotNull String username, @NotNull String password) {
+    @Getter
+    @Setter
+    @ManyToMany(cascade = {CascadeType.REFRESH}, fetch = FetchType.EAGER)
+    @JoinTable(name = "user_user_role",
+            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
+            inverseJoinColumns = {@JoinColumn(name = "user_role_id", referencedColumnName = "id")}
+    )
+    private Set<UserRole> userRoleSet;
+
+    @Getter
+    @Setter
+    @Column(name = "is_account_non_expired", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户未过期'")
+    private Boolean isAccountNonExpired;
+
+    @Getter
+    @Setter
+    @Column(name = "is_account_non_locked", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户未锁定'")
+    private Boolean isAccountNonLocked;
+
+    @Getter
+    @Setter
+    @Column(name = "is_credentials_non_expired", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '凭据未过期'")
+    private Boolean isCredentialsNonExpired;
+
+    @Getter
+    @Setter
+    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户已启用'")
+    private Boolean isEnabled;
+
+    @Getter
+    @Setter
+    @Column(name = "created_at", columnDefinition = "DATETIME COMMENT '创建时间'")
+    @CreationTimestamp
+    private Date createdAt;
+
+    @Getter
+    @Setter
+    @Column(name = "updated_at", columnDefinition = "DATETIME COMMENT '更新时间'")
+    @UpdateTimestamp
+    private Date updatedAt;
+
+    @Version
+    @Column(name = "version", columnDefinition = "INTEGER COMMENT '版本号'")
+    public Integer version;
+
+    public User(@NotNull String name, @NotNull String username, @NotNull String password) {
+        this.name = name;
         this.username = username;
         this.password = password;
     }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
+        for (UserRole userRole : this.getUserRoleSet()) {
+            grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + userRole.getRole().name()));
+        }
+        return grantedAuthorityList;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return this.isAccountNonExpired;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return this.isAccountNonLocked;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return this.isCredentialsNonExpired;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return this.isEnabled;
+    }
+
+    public boolean hasRole(UserRole.Role role) {
+        for (UserRole userRole : getUserRoleSet()) {
+            if (userRole.getRole().equals(role)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

+ 57 - 0
src/main/java/cn/minbb/edu/model/UserRole.java

@@ -0,0 +1,57 @@
+package cn.minbb.edu.model;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.persistence.*;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "user_role",
+        uniqueConstraints = {@UniqueConstraint(name = "unique_role", columnNames = {"role"})},
+        indexes = {@Index(name = "index_role", columnList = "role")})
+@NoArgsConstructor
+public class UserRole implements Serializable {
+
+    @Getter
+    @Setter
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '用户角色'")
+    private Integer id;
+
+    @Getter
+    @Setter
+    @Enumerated(EnumType.STRING)
+    @Column(name = "role", nullable = false, columnDefinition = "VARCHAR(32) COMMENT '角色名称'")
+    private Role role;
+
+    @Getter
+    @Setter
+    @Column(name = "description", columnDefinition = "VARCHAR(255) COMMENT '角色描述'")
+    private String description;
+
+    public UserRole(Role role) {
+        this.role = role;
+    }
+
+    public UserRole(Role role, String description) {
+        this.role = role;
+        this.description = description;
+    }
+
+    @Getter
+    public enum Role {
+        USER("用户"),
+        STUDENT("学生"),
+        TEACHER("教师"),
+        ADMIN("管理员");
+
+        String description;
+
+        Role(String description) {
+            this.description = description;
+        }
+    }
+}

+ 11 - 0
src/main/java/cn/minbb/edu/model/repository/BannerRepository.java

@@ -0,0 +1,11 @@
+package cn.minbb.edu.model.repository;
+
+import cn.minbb.edu.model.Banner;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface BannerRepository extends JpaRepository<Banner, Integer> {
+    List<Banner> findAllByIsEnabledTrue(Sort sort);
+}

+ 7 - 0
src/main/java/cn/minbb/edu/model/repository/CourseRepository.java

@@ -0,0 +1,7 @@
+package cn.minbb.edu.model.repository;
+
+import cn.minbb.edu.model.Course;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CourseRepository extends JpaRepository<Course, Integer> {
+}

+ 7 - 0
src/main/java/cn/minbb/edu/model/repository/HomeworkRepository.java

@@ -0,0 +1,7 @@
+package cn.minbb.edu.model.repository;
+
+import cn.minbb.edu.model.Homework;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface HomeworkRepository extends JpaRepository<Homework, Integer> {
+}

+ 7 - 0
src/main/java/cn/minbb/edu/model/repository/StudyRepository.java

@@ -0,0 +1,7 @@
+package cn.minbb.edu.model.repository;
+
+import cn.minbb.edu.model.Study;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StudyRepository extends JpaRepository<Study, Integer> {
+}

+ 14 - 0
src/main/java/cn/minbb/edu/model/repository/UserRoleRepository.java

@@ -0,0 +1,14 @@
+package cn.minbb.edu.model.repository;
+
+import cn.minbb.edu.model.UserRole;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Set;
+
+@Repository
+public interface UserRoleRepository extends JpaRepository<UserRole, Integer> {
+    UserRole findOneByRole(UserRole.Role role);
+
+    Set<UserRole> findAllByRoleIn(Set<UserRole.Role> roleSet);
+}

+ 11 - 0
src/main/java/cn/minbb/edu/service/BannerService.java

@@ -0,0 +1,11 @@
+package cn.minbb.edu.service;
+
+import cn.minbb.edu.model.Banner;
+
+import java.util.List;
+
+public interface BannerService {
+    Banner save(Banner banner);
+
+    List<Banner> findAll();
+}

+ 15 - 0
src/main/java/cn/minbb/edu/service/CourseService.java

@@ -0,0 +1,15 @@
+package cn.minbb.edu.service;
+
+import cn.minbb.edu.model.Course;
+
+import java.util.List;
+
+public interface CourseService {
+    Course save(Course course);
+
+    Course findOneById(Integer id);
+
+    List<Course> findAll();
+
+    List<Course> findAllByUserId(Integer userId);
+}

+ 7 - 0
src/main/java/cn/minbb/edu/service/HomeworkService.java

@@ -0,0 +1,7 @@
+package cn.minbb.edu.service;
+
+import cn.minbb.edu.model.Homework;
+
+public interface HomeworkService {
+    Homework save(Homework homework);
+}

+ 7 - 0
src/main/java/cn/minbb/edu/service/StudyService.java

@@ -0,0 +1,7 @@
+package cn.minbb.edu.service;
+
+import cn.minbb.edu.model.Study;
+
+public interface StudyService {
+    Study save(Study study);
+}

+ 16 - 0
src/main/java/cn/minbb/edu/service/UserRoleService.java

@@ -0,0 +1,16 @@
+package cn.minbb.edu.service;
+
+
+import cn.minbb.edu.model.UserRole;
+
+import java.util.Set;
+
+public interface UserRoleService {
+    UserRole saveOne(UserRole userRole);
+
+    UserRole findOneByRole(UserRole.Role role);
+
+    Set<UserRole> findAll();
+
+    Set<UserRole> findAllByRoleSet(Set<UserRole.Role> roleSet);
+}

+ 4 - 1
src/main/java/cn/minbb/edu/service/UserService.java

@@ -1,9 +1,12 @@
 package cn.minbb.edu.service;
 
 import cn.minbb.edu.model.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
 
-public interface UserService {
+public interface UserService extends UserDetailsService {
     User save(User user);
 
+    User findUserById(Integer id);
+
     User findUserByUsername(String username);
 }

+ 32 - 0
src/main/java/cn/minbb/edu/service/impl/BannerServiceImpl.java

@@ -0,0 +1,32 @@
+package cn.minbb.edu.service.impl;
+
+import cn.minbb.edu.model.Banner;
+import cn.minbb.edu.model.repository.BannerRepository;
+import cn.minbb.edu.service.BannerService;
+import cn.minbb.edu.util.SortTools;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class BannerServiceImpl implements BannerService {
+
+    private BannerRepository bannerRepository;
+
+    @Autowired
+    public BannerServiceImpl(BannerRepository bannerRepository) {
+        this.bannerRepository = bannerRepository;
+    }
+
+    @Override
+    public Banner save(Banner banner) {
+        banner.setIsEnabled(true);
+        return bannerRepository.save(banner);
+    }
+
+    @Override
+    public List<Banner> findAll() {
+        return bannerRepository.findAllByIsEnabledTrue(SortTools.createdAtDown());
+    }
+}

+ 48 - 0
src/main/java/cn/minbb/edu/service/impl/CourseServiceImpl.java

@@ -0,0 +1,48 @@
+package cn.minbb.edu.service.impl;
+
+import cn.minbb.edu.model.Course;
+import cn.minbb.edu.model.repository.CourseRepository;
+import cn.minbb.edu.service.CourseService;
+import cn.minbb.edu.util.SortTools;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Example;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class CourseServiceImpl implements CourseService {
+
+    private CourseRepository courseRepository;
+
+    @Autowired
+    public CourseServiceImpl(CourseRepository courseRepository) {
+        this.courseRepository = courseRepository;
+    }
+
+    @Override
+    public Course save(Course course) {
+        course.setIsEnabled(true);
+        return courseRepository.save(course);
+    }
+
+    @Override
+    public Course findOneById(Integer id) {
+        Course course = new Course(true);
+        course.setId(id);
+        Example<Course> example = Example.of(course);
+        return courseRepository.findOne(example).orElse(null);
+    }
+
+    @Override
+    public List<Course> findAll() {
+        return courseRepository.findAll(SortTools.createdAtDown());
+    }
+
+    @Override
+    public List<Course> findAllByUserId(Integer userId) {
+        Course course = new Course(true);
+        course.setTeacherId(userId);
+        return courseRepository.findAll(Example.of(course), SortTools.createdAtDown());
+    }
+}

+ 24 - 0
src/main/java/cn/minbb/edu/service/impl/HomeworkServiceImpl.java

@@ -0,0 +1,24 @@
+package cn.minbb.edu.service.impl;
+
+import cn.minbb.edu.model.Homework;
+import cn.minbb.edu.model.repository.HomeworkRepository;
+import cn.minbb.edu.service.HomeworkService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class HomeworkServiceImpl implements HomeworkService {
+
+    private HomeworkRepository homeworkRepository;
+
+    @Autowired
+    public HomeworkServiceImpl(HomeworkRepository homeworkRepository) {
+        this.homeworkRepository = homeworkRepository;
+    }
+
+    @Override
+    public Homework save(Homework homework) {
+        homework.setIsEnabled(true);
+        return homeworkRepository.save(homework);
+    }
+}

+ 24 - 0
src/main/java/cn/minbb/edu/service/impl/StudyServiceImpl.java

@@ -0,0 +1,24 @@
+package cn.minbb.edu.service.impl;
+
+import cn.minbb.edu.model.Study;
+import cn.minbb.edu.model.repository.StudyRepository;
+import cn.minbb.edu.service.StudyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class StudyServiceImpl implements StudyService {
+
+    private StudyRepository studyRepository;
+
+    @Autowired
+    public StudyServiceImpl(StudyRepository studyRepository) {
+        this.studyRepository = studyRepository;
+    }
+
+    @Override
+    public Study save(Study study) {
+        study.setIsEnabled(true);
+        return studyRepository.save(study);
+    }
+}

+ 42 - 0
src/main/java/cn/minbb/edu/service/impl/UserRoleServiceImpl.java

@@ -0,0 +1,42 @@
+package cn.minbb.edu.service.impl;
+
+import cn.minbb.edu.model.UserRole;
+import cn.minbb.edu.model.repository.UserRoleRepository;
+import cn.minbb.edu.service.UserRoleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Service
+public class UserRoleServiceImpl implements UserRoleService {
+
+    private UserRoleRepository userRoleRepository;
+
+    @Autowired
+    public UserRoleServiceImpl(UserRoleRepository userRoleRepository) {
+        this.userRoleRepository = userRoleRepository;
+    }
+
+    @Override
+    public UserRole saveOne(UserRole userRole) {
+        return userRoleRepository.save(userRole);
+    }
+
+    @Override
+    public UserRole findOneByRole(UserRole.Role role) {
+        return null == role ? null : userRoleRepository.findOneByRole(role);
+    }
+
+    @Override
+    public Set<UserRole> findAll() {
+        return new HashSet<>(userRoleRepository.findAll());
+    }
+
+    @Override
+    public Set<UserRole> findAllByRoleSet(Set<UserRole.Role> roleSet) {
+        if (roleSet.isEmpty()) return new HashSet<>();
+        return userRoleRepository.findAllByRoleIn(roleSet);
+    }
+}

+ 51 - 0
src/main/java/cn/minbb/edu/service/impl/UserServiceImpl.java

@@ -1,11 +1,25 @@
 package cn.minbb.edu.service.impl;
 
 import cn.minbb.edu.model.User;
+import cn.minbb.edu.model.UserRole;
 import cn.minbb.edu.model.repository.UserRepository;
 import cn.minbb.edu.service.UserService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Example;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Service
 public class UserServiceImpl implements UserService {
 
@@ -18,11 +32,48 @@ public class UserServiceImpl implements UserService {
 
     @Override
     public User save(User user) {
+        user.setIsAccountNonExpired(true);
+        user.setIsAccountNonLocked(true);
+        user.setIsCredentialsNonExpired(true);
+        user.setIsEnabled(true);
         return userRepository.save(user);
     }
 
+    @Override
+    public User findUserById(Integer id) {
+        User user = new User();
+        user.setId(id);
+        return userRepository.findOne(Example.of(user)).orElse(null);
+    }
+
     @Override
     public User findUserByUsername(String username) {
         return userRepository.findUserByUsername(username);
     }
+
+    @Override
+    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
+        User user = findUserByUsername(s);
+        if (user == null) {
+            throw new BadCredentialsException("用户不存在");
+        } else {
+            if (!user.isAccountNonExpired()) {
+                throw new BadCredentialsException("账户已过期");
+            } else if (!user.isAccountNonLocked()) {
+                throw new LockedException("账户被锁定");
+            } else if (!user.isCredentialsNonExpired()) {
+                throw new CredentialsExpiredException("凭据已过期");
+            } else if (!user.isEnabled()) {
+                throw new DisabledException("账户被禁用");
+            } else {
+                List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
+                Set<UserRole> userRoleSet = user.getUserRoleSet();
+                for (UserRole userRole : userRoleSet) {
+                    grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + userRole.getRole().name()));
+                }
+                new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorityList);
+            }
+        }
+        return user;
+    }
 }

+ 22 - 0
src/main/java/cn/minbb/edu/storage/FileService.java

@@ -0,0 +1,22 @@
+package cn.minbb.edu.storage;
+
+import org.springframework.core.io.Resource;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+public interface FileService {
+
+    void storeToFolder(MultipartFile file, String filename, StorageProperties.Folder folder);
+
+    void deleteFromFolder(String filename, StorageProperties.Folder folder);
+
+    void deleteAll();
+
+    Path loadFromFolder(String filename, StorageProperties.Folder folder);
+
+    Stream<Path> loadAllFromFolder(StorageProperties.Folder folder);
+
+    Resource loadAsResourceFromFolder(String filename, StorageProperties.Folder folder);
+}

+ 80 - 0
src/main/java/cn/minbb/edu/storage/FileServiceImpl.java

@@ -0,0 +1,80 @@
+package cn.minbb.edu.storage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.util.FileSystemUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+@Service
+public class FileServiceImpl implements FileService {
+
+    private Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);
+
+    @Override
+    public void storeToFolder(MultipartFile file, String filename, StorageProperties.Folder folder) {
+        try {
+            if (file.isEmpty()) {
+                throw new StorageException("Failed to store empty file " + file.getOriginalFilename());
+            }
+            Files.copy(file.getInputStream(), Paths.get(folder.getPath()).resolve(filename));
+        } catch (IOException e) {
+            throw new StorageException("Failed to store file " + file.getOriginalFilename(), e);
+        }
+    }
+
+    @Override
+    public void deleteFromFolder(String filename, StorageProperties.Folder folder) {
+        try {
+            Files.delete(loadFromFolder(filename, folder));
+        } catch (IOException e) {
+            throw new StorageException("Failed to delete file " + filename, e);
+        }
+    }
+
+    @Override
+    public void deleteAll() {
+        FileSystemUtils.deleteRecursively(Paths.get(StorageProperties.Folder.LOCATION.getPath()).toFile());
+    }
+
+    @Override
+    public Path loadFromFolder(String filename, StorageProperties.Folder folder) {
+        return Paths.get(folder.getPath()).resolve(filename);
+    }
+
+    @Override
+    public Stream<Path> loadAllFromFolder(StorageProperties.Folder folder) {
+        Path rootLocation = Paths.get(folder.getPath());
+        try {
+            return Files.walk(rootLocation, 1)
+                    .filter(path -> !path.equals(rootLocation))
+                    .map(path -> rootLocation.relativize(path));
+        } catch (IOException e) {
+            throw new StorageException("Failed to read stored files", e);
+        }
+    }
+
+    @Override
+    public Resource loadAsResourceFromFolder(String filename, StorageProperties.Folder folder) {
+        try {
+            Path file = loadFromFolder(filename, folder);
+            Resource resource = new UrlResource(file.toUri());
+            if (resource.exists() || resource.isReadable()) {
+                return resource;
+            } else {
+                throw new StorageFileNotFoundException("Could not read file: " + filename);
+            }
+        } catch (MalformedURLException e) {
+            throw new StorageFileNotFoundException("Could not read file: " + filename, e);
+        }
+    }
+}

+ 12 - 0
src/main/java/cn/minbb/edu/storage/StorageException.java

@@ -0,0 +1,12 @@
+package cn.minbb.edu.storage;
+
+public class StorageException extends RuntimeException {
+
+    public StorageException(String message) {
+        super(message);
+    }
+
+    public StorageException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 12 - 0
src/main/java/cn/minbb/edu/storage/StorageFileNotFoundException.java

@@ -0,0 +1,12 @@
+package cn.minbb.edu.storage;
+
+public class StorageFileNotFoundException extends StorageException {
+
+    public StorageFileNotFoundException(String message) {
+        super(message);
+    }
+
+    public StorageFileNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 57 - 0
src/main/java/cn/minbb/edu/storage/StorageProperties.java

@@ -0,0 +1,57 @@
+package cn.minbb.edu.storage;
+
+import lombok.Getter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("storage")
+public class StorageProperties {
+
+//    private static final String ROOT = "C:\\EDU";
+//
+//    @Getter
+//    @Setter
+//    // 存储文件的文件夹位置
+//    private String location = ROOT + "\\";
+//
+//    @Getter
+//    @Setter
+//    // 图片目录
+//    private String images = ROOT + "\\images\\";
+//
+//    @Getter
+//    @Setter
+//    // 视频目录
+//    private String videos = ROOT + "\\videos\\";
+//
+//    @Getter
+//    @Setter
+//    // 用户头像目录
+//    private String avatars = ROOT + "\\avatars\\";
+//
+//    @Getter
+//    @Setter
+//    // 文件下载目录
+//    private String download = ROOT + "\\download\\";
+
+    public enum Folder {
+        // 根目录
+        ROOT("C:\\EDU"),
+        // 存储文件的文件夹位置
+        LOCATION("C:\\EDU\\"),
+        // 图片目录
+        IMAGES("C:\\EDU\\IMAGES\\"),
+        // 视频目录
+        VIDEOS("C:\\EDU\\VIDEOS\\"),
+        // 用户头像目录
+        AVATARS("C:\\EDU\\AVATARS\\"),
+        // 文件下载目录
+        DOWNLOAD("C:\\EDU\\DOWNLOAD\\");
+
+        @Getter
+        private String path;
+
+        Folder(String path) {
+            this.path = path;
+        }
+    }
+}

+ 24 - 0
src/main/java/cn/minbb/edu/storage/StorageService.java

@@ -0,0 +1,24 @@
+package cn.minbb.edu.storage;
+
+import org.springframework.core.io.Resource;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+public interface StorageService {
+
+    void init();
+
+    void store(MultipartFile file);
+
+    void delete(String fileName);
+
+    void deleteAll();
+
+    Path load(String fileName);
+
+    Stream<Path> loadAll();
+
+    Resource loadAsResource(String fileName);
+}

+ 184 - 0
src/main/java/cn/minbb/edu/storage/StorageServiceFileSystem.java

@@ -0,0 +1,184 @@
+package cn.minbb.edu.storage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.util.FileSystemUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+@Service
+public class StorageServiceFileSystem implements StorageService {
+
+    private Logger logger = LoggerFactory.getLogger(StorageServiceFileSystem.class);
+
+    private Path rootLocation;
+
+    private StorageProperties storageProperties;
+
+    @Autowired
+    public StorageServiceFileSystem(StorageProperties properties) {
+        this.storageProperties = properties;
+        // this.rootLocation = Paths.get(properties.getLocation());
+        this.rootLocation = Paths.get(StorageProperties.Folder.LOCATION.getPath());
+    }
+
+    /**
+     * 初始化存储空间(各文件夹)
+     */
+    @Override
+    public void init() {
+        try {
+            File rootDir = rootLocation.toFile();
+            // File imageDir = new File(storageProperties.getImages());
+            // File videoDir = new File(storageProperties.getVideos());
+            // File avatarDir = new File(storageProperties.getAvatars());
+            // File downloadDir = new File(storageProperties.getDownload());
+            File imageDir = new File(StorageProperties.Folder.IMAGES.getPath());
+            File videoDir = new File(StorageProperties.Folder.VIDEOS.getPath());
+            File avatarDir = new File(StorageProperties.Folder.AVATARS.getPath());
+            File downloadDir = new File(StorageProperties.Folder.DOWNLOAD.getPath());
+            List<File> dirList = new ArrayList<>(4);
+            dirList.add(imageDir);
+            dirList.add(videoDir);
+            dirList.add(avatarDir);
+            dirList.add(downloadDir);
+            if (!rootDir.exists()) {
+                Files.createDirectory(rootLocation);
+            }
+            for (File dir : dirList) {
+                if (!dir.exists()) {
+                    Files.createDirectory(dir.toPath());
+                }
+            }
+        } catch (IOException e) {
+            throw new StorageException("初始化存储空间失败!", e);
+        }
+    }
+
+    /**
+     * 存储文件
+     *
+     * @param file MultipartFile
+     */
+    @Override
+    public void store(MultipartFile file) {
+        try {
+            if (file.isEmpty()) {
+                throw new StorageException("Failed to store empty file " + file.getOriginalFilename());
+            }
+            Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename()));
+        } catch (IOException e) {
+            throw new StorageException("Failed to store file " + file.getOriginalFilename(), e);
+        }
+    }
+
+    /**
+     * 删除单个文件
+     *
+     * @param filename 文件名
+     */
+    @Override
+    public void delete(String filename) {
+        try {
+            Files.delete(load(filename));
+        } catch (IOException e) {
+            throw new StorageException("Failed to delete file " + filename, e);
+        }
+    }
+
+    /**
+     * 删除存储空间全部文件(递归删除)
+     */
+    @Override
+    public void deleteAll() {
+        FileSystemUtils.deleteRecursively(rootLocation.toFile());
+    }
+
+    /**
+     * 通过文件名加载路径
+     *
+     * @param filename 文件名
+     * @return Path
+     */
+    @Override
+    public Path load(String filename) {
+        return rootLocation.resolve(filename);
+    }
+
+    /**
+     * 加载全部文件(通过遍历文件夹而非数据库文件名)
+     *
+     * @return Stream<Path>
+     */
+    @Override
+    public Stream<Path> loadAll() {
+        try {
+            return Files.walk(this.rootLocation, 1)
+                    .filter(path -> !path.equals(this.rootLocation))
+                    .map(path -> rootLocation.relativize(path));
+        } catch (IOException e) {
+            throw new StorageException("Failed to read stored files", e);
+        }
+    }
+
+    /**
+     * 通过文件名加载资源
+     *
+     * @param filename 文件名
+     * @return 文件资源
+     */
+    @Override
+    public Resource loadAsResource(String filename) {
+        try {
+            Path file = load(filename);
+            Resource resource = new UrlResource(file.toUri());
+            if (resource.exists() || resource.isReadable()) {
+                return resource;
+            } else {
+                throw new StorageFileNotFoundException("Could not read file: " + filename);
+            }
+        } catch (MalformedURLException e) {
+            throw new StorageFileNotFoundException("Could not read file: " + filename, e);
+        }
+    }
+
+    /**
+     * 存储文件到文件夹
+     *
+     * @param file 文件
+     * @param path 存储路径
+     * @return 存储成功返回文件名 失败返回空
+     */
+    public String storeToDictionary(MultipartFile file, Path path) {
+        String originalFilename = file.getOriginalFilename();
+        if (null == originalFilename) {
+            logger.error("文件名不能为空:{}", file);
+            return null;
+        }
+        String filename = System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."));
+        try {
+            if (file.isEmpty()) {
+                logger.error("文件不能为空:{}", originalFilename);
+                return null;
+            }
+            Files.copy(file.getInputStream(), path.resolve(filename));
+        } catch (IOException e) {
+            logger.error("存储失败,原因为:{}", e);
+            return null;
+        }
+        return filename;
+    }
+}

+ 10 - 0
src/main/java/cn/minbb/edu/system/Const.java

@@ -0,0 +1,10 @@
+package cn.minbb.edu.system;
+
+public class Const {
+
+    private Const() {
+    }
+
+    public static final String ACTIVE = "ACTIVE";
+    public static final String STORAGE_HOST = "http://123.207.151.92:8080/file/";
+}

+ 74 - 0
src/main/java/cn/minbb/edu/system/UserSession.java

@@ -0,0 +1,74 @@
+package cn.minbb.edu.system;
+
+import cn.minbb.edu.model.User;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+public class UserSession {
+
+    private UserSession() {
+    }
+
+    /**
+     * 用户认证 - 获取当前会话中已登录的用户
+     *
+     * @return 已登录用户
+     */
+    public static User getUserAuthentication() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (null != authentication) {
+            Object object = authentication.getPrincipal();
+            if (object instanceof User) {
+                return (User) object;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 更新用户认证信息,此操作会导致原会话失效,即需要用户重新登录
+     *
+     * @param request HttpServletRequest
+     * @param user    用户
+     */
+    public static void updateUserAuthentication(HttpServletRequest request, User user) {
+        // 1.从 HttpServletRequest 中获取 SecurityContextImpl 对象
+        SecurityContextImpl securityContextImpl = (SecurityContextImpl) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
+        // 2.从 SecurityContextImpl 中获取 Authentication 对象
+        Authentication authentication = securityContextImpl.getAuthentication();
+        // 3.初始化 UsernamePasswordAuthenticationToken 实例,参数 user 就是我们要更新的用户信息
+        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, authentication.getCredentials());
+        auth.setDetails(authentication.getDetails());
+        // 4.重新设置 SecurityContextImpl 对象的 Authentication
+        securityContextImpl.setAuthentication(auth);
+        // SecurityContextHolder.getContext().setAuthentication(auth);
+    }
+
+    /**
+     * 更新用户会话信息,此操作不会导致原会话失效,不需要用户重新登录
+     *
+     * @param request HttpServletRequest
+     * @param user    包含认证凭证(即密码)和 Authorities 的用户
+     */
+    public static void updateUserSession(HttpServletRequest request, User user) {
+        // 根据 userDetails 构建新的 Authentication
+        // 这里使用 PreAuthenticatedAuthenticationToken
+        // 也可以用其他 token, 如 UsernamePasswordAuthenticationToken               
+        PreAuthenticatedAuthenticationToken authentication =
+                new PreAuthenticatedAuthenticationToken(user, user.getPassword(), user.getAuthorities());
+        // 设置 authentication 中 details
+        authentication.setDetails(new WebAuthenticationDetails(request));
+        // 存放 authentication 到 SecurityContextHolder
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        HttpSession session = request.getSession(true);
+        // 在 session 中存放 security context,方便同一个 session 中控制用户的其他操作
+        session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
+    }
+}

+ 74 - 0
src/main/java/cn/minbb/edu/task/InitDataRunner.java

@@ -0,0 +1,74 @@
+package cn.minbb.edu.task;
+
+import cn.minbb.edu.model.Banner;
+import cn.minbb.edu.model.UserRole;
+import cn.minbb.edu.service.BannerService;
+import cn.minbb.edu.service.CourseService;
+import cn.minbb.edu.service.UserRoleService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Order(1)
+@Component
+public class InitDataRunner implements ApplicationRunner {
+
+    private Logger logger = LoggerFactory.getLogger(InitDataRunner.class);
+
+    private UserRoleService userRoleService;
+    private CourseService courseService;
+    private BannerService bannerService;
+
+    public InitDataRunner(UserRoleService userRoleService, CourseService courseService, BannerService bannerService) {
+        this.userRoleService = userRoleService;
+        this.courseService = courseService;
+        this.bannerService = bannerService;
+    }
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        // 检查用户角色 - 初始化用户角色数据
+        for (UserRole.Role role : UserRole.Role.values()) {
+            if (null == userRoleService.findOneByRole(role)) {
+                logger.info("初始化用户角色数据 -> {} = {}", role.name(), role.getDescription());
+                userRoleService.saveOne(new UserRole(role, role.getDescription()));
+            }
+        }
+        // 检查主页轮播 - 初始化轮播数据
+        List<Banner> bannerList = bannerService.findAll();
+        if (bannerList.isEmpty()) {
+            Banner banner1 = new Banner();
+            banner1.setTitle("一一一");
+            banner1.setSubtitle("一一一一一一一一一一一一一一一一一一一一一");
+            banner1.setAction("现在去学习");
+            banner1.setCover("");
+            banner1.setPath("");
+            banner1.setRemark("一");
+            banner1.setUserId(1);
+            bannerService.save(banner1);
+            Banner banner2 = new Banner();
+            banner2.setTitle("二二二");
+            banner2.setSubtitle("二二二二二二二二二二二二二二二二二二二二二");
+            banner2.setAction("了解更多");
+            banner2.setCover("");
+            banner2.setPath("");
+            banner2.setRemark("二");
+            banner2.setUserId(1);
+            bannerService.save(banner2);
+            Banner banner3 = new Banner();
+            banner3.setTitle("三三三");
+            banner3.setSubtitle("三三三三三三三三三三三三三三三三三三三三三");
+            banner3.setAction("了解更多");
+            banner3.setCover("");
+            banner3.setPath("");
+            banner3.setRemark("三");
+            banner3.setUserId(1);
+            bannerService.save(banner3);
+        }
+    }
+}

+ 16 - 0
src/main/java/cn/minbb/edu/util/RequestUtil.java

@@ -0,0 +1,16 @@
+package cn.minbb.edu.util;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RequestUtil {
+
+    /**
+     * 判断请求是否为 Ajax 请求
+     *
+     * @param request HttpServletRequest
+     * @return 是否为 Ajax 请求
+     */
+    public static boolean isAjax(HttpServletRequest request) {
+        return (null != request.getHeader("X-Requested-With") && "XMLHttpRequest".equals(request.getHeader("X-Requested-With")));
+    }
+}

+ 50 - 0
src/main/java/cn/minbb/edu/util/SortTools.java

@@ -0,0 +1,50 @@
+package cn.minbb.edu.util;
+
+import org.springframework.data.domain.Sort;
+
+public class SortTools {
+
+    private SortTools() {
+    }
+
+    public static Sort idUp() {
+        return new Sort(Sort.Direction.ASC, "id");
+    }
+
+    public static Sort idDown() {
+        return new Sort(Sort.Direction.DESC, "id");
+    }
+
+    public static Sort createdAtUp() {
+        return new Sort(Sort.Direction.ASC, "createdAt");
+    }
+
+    public static Sort createdAtDown() {
+        return new Sort(Sort.Direction.DESC, "createdAt");
+    }
+
+    public static Sort basicAscSort(String sort) {
+        return new Sort(Sort.Direction.ASC, sort);
+    }
+
+    public static Sort basicDescSort(String sort) {
+        return new Sort(Sort.Direction.DESC, sort);
+    }
+
+    /**
+     * 基本排序
+     *
+     * @param order 默认降序 - desc
+     * @param sort  默认ID - id
+     * @return 排序规则
+     */
+    public static Sort basicSort(String order, String sort) {
+        if (order == null) {
+            order = "desc";
+        }
+        if (sort == null) {
+            sort = "id";
+        }
+        return new Sort(Sort.Direction.fromString(order), sort);
+    }
+}

+ 1 - 1
src/main/resources/application-dev.properties

@@ -1,6 +1,6 @@
 # ¿ª·¢»·¾³
 server.servlet.context-path=/
 # MySQL
-spring.datasource.url=jdbc:mysql://127.0.0.1:3306/edu?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
+spring.datasource.url=jdbc:mysql://127.0.0.1:3306/edu?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
 spring.datasource.username=
 spring.datasource.password=

+ 1 - 1
src/main/resources/application-pro.properties

@@ -1,6 +1,6 @@
 # Éú²ú»·¾³
 server.servlet.context-path=/
 # MySQL
-spring.datasource.url=jdbc:mysql://www.minbb.cn:3306/edu?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
+spring.datasource.url=jdbc:mysql://www.minbb.cn:3306/edu?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
 spring.datasource.username=Yumin
 spring.datasource.password=Wang19970305

+ 6 - 1
src/main/resources/application.properties

@@ -2,14 +2,19 @@
 spring.profiles.active=pro
 server.port=80
 server.servlet.context-path=/
+spring.jmx.default-domain=edu
+spring.servlet.multipart.max-file-size=500MB
+spring.servlet.multipart.max-request-size=600MB
 spring.datasource.url=
 spring.datasource.username=
 spring.datasource.password=
-spring.datasource.driver-class-name=com.mysql.jdbc.Driver
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 spring.datasource.sql-script-encoding=UTF-8
 spring.jpa.show-sql=false
 spring.jpa.properties.hibernate.hbm2ddl.auto=update
 spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
+# ¿ªÆôÀÁ¼ÓÔØ
+spring.jpa.open-in-view=true
 # Ä£°åÒýÇæ
 spring.thymeleaf.mode=HTML
 spring.thymeleaf.cache=false

+ 60 - 0
src/main/resources/static/css/user-sign.css

@@ -0,0 +1,60 @@
+html, body {
+    height: 100%;
+}
+
+a {
+    color: gray;
+    font-size: small;
+}
+
+a:hover {
+    color: pink;
+    text-decoration: none;
+}
+
+body {
+    display: -ms-flexbox;
+    display: -webkit-box;
+    display: flex;
+    -ms-flex-align: center;
+    -ms-flex-pack: center;
+    -webkit-box-align: center;
+    align-items: center;
+    -webkit-box-pack: center;
+    justify-content: center;
+    padding-top: 40px;
+    padding-bottom: 40px;
+    background-color: #f5f5f5;
+}
+
+.form-signin {
+    width: 100%;
+    max-width: 360px;
+    padding: 15px;
+    margin: 0 auto;
+}
+
+.form-signin .form-control {
+    position: relative;
+    box-sizing: border-box;
+    height: auto;
+    padding: 10px;
+    font-size: 16px;
+}
+
+.form-signin .form-control:focus {
+    z-index: 2;
+}
+
+.form-signin input[id="name"] {
+    margin-top: 24px;
+    margin-bottom: 0;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+}
+
+.form-signin input[type="password"] {
+    margin-bottom: 10px;
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+}

BIN
src/main/resources/static/favicon.ico


+ 17 - 0
src/main/resources/templates/admin-config.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>系统配置 - 知学教育</title>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container" style="margin-top: 24px;">
+        知学教育APP,您身边的教育专家。
+    </div>
+</th:block>
+</body>
+</html>

+ 59 - 0
src/main/resources/templates/course-center.html

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>课程中心 - 知学教育</title>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container" style="margin-top: 96px;">
+        <section class="jumbotron text-center" style="padding: 48px;">
+            <div class="container">
+                <h1 class="jumbotron-heading">课程中心</h1>
+                <p class="lead text-muted" style="padding: 0;">在这里添加您的课程</p>
+                <p style="margin-bottom: 0;">
+                    <a href="/course/create" class="btn btn-primary my-2">创建课程</a>
+                    <a href="#" class="btn btn-secondary my-2">管理课程</a>
+                </p>
+            </div>
+        </section>
+
+        <div class="album py-5 bg-light">
+            <div class="container">
+                <div class="row">
+                    <div class="col-xs-12 col-sm-6 col-md-4" th:each="course, iter : ${courseList}">
+                        <div class="card mb-4 box-shadow">
+                            <img class="card-img-top" src="" alt=""
+                                 th:src="${course.getCover()}"
+                                 th:alt="${course.getName()}"/>
+                            <div class="card-body">
+                                <strong class="h4" th:text="${course.getName()}">课程名称</strong>
+                                <p class="card-text" th:text="${#strings.abbreviate(course.getIntroduction(), 42)}">课程介绍</p>
+                                <div class="d-flex justify-content-between align-items-center">
+                                    <small class="text-muted" th:text="${#dates.format(course.getCreatedAt(), 'yyyy-MM-dd HH:mm:ss')}"></small>
+                                    <div class="btn-group">
+                                        <button type="button" class="btn btn-sm btn-outline-secondary">编辑</button>
+                                        <button class="btn btn-sm btn-outline-secondary" type="button" onclick="viewer(this);"
+                                                th:data-id="${course.getId()}">查看
+                                        </button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        function viewer(obj) {
+            window.location.href = "/course/player?id=" + obj.dataset.id;
+        }
+    </script>
+</th:block>
+</body>
+</html>

+ 56 - 0
src/main/resources/templates/course-create.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>创建课程 - 知学教育</title>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container" style="margin-top: 24px;">
+        <div class="py-4 text-center">
+            <!-- <img class="d-block mx-auto mb-4" src="/favicon.ico" alt="" width="72" height="72"/>-->
+            <h2>创建课程</h2>
+            <!-- <p class="lead">创建属于您的课程</p>-->
+        </div>
+
+        <hr class="mb-4"/>
+
+        <div class="row">
+            <div class="col-md-12">
+                <h4 class="mb-3">课程信息</h4>
+                <form method="post" action="/course/create" enctype="multipart/form-data">
+                    <div class="mb-3">
+                        <label for="name">课程名称</label>
+                        <input class="form-control" id="name" name="name" type="text" placeholder="不超过六十个字" required autofocus/>
+                    </div>
+                    <div class="mb-3">
+                        <label for="introduction">课程介绍</label>
+                        <textarea class="form-control" id="introduction" name="introduction" placeholder="不超过两百四十个字" rows="3" required></textarea>
+                    </div>
+                    <div class="mb-3">
+                        <label for="remark">课程备注</label>
+                        <textarea class="form-control" id="remark" name="remark" placeholder="不超过两百四十个字(非必填)" rows="2"></textarea>
+                    </div>
+                    <div class="row">
+                        <div class="col-sm-12 col-md-6 mb-3">
+                            <label for="cover">课程封面</label>
+                            <input class="form-control" id="cover" name="cover" type="file" placeholder="图片大小不超过2MB" required
+                                   accept="image/png, image/jpeg, image/gif, image/jpg"/>
+                        </div>
+                        <div class="col-sm-12 col-md-6 mb-3">
+                            <label for="video">课程视频</label>
+                            <input class="form-control" id="video" name="video" type="file" placeholder="视频大小不超过500MB" required
+                                   accept="video/mp4, video/3gpp"/>
+                        </div>
+                    </div>
+                    <button class="btn btn-primary btn-lg btn-block mt-3" type="submit">创建课程</button>
+                </form>
+            </div>
+        </div>
+    </div>
+</th:block>
+</body>
+</html>

+ 46 - 0
src/main/resources/templates/course-player.html

@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title th:if="${course == null}">课程不存在 - 知学教育</title>
+    <title th:unless="${course == null}" th:text="${course.getName()} + ' - 知学教育'"></title>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="jumbotron" th:if="${course == null}">
+        <div class="col-sm-8 mx-auto">
+            <h1>抱歉哦</h1>
+            <p>您要查找的课程不存在</p>
+            <p>建议您去课程广场看看哦,那里有更多优质课程等着您!</p>
+            <p><a class="btn btn-primary" href="/course/square" role="button">课程广场 »</a></p>
+        </div>
+    </div>
+    <div th:unless="${course == null}">
+        <nav class="navbar navbar-expand navbar-dark bg-dark" style="z-index: 0;">
+            <a class="navbar-brand" href="#!" th:text="${course.getName()}">课程名称</a>
+            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExample02" aria-controls="navbarsExample02"
+                    aria-expanded="false" aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+
+            <div class="collapse navbar-collapse" id="navbarsExample02">
+                <ul class="navbar-nav mr-auto">
+                    <li class="nav-item active">
+                        <a class="nav-link" href="/course/center">课程中心</a>
+                    </li>
+                </ul>
+            </div>
+        </nav>
+        <video src="" controls="controls" width="100%" th:src="${course.getVideo()}"></video>
+
+        <div class="container" style="margin-top: 24px;">
+            <h4>课程简介</h4>
+            <div th:text="${course.getIntroduction()}" style="text-indent: 2em;"></div>
+        </div>
+    </div>
+</th:block>
+</body>
+</html>

+ 17 - 0
src/main/resources/templates/course-square.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>课程广场 - 知学教育</title>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container" style="margin-top: 24px;">
+        知学教育APP,您身边的教育专家。
+    </div>
+</th:block>
+</body>
+</html>

+ 17 - 0
src/main/resources/templates/fragments/footer.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title></title>
+
+    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"/>
+</head>
+<body>
+<footer style="position: absolute; bottom: 0; width: 100%; height: 60px; line-height: 60px; background-color: #f5f5f5;" th:fragment="footer">
+    <div class="container" style="padding: 0 15px;">
+        <span class="text-muted">&copy; 2019 <a href="/"> 知学教育</a></span>
+    </div>
+</footer>
+</body>
+</html>

+ 70 - 0
src/main/resources/templates/fragments/header.html

@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title></title>
+    <!-- Material Design for Bootstrap fonts and icons -->
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"/>
+    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"/>
+</head>
+<body>
+<nav class="navbar navbar-expand-lg navbar-light fixed-top bg-light" th:fragment="header">
+    <a class="navbar-brand" href="/">知学教育</a>
+    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-supported-content"
+            aria-controls="navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
+        <span class="navbar-toggler-icon"></span>
+    </button>
+
+    <div class="collapse navbar-collapse" id="navbar-supported-content">
+        <ul class="navbar-nav mr-auto">
+            <li class="nav-item" th:classappend="${ACTIVE == 'index'}? 'active'">
+                <a class="nav-link" href="/">首页<span class="sr-only">(current)</span></a>
+            </li>
+            <li class="nav-item" th:class="${ACTIVE == 'about'}? 'active'">
+                <a class="nav-link" href="/about">关于</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link" href="#!" data-toggle="tooltip" data-placement="bottom" data-html="true"
+                   title="<img src='https://files-1252373323.cos.ap-beijing.myqcloud.com/images/er_code.jpg' width='150' alt='下载二维码'/><p>Version: 1.0</p>">
+                    下载二维码
+                </a>
+            </li>
+        </ul>
+        <form class="form-inline my-2 my-lg-0">
+            <input class="form-control mr-sm-2" type="search" placeholder="搜索..." aria-label="Search"/>
+            <button class="btn btn-sm btn-outline-success my-2 my-sm-0" type="submit">搜索</button>
+        </form>
+        <ul class="navbar-nav ml-auto">
+            <li class="nav-item" th:if="${user == null}">
+                <a class="nav-link" href="/sign-in">登录 / 注册</a>
+            </li>
+            <li class="nav-item dropdown" th:unless="${user == null}">
+                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true"
+                   aria-expanded="false" th:text="${user.getName()}">
+                    用户名
+                </a>
+                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
+                    <a class="dropdown-item" href="/user/center">用户中心</a>
+                    <a class="dropdown-item" href="/course/center">课程中心</a>
+                    <div th:if="${user.hasRole(T(cn.minbb.edu.model.UserRole.Role).ADMIN)}">
+                        <div class="dropdown-divider"></div>
+                        <a class="dropdown-item" href="/admin/config">系统配置</a>
+                    </div>
+                    <div class="dropdown-divider"></div>
+                    <a class="dropdown-item" href="/sign-out">退出登录</a>
+                </div>
+            </li>
+        </ul>
+    </div>
+</nav>
+
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js"></script>
+<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js"></script>
+<script>
+    $(document).ready(function () {
+    });
+</script>
+</body>
+</html>

+ 145 - 0
src/main/resources/templates/index-about.html

@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>关于 - 知学教育</title>
+
+    <style>
+        .marketing .col-lg-4 {
+            margin-bottom: 1.5rem;
+            text-align: center;
+        }
+
+        .marketing h2 {
+            font-weight: 400;
+            margin-top: 12px;
+        }
+
+        .marketing .col-lg-4 p {
+            margin-right: .75rem;
+            margin-left: .75rem;
+        }
+    </style>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="jumbotron">
+        <div class="container">
+            <h1 class="display-3">知学教育APP</h1>
+            <p>
+                一款很方便的教学APP - 来自 <code class="highlighter-rouge">朱孟尧.</code>
+            </p>
+            <hr class="my-4">
+            <p>觉得好用你就赶快下载吧!还不快去!</p>
+            <p><a class="btn btn-primary btn-lg"
+                  href="https://files-1252373323.cos.ap-beijing.myqcloud.com/apk/%E7%9F%A5%E5%AD%A6%E6%95%99%E8%82%B2v1.0.apk" role="button">
+                立即下载&raquo;</a></p>
+        </div>
+    </div>
+
+    <div class="container marketing">
+        <div class="row">
+            <div class="col-lg-4">
+                <img class="rounded-circle" src="/favicon.ico" alt="" width="140" height="140"/>
+                <h2>加油</h2>
+                <p>不管什么时候,学习都不晚。</p>
+                <p><a class="btn btn-secondary"
+                      href="https://files-1252373323.cos.ap-beijing.myqcloud.com/apk/%E7%9F%A5%E5%AD%A6%E6%95%99%E8%82%B2v1.0.apk" role="button">
+                    去下载 &raquo;</a></p>
+            </div>
+
+            <div class="col-lg-4">
+                <img class="rounded-circle" src="/favicon.ico" alt="" width="140" height="140"/>
+                <h2>奋斗</h2>
+                <p>十年如未死,卷土定重来!</p>
+                <p><a class="btn btn-secondary"
+                      href="https://files-1252373323.cos.ap-beijing.myqcloud.com/apk/%E7%9F%A5%E5%AD%A6%E6%95%99%E8%82%B2v1.0.apk" role="button">
+                    去下载 &raquo;</a></p>
+            </div>
+
+            <div class="col-lg-4">
+                <img class="rounded-circle" src="/favicon.ico" alt="" width="140" height="140"/>
+                <h2>永不放弃</h2>
+                <p>三人行,必有我师焉。</p>
+                <p><a class="btn btn-secondary"
+                      href="https://files-1252373323.cos.ap-beijing.myqcloud.com/apk/%E7%9F%A5%E5%AD%A6%E6%95%99%E8%82%B2v1.0.apk" role="button">
+                    去下载 &raquo;</a></p>
+            </div>
+        </div>
+
+        <hr class="featurette-divider">
+
+        <div class="row featurette">
+            <div class="col-md-7">
+                <h2 class="featurette-heading">
+                    一对一备考指导<span class="text-muted"></span>
+                </h2>
+                <p class="lead" style="text-indent: 2em;">
+                    为了让参加考试的考生轻松报考,智学教育特别策划了一对一报考指导。智学教育的一对一报考指导会,解答考生朋友们在报名时遇到的疑难问题,为大家提供详细且十分具体的备考方案,并根据您自身条件,力求避开雷区,合理科学推荐报考职位。
+                </p>
+            </div>
+            <div class="col-md-5">
+                <img class="featurette-image img-fluid mx-auto"
+                     src="https://files-1252373323.cos.ap-beijing.myqcloud.com/images/about/%E5%A4%87%E8%80%83%E6%8C%87%E5%AF%BC.jpg" alt=""/>
+            </div>
+        </div>
+
+        <hr class="featurette-divider"/>
+
+        <div class="row featurette">
+            <div class="col-md-7 order-md-2">
+                <h2 class="featurette-heading">
+                    全真的考试题库<span class="text-muted"></span>
+                </h2>
+                <p class="lead" style="text-indent: 2em;">
+                    考试题库是基于大数据的人工智能算法研发而成的考试题库,根据职业资格考试的考点、考频、难度分布,提供10万套在线试卷进行全真测试、模拟练习,同时提供公务员,外语类,财会类,建筑类,职业资格,学历考试,医药类,外贸类,计算机类等考试试题。
+                </p>
+            </div>
+            <div class="col-md-5 order-md-1">
+                <img class="featurette-image img-fluid mx-auto"
+                     src="https://files-1252373323.cos.ap-beijing.myqcloud.com/images/about/%E8%80%83%E8%AF%95%E9%A2%98%E5%BA%93.jpg" alt=""/>
+            </div>
+        </div>
+
+        <hr class="featurette-divider"/>
+
+        <div class="row featurette">
+            <div class="col-md-7">
+                <h2 class="featurette-heading">
+                    快速智能的练习<span class="text-muted"></span>
+                </h2>
+                <p class="lead" style="text-indent: 2em;">
+                    快速智能练习在学生选定学习区域后,从题库中随机生成在对应的学习区域出现相应的测试题,学生可以方便快捷点击按钮去做题,学习知识。
+                </p>
+            </div>
+            <div class="col-md-5">
+                <img class="featurette-image img-fluid mx-auto"
+                     src="https://files-1252373323.cos.ap-beijing.myqcloud.com/images/about/%E5%BF%AB%E9%80%9F%E7%BB%83%E4%B9%A0.jpg" alt=""/>
+            </div>
+        </div>
+
+        <hr class="featurette-divider"/>
+
+        <div class="row featurette">
+            <div class="col-md-7 order-md-2">
+                <h2 class="featurette-heading">
+                    精心的家庭作业<span class="text-muted"></span>
+                </h2>
+                <p class="lead" style="text-indent: 2em;">
+                    课上听过老师讲解后,老师还会在课下留置作业,这些作业都是根据每节课的内容精心编制的,帮助学生对每一节课的疑难点进行复习巩固,避免在同样的地方出现错误,同时老师也会对作业进行收缴、批注,再分发,帮助学生学习易错点。
+                </p>
+            </div>
+            <div class="col-md-5 order-md-1">
+                <img class="featurette-image img-fluid mx-auto"
+                     src="https://files-1252373323.cos.ap-beijing.myqcloud.com/images/about/%E5%AE%B6%E5%BA%AD%E4%BD%9C%E4%B8%9A.jpg" alt=""/>
+            </div>
+        </div>
+
+        <hr class="featurette-divider"/>
+    </div>
+</th:block>
+</body>
+</html>

+ 138 - 2
src/main/resources/templates/index.html

@@ -1,10 +1,146 @@
 <!DOCTYPE html>
-<html lang="zh-CN">
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
 <head>
     <meta charset="UTF-8"/>
-    <title>Title</title>
+    <title>首页 - 知学教育</title>
+
+    <style>
+        /* Since positioning the image, we need to help out the caption */
+        .carousel-caption {
+            bottom: 3rem;
+            z-index: 10;
+        }
+
+        /* Declare heights because of positioning of img element */
+        .carousel-item {
+            height: 32rem;
+            background-color: #777;
+        }
+
+        .carousel-item > img {
+            position: absolute;
+            top: 0;
+            left: 0;
+            min-width: 100%;
+            height: 32rem;
+        }
+
+        /* MARKETING CONTENT */
+        /* Center align the text within the three columns below the carousel */
+        .marketing .col-lg-4 {
+            margin-bottom: 1.5rem;
+            text-align: center;
+        }
+
+        .marketing h2 {
+            font-weight: 400;
+        }
+
+        .marketing .col-lg-4 p {
+            margin-right: .75rem;
+            margin-left: .75rem;
+        }
+
+        /* Featurettes */
+
+        .featurette-divider {
+            margin: 5rem 0; /* Space out the Bootstrap <hr> more */
+        }
+
+        /* Thin out the marketing headings */
+        .featurette-heading {
+            font-weight: 300;
+            line-height: 1;
+            letter-spacing: -.05rem;
+        }
+
+        /* RESPONSIVE CSS */
+
+        @media (min-width: 40em) {
+            /* Bump up size of carousel content */
+            .carousel-caption p {
+                margin-bottom: 20px;
+                font-size: 1.25rem;
+                line-height: 1.4;
+            }
+
+            .featurette-heading {
+                font-size: 50px;
+            }
+        }
+
+        @media (min-width: 62em) {
+            .featurette-heading {
+                margin-top: 7rem;
+            }
+        }
+    </style>
 </head>
 <body>
+<th:block layout:fragment="content">
+    <div class="carousel slide" id="index-carousel" data-ride="carousel">
+        <ol class="carousel-indicators">
+            <li data-target="#index-carousel" th:each="banner, stat : ${bannerList}"
+                th:data-slide-to="${stat.index}" th:class="${stat.index == 0}? 'active'"></li>
+        </ol>
+        <div class="carousel-inner">
+            <div class="carousel-item" th:each="banner, stat : ${bannerList}" th:classappend="${stat.index == 0}? 'active'">
+                <img class="first-slide" src="" alt="" th:src="${banner.getCover()}" th:alt="${banner.getRemark()}"/>
+                <div class="container">
+                    <div class="carousel-caption" th:classappend="${stat.count % 3 == 1}? 'text-left' : (${stat.count % 3 == 2}? '' : 'text-right')">
+                        <h1 th:text="${banner.getTitle()}"></h1>
+                        <p th:text="${banner.getSubtitle()}"></p>
+                        <p>
+                            <a class="btn btn-lg btn-success" href="" role="button" target="_blank"
+                               th:href="${banner.getPath()}" th:text="${banner.getAction()}"></a>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <a class="carousel-control-prev" href="#index-carousel" role="button" data-slide="prev">
+            <span class="carousel-control-prev-icon" aria-hidden="true"></span>
+            <span class="sr-only">Previous</span>
+        </a>
+        <a class="carousel-control-next" href="#index-carousel" role="button" data-slide="next">
+            <span class="carousel-control-next-icon" aria-hidden="true"></span>
+            <span class="sr-only">Next</span>
+        </a>
+    </div>
 
+    <div class="container" style="margin-top: 24px;">
+        <h1>知学教育APP,您身边的教育专家。</h1>
+        <br/>
+        <h3>1、项目背景:</h3>
+        <p>
+            随着网络技术的飞速发展和网络全球化的逐步推进,移动智能设备已经广泛进入人们的日常生活并成为其中不可或缺的一部分。人们的学习需求对移动智能终端及移动互联网的依赖也随之越来越严重。在这种趋势下,移动学习成为了被广泛采用并接受的新型学习方式。
+        </p>
+        <h3>2、主要内容:</h3>
+        <p>
+            ①在认真参考和阅读有关资料的基础上,对使用学习系统需求分析进行较为全面、细致的描述。②给出相关软件设计的整体结构,工作流程,功能模块设置、系统性能要求。③结合最前沿的软件开发技术,对部分应用实例进行设计与实现。
+        </p>
+        <h3>3、现有条件:</h3>
+        <p>
+            硬件配备:个人电脑一台,支持连接到互联网的智能手机一部,阿里云服务器。
+            <br/>
+            软件配置:系统采用Microsoft公司的Windows 10企业版,安装有Office 2016系列办公软件,IntelliJ IDEA,Android Studio,MySQL等等, 服务器环境支持Apache Tomcat 9.0以及Nginx运行环境。
+            <br/>
+            资料准备:第一行代码、上网查阅相关资料、借阅相关书籍。
+        </p>
+        <h3>4、时间安排:</h3>
+        <p>
+            第1-2周熟悉题目,对Android编程技术进行熟悉以及对学习系统的设计进行分析,完成开题报告、文献综述以及需求分析。第3-5周完成总体设计,根据应用需要搭建软件框架。第6-9周初步完成系统详细设计,实现全部应用功能。第10-12周对系统进行细节完善和优化。第13-16周根据设计过程中的记录文档及其功能编写毕业论文。
+        </p>
+        <h3>5、预期成果及表现形式:</h3>
+        <p>
+            预期成果:为用户提供功能完全的学习系统的软件服务,使用户可以通过体学习系统学习各种资料等。
+            <br/>
+            表现形式:以友好的图形化界面实现该系统,方便各个用户的使用,提交毕业设计论文。
+        </p>
+    </div>
+</th:block>
 </body>
 </html>

+ 45 - 0
src/main/resources/templates/layouts/layout.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title></title>
+
+    <!-- Material Design for Bootstrap fonts and icons -->
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"/>
+    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"/>
+    <style>
+        html {
+            position: relative;
+            min-height: 100%;
+        }
+
+        a:hover {
+            text-decoration: none;
+        }
+    </style>
+</head>
+<body style="margin-bottom: 60px;">
+<header th:replace="~{fragments/header :: header}"></header>
+
+<div style="margin-top: 57px; margin-bottom: 96px;">
+    <div layout:fragment="content">
+        <p>内容</p>
+    </div>
+</div>
+
+<footer th:replace="~{fragments/footer :: footer}"></footer>
+
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js"></script>
+<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js"></script>
+<script>
+    $(document).ready(function () {
+        $('body').bootstrapMaterialDesign();
+        $('[data-toggle="tooltip"]').tooltip();
+    });
+</script>
+</body>
+</html>

+ 48 - 35
src/main/resources/templates/sign-in.html

@@ -1,55 +1,68 @@
 <!DOCTYPE html>
-<html lang="zh-CN"
-      xmlns="http://www.w3.org/1999/html"
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
       xmlns:th="http://www.thymeleaf.org">
 <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
-    <title>用户登录</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title>用户登录 - 知学教育</title>
 
-    <!-- Bootstrap -->
-    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
-
-    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
-    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
-    <!--[if lt IE 9]>
-    <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
-    <![endif]-->
+    <link href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link rel="stylesheet" href="../static/css/user-sign.css" th:href="@{/css/user-sign.css}"/>
 </head>
-<body>
-<div class="container">
-    <div class="row">
-        <h1>用户登录</h1>
+<body class="text-center">
+<form class="form-signin" method="post" action="/sign-in">
+    <a href="/"><img class="mb-4" src="../static/favicon.ico" alt="" width="72" height="72" th:src="@{/static/favicon.ico}"/></a>
+    <h1 class="h3 mb-3 font-weight-normal">用户登录</h1>
+    <small style="color: blue;" th:if="${param.logout}">退出登录成功,重新登录以开始您的会话</small>
+    <div th:unless="${param.logout}">
+        <small style="color: red;" th:if="${param.error != null && session.SPRING_SECURITY_LAST_EXCEPTION != null}"
+               th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></small>
+        <small th:if="${param.error == null || session.SPRING_SECURITY_LAST_EXCEPTION == null}">登录以开始您的会话</small>
     </div>
 
-    <div class="row">
-        <div class="col-sm-12 col-md-6 col-lg-6">
-            <div class="form-group">
-                <input id="username" type="text" class="form-control" placeholder="用户名"/>
-            </div>
-
-            <div class="form-group">
-                <input id="password" type="password" class="form-control" placeholder="密码"/>
-            </div>
+    <label for="username" class="sr-only">用户名</label>
+    <input class="form-control" id="username" name="username" type="text" placeholder="用户名" style="height: 44px;" required autofocus/>
 
-            <button type="button" class="btn btn-default" onclick="login();">登录</button>
-        </div>
+    <label for="password" class="sr-only">密码</label>
+    <input class="form-control" id="password" name="password" type="password" placeholder="密码" style="height: 44px;" required/>
 
-        <div class="col-sm-12 col-md-6 col-lg-6">
+    <div class="mb-3">
+        <a href="/sign-up" style="margin-right: 36px;">注册账号</a>
+        <a href="#password-modal" data-toggle="modal">忘记密码</a>
+    </div>
+    <button class="btn btn-lg btn-success btn-block" type="submit">登录</button>
+    <p class="mt-5 mb-3 text-muted">&copy; 2019 知学教育</p>
+</form>
 
+<!-- Modal -->
+<div class="modal fade" id="password-modal" tabindex="-1" role="dialog" aria-labelledby="" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">忘记密码</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                请联系系统管理员
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary" data-dismiss="modal">我知道了</button>
+            </div>
         </div>
     </div>
 </div>
 
-<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
-<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
-<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
-<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
-
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js"></script>
+<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js"></script>
 <script>
+    $(document).ready(function () {
+    });
+
     function login() {
         $.ajax({
             url: '/user/login?username=' + $('#username').val() + '&password=' + $('#password').val(),

+ 89 - 0
src/main/resources/templates/sign-up.html

@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html lang="zh-CN"
+      xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title>用户注册 - 知学教育</title>
+
+    <link href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link rel="stylesheet" href="../static/css/user-sign.css" th:href="@{/css/user-sign.css}"/>
+</head>
+<body class="text-center">
+
+<div class="form-signin">
+    <a href="/"><img class="mb-4" src="../static/favicon.ico" alt="" width="72" height="72" th:src="@{/static/favicon.ico}"/></a>
+    <h1 class="h3 mb-3 font-weight-normal">用户注册</h1>
+
+    <label for="name" class="sr-only">姓名</label>
+    <input class="form-control" id="name" type="text" placeholder="姓名" maxlength="16"
+           style="height: 44px;" required autofocus/>
+
+    <label for="username" class="sr-only">用户名</label>
+    <input class="form-control" id="username" type="text" placeholder="用户名" maxlength="16"
+           style="height: 44px;" required/>
+
+    <label for="password" class="sr-only">密码</label>
+    <input class="form-control" id="password" type="password" placeholder="密码" minlength="6" maxlength="24"
+           style="height: 44px; margin-bottom: 0;" required/>
+
+    <label for="repeat-password" class="sr-only">重复密码</label>
+    <input class="form-control" id="repeat-password" type="password" placeholder="重复密码" minlength="6" maxlength="24"
+           style="height: 44px;" required/>
+
+    <div class="mb-3"><a href="/sign-in">我有账号? 去登录</a></div>
+    <button class="btn btn-lg btn-success btn-block" type="button" onclick="return submit();">注册</button>
+    <p class="mt-5 mb-3 text-muted">&copy; 2019 知学教育</p>
+</div>
+
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script>
+    $(document).ready(function () {
+    });
+
+    function submit() {
+        let nameInput = $('#name'), usernameInput = $('#username'), passwordInput = $('#password'), passwordInput2 = $('#repeat-password');
+        let name = nameInput.val(), username = usernameInput.val(), password = passwordInput.val(), password2 = passwordInput2.val();
+        if (name === "") {
+            alert("姓名不能为空");
+            return;
+        } else if (username === "") {
+            alert("用户名不能为空");
+            return;
+        } else if (password === "") {
+            alert("密码不能为空");
+            return;
+        } else if (password.length < 6) {
+            alert("密码至少六位");
+            return;
+        } else if (password !== password2) {
+            alert("两次密码输入不一致");
+            passwordInput.val("");
+            passwordInput2.val("");
+            return;
+        }
+        $.ajax({
+            url: '/sign-up',
+            type: 'post',
+            data: JSON.stringify({"name": name, "username": username, "password": password}),
+            async: true,
+            dataType: 'json',
+            contentType: false,
+            success: function (result) {
+                if (result && result.success) {
+                    let user = result.data;
+                    alert("恭喜你 " + user.name + " ,注册成功,登录用户名为:" + user.username);
+                    window.location.href = "/sign-in";
+                } else {
+                    alert("注册失败:" + result.message);
+                    window.location.reload();
+                }
+            }
+        });
+        return false;
+    }
+</script>
+</body>
+</html>

+ 17 - 0
src/main/resources/templates/user-center.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>用户中心 - 知学教育</title>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container" style="margin-top: 24px;">
+        知学教育APP,您身边的教育专家。
+    </div>
+</th:block>
+</body>
+</html>