Răsfoiți Sursa

1. 新建页面访问日志类及实现相关记录功能
2. 新新增拦截器
3. 新增工具类
4. 完善前端页面

王育民 5 ani în urmă
părinte
comite
75f7e3fdd4
26 a modificat fișierele cu 875 adăugiri și 78 ștergeri
  1. 8 1
      pom.xml
  2. 105 0
      src/main/java/cn/minbb/job/aspect/WebLogAspect.java
  3. 38 0
      src/main/java/cn/minbb/job/config/WebMvcConfig.java
  4. 7 1
      src/main/java/cn/minbb/job/controller/web/MainController.java
  5. 1 0
      src/main/java/cn/minbb/job/data/Const.java
  6. 15 0
      src/main/java/cn/minbb/job/enumerate/Role.java
  7. 40 0
      src/main/java/cn/minbb/job/interceptor/AppInterceptor.java
  8. 1 1
      src/main/java/cn/minbb/job/model/Auditable.java
  9. 78 5
      src/main/java/cn/minbb/job/model/User.java
  10. 49 0
      src/main/java/cn/minbb/job/model/UserRole.java
  11. 69 0
      src/main/java/cn/minbb/job/model/WebLog.java
  12. 24 0
      src/main/java/cn/minbb/job/model/repository/WebLogRepository.java
  13. 21 0
      src/main/java/cn/minbb/job/service/WebLogService.java
  14. 70 0
      src/main/java/cn/minbb/job/service/impl/WebLogServiceImpl.java
  15. 74 0
      src/main/java/cn/minbb/job/system/UserSession.java
  16. 90 0
      src/main/java/cn/minbb/job/util/IPUtil.java
  17. 4 4
      src/main/resources/application-pro.properties
  18. 2 2
      src/main/resources/application.properties
  19. 22 0
      src/main/resources/static/css/style.css
  20. 108 0
      src/main/resources/templates/about.html
  21. 15 40
      src/main/resources/templates/fragments/footer.html
  22. 8 9
      src/main/resources/templates/fragments/header.html
  23. 2 2
      src/main/resources/templates/index.html
  24. 18 7
      src/main/resources/templates/layouts/layout.html
  25. 3 3
      src/main/resources/templates/sign-in.html
  26. 3 3
      src/main/resources/templates/sign-up.html

+ 8 - 1
pom.xml

@@ -12,7 +12,7 @@
     <name>Job</name>
     <groupId>cn.minbb</groupId>
     <artifactId>job</artifactId>
-    <version>0.0.1.RELEASE</version>
+    <version>0.0.1.SNAPSHOT</version>
     <description>Job project for Spring Boot</description>
 
     <properties>
@@ -96,6 +96,13 @@
             <version>1.1.22</version>
         </dependency>
 
+        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.68</version>
+        </dependency>
+
         <!-- Actuator -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 105 - 0
src/main/java/cn/minbb/job/aspect/WebLogAspect.java

@@ -0,0 +1,105 @@
+package cn.minbb.job.aspect;
+
+import cn.minbb.job.controller.UniqueNameGenerator;
+import cn.minbb.job.data.Const;
+import cn.minbb.job.model.User;
+import cn.minbb.job.model.WebLog;
+import cn.minbb.job.service.WebLogService;
+import cn.minbb.job.system.UserSession;
+import cn.minbb.job.util.IPUtil;
+import com.alibaba.fastjson.JSON;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.lang.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Aspect
+@Component
+@Log4j2
+public class WebLogAspect {
+
+    private final IPUtil ipUtil;
+    private final WebLogService webLogService;
+
+    public WebLogAspect(IPUtil ipUtil, WebLogService webLogService) {
+        this.ipUtil = ipUtil;
+        this.webLogService = webLogService;
+    }
+
+    @Pointcut("execution(public * cn.minbb.job.controller..*.*(..))")
+    public void webLogPointCut() {
+    }
+
+    @Before("webLogPointCut()")
+    public void doBefore(JoinPoint joinPoint) throws Throwable {
+        // 接收到请求
+    }
+
+    @AfterReturning(pointcut = "webLogPointCut()", returning = "object")
+    public void doAfterReturning(Object object) throws Throwable {
+        // 处理完请求
+    }
+
+    @Around("webLogPointCut()")
+    public Object doAround(ProceedingJoinPoint point) throws Throwable {
+        // 记录方法开始执行时间
+        long startTime = System.currentTimeMillis();
+        // 获取目标类名称 // point.getSignature().getDeclaringTypeName()
+        String className = point.getTarget().getClass().getName();
+        // 获取目标类方法名称
+        String classMethodName = className.replaceAll(UniqueNameGenerator.class.getPackage().getName(), "*") + "#" + point.getSignature().getName();
+        // 入参
+        Object[] args = point.getArgs();
+        // 记录请求日志
+        WebLog webLog = null;
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (null != attributes) {
+            HttpServletRequest request = attributes.getRequest();
+            String queryString = request.getQueryString();
+            String requestURI = request.getRequestURI() + (StringUtils.isEmpty(queryString) ? "" : ("?" + request.getQueryString()));
+            String requestMethod = request.getMethod();
+            String requestProtocol = request.getProtocol();
+            String requestHeader = request.getHeader("User-Agent");
+            String ipAddress = ipUtil.getIpAddress(request);
+            // 请求用户
+            User user = UserSession.getUserAuthentication();
+            String userRemark = null;
+            if (null != user) {
+                User userItem = new User();
+                userItem.setId(user.getId());
+                userItem.setUsername(user.getUsername());
+                userItem.setName(user.getName());
+                userRemark = JSON.toJSONString(userItem);
+            }
+            webLog = webLogService.saveOne(new WebLog(requestURI, requestMethod, requestProtocol, requestHeader, classMethodName, null, userRemark, ipAddress));
+            if (request.getAttribute("intercept") != null) {
+                log.info("{} -> {} -> 拒绝访问", requestMethod, requestURI);
+            } else {
+                log.info("{} -> {} {} data -> {}", requestMethod, requestURI, classMethodName, String.valueOf(args));
+            }
+            // 关联请求信息
+            request.setAttribute(Const.Key.KEY_REMARK, webLog.getId());
+        }
+        // 方法执行
+        Object object = point.proceed();
+        // 更新请求日志 - 记录请求时长
+        if (null != webLog) {
+            // 处理时长
+            int processTime = (int) (System.currentTimeMillis() - startTime);
+            webLog.setProcessTime(processTime);
+            webLogService.saveOne(webLog);
+        }
+        return object;
+    }
+
+    @AfterThrowing(value = "webLogPointCut()", throwing = "e")
+    public void afterReturningMethod(JoinPoint joinPoint, Exception e) {
+        log.error("记录访问日志异常 -> ", e);
+    }
+}

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

@@ -0,0 +1,38 @@
+package cn.minbb.job.config;
+
+import cn.minbb.job.interceptor.AppInterceptor;
+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 {
+
+    private final AppInterceptor appInterceptor;
+
+    public WebMvcConfig(AppInterceptor appInterceptor) {
+        this.appInterceptor = appInterceptor;
+    }
+
+    @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) {
+    }
+}

+ 7 - 1
src/main/java/cn/minbb/job/controller/web/MainController.java

@@ -9,7 +9,7 @@ import org.springframework.web.servlet.ModelAndView;
 
 @Controller
 @Log4j2
-@RequestMapping
+@RequestMapping("")
 public class MainController {
 
     @GetMapping("")
@@ -17,4 +17,10 @@ public class MainController {
         modelAndView.setViewName(Const.ViewName.VIEW_INDEX);
         return modelAndView;
     }
+
+    @GetMapping("/about")
+    public ModelAndView aboutPage(ModelAndView modelAndView) {
+        modelAndView.setViewName(Const.ViewName.VIEW_ABOUT);
+        return modelAndView;
+    }
 }

+ 1 - 0
src/main/java/cn/minbb/job/data/Const.java

@@ -34,6 +34,7 @@ public class Const {
      * 应用常量
      */
     public interface Application {
+        String APP_NAME = "校园招聘网";
     }
 
     /**

+ 15 - 0
src/main/java/cn/minbb/job/enumerate/Role.java

@@ -0,0 +1,15 @@
+package cn.minbb.job.enumerate;
+
+import lombok.Getter;
+
+@Getter
+public enum Role {
+    USER("用户"),
+    ADMIN("管理员");
+
+    String description;
+
+    Role(String description) {
+        this.description = description;
+    }
+}

+ 40 - 0
src/main/java/cn/minbb/job/interceptor/AppInterceptor.java

@@ -0,0 +1,40 @@
+package cn.minbb.job.interceptor;
+
+import cn.minbb.job.data.Const;
+import cn.minbb.job.model.User;
+import cn.minbb.job.system.UserSession;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 自定义拦截器
+ */
+@Component
+@Log4j2
+public class AppInterceptor extends HandlerInterceptorAdapter {
+
+    @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) {
+            modelAndView.addObject(Const.Key.KEY_APP_NAME, Const.Application.APP_NAME);
+            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);
+    }
+}

+ 1 - 1
src/main/java/cn/minbb/job/model/Auditable.java

@@ -25,7 +25,7 @@ public abstract class Auditable {
     @Getter
     @Setter
     @CreatedBy
-    @Column(name = "created_by", columnDefinition = "VARCHAR(200) COMMENT '创建者'")
+    @Column(name = "created_by", updatable = false, columnDefinition = "VARCHAR(200) COMMENT '创建者'")
     protected String createdBy;
 
     @Getter

+ 78 - 5
src/main/java/cn/minbb/job/model/User.java

@@ -1,8 +1,18 @@
 package cn.minbb.job.model;
 
+import cn.minbb.job.enumerate.Role;
 import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
 
 import javax.persistence.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
 
 /**
  * 用户基本信息
@@ -16,11 +26,7 @@ import javax.persistence.*;
                 @Index(name = "index_name", columnList = "name")
         }
 )
-public class User {
-
-    @Id
-    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '主键'")
-    protected Integer id;
+public class User extends Auditable implements UserDetails {
 
     @Column(name = "username", nullable = false, columnDefinition = "VARCHAR(32) COMMENT '用户名'")
     private String username;
@@ -45,4 +51,71 @@ public class User {
 
     @Column(name = "qq", columnDefinition = "VARCHAR(16) COMMENT 'QQ'")
     private String qq;
+
+    @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
+    @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;
+
+    @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(Role role) {
+        for (UserRole userRole : getUserRoleSet()) {
+            if (userRole.getRole().equals(role)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

+ 49 - 0
src/main/java/cn/minbb/job/model/UserRole.java

@@ -0,0 +1,49 @@
+package cn.minbb.job.model;
+
+import cn.minbb.job.enumerate.Role;
+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;
+    }
+}

+ 69 - 0
src/main/java/cn/minbb/job/model/WebLog.java

@@ -0,0 +1,69 @@
+package cn.minbb.job.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.CreationTimestamp;
+
+import javax.persistence.*;
+import java.util.Date;
+
+@Data
+@Entity
+@Table(name = "web_log",
+        indexes = {
+                @Index(name = "index_uri", columnList = "uri"),
+                @Index(name = "index_protocol", columnList = "protocol"),
+                @Index(name = "index_ip", columnList = "ip"),
+                @Index(name = "index_created_at", columnList = "created_at"),
+        }
+)
+@NoArgsConstructor
+public class WebLog {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INTEGER COMMENT '访问日志'")
+    private Integer id;
+
+    @Column(name = "uri", nullable = false, columnDefinition = "VARCHAR(768) COMMENT 'URI'")
+    private String uri;
+
+    @Column(name = "method", nullable = false, columnDefinition = "VARCHAR(16) COMMENT '请求方法'")
+    private String method;
+
+    @Column(name = "protocol", columnDefinition = "VARCHAR(16) COMMENT '请求协议'")
+    private String protocol;
+
+    @Column(name = "user_agent", columnDefinition = "TEXT COMMENT '用户代理'")
+    private String userAgent;
+
+    @Column(name = "process_method", columnDefinition = "VARCHAR(255) COMMENT '处理方法'")
+    private String processMethod;
+
+    @Column(name = "process_time", columnDefinition = "INTEGER COMMENT '处理时长 (ms)'")
+    private Integer processTime;
+
+    @Column(name = "remark", columnDefinition = "VARCHAR(1000) COMMENT '备注'")
+    private String remark;
+
+    @Column(name = "ip", columnDefinition = "VARCHAR(16) COMMENT 'IP'")
+    private String ip;
+
+    @Column(name = "location", columnDefinition = "VARCHAR(255) COMMENT '位置'")
+    private String location;
+
+    @Column(name = "created_at", columnDefinition = "DATETIME COMMENT '创建时间'")
+    @CreationTimestamp
+    private Date createdAt;
+
+    public WebLog(String uri, String method, String protocol, String userAgent, String processMethod, Integer processTime, String remark, String ip) {
+        this.uri = uri;
+        this.method = method;
+        this.protocol = protocol;
+        this.userAgent = userAgent;
+        this.processMethod = processMethod;
+        this.processTime = processTime;
+        this.remark = remark;
+        this.ip = ip;
+    }
+}

+ 24 - 0
src/main/java/cn/minbb/job/model/repository/WebLogRepository.java

@@ -0,0 +1,24 @@
+package cn.minbb.job.model.repository;
+
+import cn.minbb.job.model.WebLog;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Repository
+public interface WebLogRepository extends JpaRepository<WebLog, Integer>, JpaSpecificationExecutor<WebLog> {
+    WebLog findTop1ByLocationIsNull();
+
+    List<WebLog> findTop10ByLocationIsNull();
+
+    @Query(value = "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(uri) AS pv, COUNT(DISTINCT ip) AS uv FROM web_Log WHERE created_at BETWEEN ?1 AND ?2 GROUP BY date", nativeQuery = true)
+    List<Map<String, Integer>> selectPvUvByDateBetween(Date startDate, Date endDate);
+
+    @Query(value = "SELECT COUNT(*) FROM web_log WHERE\tcreated_at BETWEEN ?1 AND ?2", nativeQuery = true)
+    int countByDateBetween(Date startTime, Date endTime);
+}

+ 21 - 0
src/main/java/cn/minbb/job/service/WebLogService.java

@@ -0,0 +1,21 @@
+package cn.minbb.job.service;
+
+import cn.minbb.job.model.WebLog;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public interface WebLogService {
+    WebLog saveOne(WebLog webLog);
+
+    WebLog findTop1ByLocationIsNull();
+
+    List<WebLog> findTop10ByLocationIsNull();
+
+    List<WebLog> findAllByParamsIn(Date startTime, Date endTime, String uri, String method, String protocol, String userAgent, String ip, String location, String remark);
+
+    List<Map<String, Integer>> selectPvUvByDateBetween(Date startDate, Date endDate);
+
+    int countByDateBetween(Date startTime, Date endTime);
+}

+ 70 - 0
src/main/java/cn/minbb/job/service/impl/WebLogServiceImpl.java

@@ -0,0 +1,70 @@
+package cn.minbb.job.service.impl;
+
+import cn.minbb.job.model.WebLog;
+import cn.minbb.job.model.repository.WebLogRepository;
+import cn.minbb.job.service.WebLogService;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.Predicate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class WebLogServiceImpl implements WebLogService {
+
+    private final WebLogRepository webLogRepository;
+
+    public WebLogServiceImpl(WebLogRepository webLogRepository) {
+        this.webLogRepository = webLogRepository;
+    }
+
+    @Override
+    public WebLog saveOne(WebLog webLog) {
+        return webLogRepository.save(webLog);
+    }
+
+    @Override
+    public WebLog findTop1ByLocationIsNull() {
+        return webLogRepository.findTop1ByLocationIsNull();
+    }
+
+    @Override
+    public List<WebLog> findTop10ByLocationIsNull() {
+        return webLogRepository.findTop10ByLocationIsNull();
+    }
+
+    @Override
+    public List<WebLog> findAllByParamsIn(Date startTime, Date endTime, String uri, String method, String protocol, String userAgent, String ip, String location, String remark) {
+        Specification<WebLog> specification = (root, criteriaQuery, criteriaBuilder) -> {
+            // 一级查询条件列表
+            List<Predicate> predicateList = new ArrayList<>(8);
+            if (null != startTime) predicateList.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createdAt"), startTime));
+            if (null != endTime) predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get("createdAt"), endTime));
+            if (null != uri) predicateList.add(criteriaBuilder.like(root.get("uri"), uri + "%"));
+            if (null != method) predicateList.add(criteriaBuilder.equal(root.get("method"), method));
+            if (null != protocol) predicateList.add(criteriaBuilder.like(root.get("protocol"), protocol + "%"));
+            if (null != userAgent) predicateList.add(criteriaBuilder.like(root.get("userAgent"), userAgent + "%"));
+            if (null != ip) predicateList.add(criteriaBuilder.equal(root.get("ip"), ip));
+            if (null != location) predicateList.add(criteriaBuilder.like(root.get("location"), location + "%"));
+            if (null != remark) predicateList.add(criteriaBuilder.like(root.get("remark"), remark + "%"));
+            criteriaQuery.orderBy(criteriaBuilder.desc(root.get("createdAt")));
+            Predicate[] predicates = new Predicate[predicateList.size()];
+            // AND 拼接一级查询条件列表
+            return criteriaBuilder.and(predicateList.toArray(predicates));
+        };
+        return webLogRepository.findAll(specification);
+    }
+
+    @Override
+    public List<Map<String, Integer>> selectPvUvByDateBetween(Date startDate, Date endDate) {
+        return webLogRepository.selectPvUvByDateBetween(startDate, endDate);
+    }
+
+    @Override
+    public int countByDateBetween(Date startTime, Date endTime) {
+        return webLogRepository.countByDateBetween(startTime, endTime);
+    }
+}

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

@@ -0,0 +1,74 @@
+package cn.minbb.job.system;
+
+import cn.minbb.job.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());
+    }
+}

+ 90 - 0
src/main/java/cn/minbb/job/util/IPUtil.java

@@ -0,0 +1,90 @@
+package cn.minbb.job.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+
+@Component
+@Log4j2
+public class IPUtil {
+
+    private static final String UNKNOWN = "unknown";
+
+    private IPUtil() {
+    }
+
+    /**
+     * 根据 HttpServletRequest 请求获取客户端真实 IP 地址
+     *
+     * @param request HttpServletRequest
+     * @return 真实 IP 地址串
+     */
+    public String getIpAddress(HttpServletRequest request) {
+        String ipAddress;
+        try {
+            // x-forwarded-for
+            ipAddress = request.getHeader("X-Forwarded-For");
+            if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getHeader("Proxy-Client-IP");
+            }
+            if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getHeader("WL-Proxy-Client-IP");
+            }
+            if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getHeader("HTTP_CLIENT_IP");
+            }
+            if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
+            }
+            if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getRemoteAddr();
+                if (ipAddress.equals("127.0.0.1")) {
+                    // 根据网卡取本机配置的 IP
+                    ipAddress = InetAddress.getLocalHost().getHostAddress();
+                }
+            }
+            // 对于通过多个代理的情况,第一个 IP 为客户端真实 IP,多个 IP 按照','分割
+            if (ipAddress != null && ipAddress.length() > 15) {
+                // "***.***.***.***".length() = 15
+                int index = ipAddress.indexOf(',');
+                if (index > 0) {
+                    ipAddress = ipAddress.substring(0, index);
+                }
+            }
+        } catch (Exception e) {
+            ipAddress = "";
+            log.info("{} - {}", e.getCause(), e.getMessage());
+        }
+        return ipAddress;
+    }
+
+    /**
+     * 太平洋网络 IP 地址查询接口
+     * 不存在时间查询间隔
+     *
+     * @param ip IP 地址
+     * @return 位置信息和运营商
+     */
+    public String getIPLocationByTPY(String ip) {
+        String address = null;
+        try {
+            String url = "http://whois.pconline.com.cn/ipJson.jsp?ip={1}&json=true";
+            ResponseEntity<String> entity = new RestTemplate().getForEntity(url, String.class, ip);
+            if (entity.getStatusCode() == HttpStatus.OK) {
+                String body = entity.getBody();
+                JSONObject json = JSON.parseObject(null == body ? "{}" : body.trim());
+                address = json.getString("addr");
+            }
+        } catch (Exception e) {
+            log.error("查询 IP 地址异常,{}", e.getMessage());
+        }
+        return address;
+    }
+}

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

@@ -1,10 +1,10 @@
 ## Éú²ú»·¾³ ##
 ## ×¢²áÖÐÐÄ
-eureka.instance.ip-address=
+eureka.instance.ip-address=132.232.30.126
 eureka.client.service-url.defaultZone=${EUREKA_SERVICE_URL:http://admin:yumin1997@49.232.30.218:8000}/eureka/,${EUREKA_SERVICE_URL:http://admin:yumin1997@132.232.30.126:8000}/eureka/
 ## MySQL
 spring.datasource.url=jdbc:mysql://cdb-7yzyxn36.bj.tencentcdb.com:10047/job?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
 spring.datasource.username=job
-spring.datasource.password=
-public-key=
-private-key=
+spring.datasource.password=C2ElMqM3flW6F0JM5wuSu09PDgFQtx8ERoBYGtiOml7HhkKrhJ2G9E9+cDE9D92NVhqIeWZP88TMeLpynBSXTQ==
+public-key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKr1T9SuTMVJkDELRZ1Xr39l7ldugd6O0wNnQqt8jj4pgkC1mUwYWJI5a5O3aT1VYryZYChx54qnaNBExPeeZGECAwEAAQ==
+private-key=MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAqvVP1K5MxUmQMQtFnVevf2XuV26B3o7TA2dCq3yOPimCQLWZTBhYkjlrk7dpPVVivJlgKHHniqdo0ETE955kYQIDAQABAkEAqFraGezoB2UxTUr3bEJ8vm4H8CwQUra+ENzwp+YKQiWMWdNEsCQGhTKq0n1nHFjGnrl4v9CAJLOd80uuagazEQIhANTMVMl0+uFoikEslRVxHQ0BooNoKpA9jcLGdBl09kYdAiEAzapzyTjutGPpwIy6g+XMDxRPvGX+eZBVvGX7/odD9BUCIFLm9/E0PbkrPRF/COW9l4/Fn0aKgmqHUH8dSYXRZ1CRAiA2se8LA5YP6UZgC80KiNGE7RzreKB9idITQXgLz8NVpQIhAL24p0tMfvYfKz8QFli9ORtqFTCntL8BElH+Gov7JAhU

+ 2 - 2
src/main/resources/application.properties

@@ -5,8 +5,8 @@ server.servlet.context-path=/
 server.forward-headers-strategy=native
 #server.tomcat.protocol-header=x-forwarded-proto
 #server.servlet.session.cookie.name=SESSION_JOB
-spring.application.name=APP
-spring.jmx.default-domain=app
+spring.application.name=JOB
+spring.jmx.default-domain=job
 spring.main.allow-bean-definition-overriding=true
 spring.main.banner-mode=log
 spring.http.encoding.charset=UTF-8

+ 22 - 0
src/main/resources/static/css/style.css

@@ -0,0 +1,22 @@
+/* 页脚 */
+footer a {
+    color: #EEEEEE;
+}
+
+footer a:hover {
+    color: lightgray;
+    text-decoration: none;
+}
+
+footer ul {
+    padding-left: 0;
+}
+
+footer ul li {
+    font-size: 14px;
+    list-style: none;
+}
+
+footer p {
+    font-size: 14px;
+}

+ 108 - 0
src/main/resources/templates/about.html

@@ -0,0 +1,108 @@
+<!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>关于 - [[${APP_NAME}]]</title>
+
+    <style>
+        .marketing h2 {
+            margin: 12px 0 16px;
+        }
+    </style>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="jumbotron">
+        <div class="container">
+            <h1 class="display-3">[[${APP_NAME}]]</h1>
+            <p>基于学校的招聘资源打造求职招聘平台,为即将毕业的学生提供一个稳定,安全,规范的求职渠道,学校可让长期合作的企业在此投放招聘信息供学生选择投递简历,并为他们提供最新的求职招聘资讯,有效的提供良好的服务。 - 来自 <code class="highlighter-rouge">[[${APP_NAME}]].</code></p>
+            <hr class="my-4">
+            <p>本系统采用 B/S 结构。</p>
+            <p>
+                <a class="btn btn-primary btn-lg" href="/" role="button">立即体验 &raquo;</a>
+            </p>
+        </div>
+    </div>
+
+    <div class="container marketing">
+        <div class="row text-center">
+            <div class="col-md-4">
+                <img class="rounded-circle" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Generic placeholder image" width="140"
+                     height="140">
+                <h2>Spring Cloud</h2>
+                <p>工程采用企业级开发框架 Spring Boot 进行开发,同时整合企业级安全 Spring Security 和分布式云计算解决方案 Spring Cloud,接入服务发现与注册中心 Eureka 和健康监控管理中心 Admin,由统一的服务管理进行治理,为用户提供安全、稳定、可靠的互联网服务。</p>
+                <p><a class="btn btn-secondary" href="#" role="button">查看详情 »</a></p>
+            </div>
+            <div class="col-md-4">
+                <img class="rounded-circle" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Generic placeholder image" width="140"
+                     height="140">
+                <h2>MySQL</h2>
+                <p>使用云 MySQL 关系型数据库进行数据存储。在 Web 应用方面,MySQL 是最好的 RDBMS 关系型数据库应用软件之一,可以保证数据存取速度和灵活性,同时保证数据的安全性。</p>
+                <p><a class="btn btn-secondary" href="#" role="button">查看详情 »</a></p>
+            </div>
+            <div class="col-md-4">
+                <img class="rounded-circle" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Generic placeholder image" width="140"
+                     height="140">
+                <h2>Undertow</h2>
+                <p>基于 NIO 的行业高性能、高并发轻量级 Web 嵌入式应用服务器,在运行时更加节省内存,NIO 非阻塞 IO 在处理 I/O 请求上更加占有优势,性能较高,便于配置,可以长期稳定的提供 Web 端资源请求和服务。</p>
+                <p><a class="btn btn-secondary" href="#" role="button">查看详情 »</a></p>
+            </div>
+        </div>
+
+        <hr class="featurette-divider">
+
+        <div class="row featurette">
+            <div class="col-md-7">
+                <h2 class="featurette-heading">First featurette heading. <span class="text-muted">It'll blow your mind.</span></h2>
+                <p class="lead">Donec ullamcorper nulla non metus auctor fringilla. Vestibulum id ligula porta felis euismod semper. Praesent commodo cursus magna, vel scelerisque
+                    nisl consectetur. Fusce dapibus, tellus ac cursus commodo.</p>
+            </div>
+            <div class="col-md-5">
+                <img class="featurette-image img-fluid mx-auto" data-src="holder.js/500x250/auto" alt="500x250" style="width: 500px; height: 250px;"
+                     src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22500%22%20height%3D%22500%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20500%20500%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_171a4f04766%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A25pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_171a4f04766%22%3E%3Crect%20width%3D%22500%22%20height%3D%22500%22%20fill%3D%22%23EEEEEE%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22185.1171875%22%20y%3D%22261.1%22%3E500x500%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E"
+                     data-holder-rendered="true">
+            </div>
+        </div>
+
+        <hr class="featurette-divider">
+
+        <div class="row featurette">
+            <div class="col-md-7 order-md-2">
+                <h2 class="featurette-heading">Oh yeah, it's that good. <span class="text-muted">See for yourself.</span></h2>
+                <p class="lead">Donec ullamcorper nulla non metus auctor fringilla. Vestibulum id ligula porta felis euismod semper. Praesent commodo cursus magna, vel scelerisque
+                    nisl consectetur. Fusce dapibus, tellus ac cursus commodo.</p>
+            </div>
+            <div class="col-md-5 order-md-1">
+                <img class="featurette-image img-fluid mx-auto" data-src="holder.js/500x250/auto" alt="500x250"
+                     src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22500%22%20height%3D%22500%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20500%20500%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_171a4f04768%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A25pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_171a4f04768%22%3E%3Crect%20width%3D%22500%22%20height%3D%22500%22%20fill%3D%22%23EEEEEE%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22185.1171875%22%20y%3D%22261.1%22%3E500x500%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E"
+                     data-holder-rendered="true" style="width: 500px; height: 250px;">
+            </div>
+        </div>
+
+        <hr class="featurette-divider">
+
+        <div class="row featurette">
+            <div class="col-md-7">
+                <h2 class="featurette-heading">And lastly, this one. <span class="text-muted">Checkmate.</span></h2>
+                <p class="lead">Donec ullamcorper nulla non metus auctor fringilla. Vestibulum id ligula porta felis euismod semper. Praesent commodo cursus magna, vel scelerisque
+                    nisl consectetur. Fusce dapibus, tellus ac cursus commodo.</p>
+            </div>
+            <div class="col-md-5">
+                <img class="featurette-image img-fluid mx-auto" data-src="holder.js/500x250/auto" alt="500x250"
+                     src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22500%22%20height%3D%22500%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20500%20500%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_171a4f0476a%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A25pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_171a4f0476a%22%3E%3Crect%20width%3D%22500%22%20height%3D%22500%22%20fill%3D%22%23EEEEEE%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22185.1171875%22%20y%3D%22261.1%22%3E500x500%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E"
+                     data-holder-rendered="true" style="width: 500px; height: 250px;">
+            </div>
+        </div>
+
+        <hr class="featurette-divider">
+    </div>
+
+    <div class="container" style="margin-top: 24px;">
+        更多介绍...
+    </div>
+</th:block>
+</body>
+</html>

+ 15 - 40
src/main/resources/templates/fragments/footer.html

@@ -5,61 +5,36 @@
     <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"/>
-
-    <style>
-        footer a {
-            color: #EEEEEE;
-        }
-
-        footer a:hover {
-            color: lightgray;
-            text-decoration: none;
-        }
-
-        footer ul {
-            padding-left: 0;
-        }
-
-        footer ul li {
-            font-size: 14px;
-            list-style: none;
-        }
-
-        footer p {
-            font-size: 14px;
-        }
-    </style>
+    <link href="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/css/bootstrap-material-design.css" rel="stylesheet"/>
+    <link href="../static/css/style.css" th:href="@{/css/style.css}" rel="stylesheet"/>
 </head>
 <body>
 <footer th:fragment="footer">
     <div class="container-fluid text-white">
         <div class="row" style="background-color: #444444; padding: 24px 48px;">
-            <div class="col col-xs-12 col-sm-12 col-md-4 col-lg-4">
-                <h4 class="white-text">页脚内容</h4>
-                <p>你可以用行和列来组织你的页脚内容。</p>
+            <div class="col-xs-12 col-sm-12 col-md-4 col-lg-4">
+                <h4 class="white-text">校园招聘网</h4>
+                <p>基于学校的招聘资源打造求职招聘平台,为即将毕业的学生提供一个稳定,安全,规范的求职渠道。</p>
+                <p>下一个,就是你!</p>
             </div>
-            <div class="col col-xs-6 col-sm-6 col-md-3 col-lg-3 offset-md-1 offset-lg-1">
+            <div class="col col-xs-12 col-sm-6 col-md-3 col-lg-3 offset-md-1 offset-lg-1">
                 <h5>链接</h5>
                 <ul>
-                    <li><a class="#" href="#!">链接 1</a></li>
-                    <li><a class="#" href="#!">链接 2</a></li>
-                    <li><a class="#" href="#!">链接 3</a></li>
-                    <li><a class="#" href="#!">链接 4</a></li>
+                    <li><a class="" href="/about">关于</a></li>
+                    <li><a class="" href="/">反馈建议</a></li>
                 </ul>
             </div>
-            <div class="col col-xs-6 col-sm-6 col-md-3 col-lg-3 offset-md-1 offset-lg-1">
+            <div class="col col-xs-12 col-sm-6 col-md-3 col-lg-3 offset-md-1 offset-lg-1">
                 <h5>链接</h5>
                 <ul>
-                    <li><a class="#" href="#!">链接 1</a></li>
-                    <li><a class="#" href="#!">链接 2</a></li>
-                    <li><a class="#" href="#!">链接 3</a></li>
-                    <li><a class="#" href="#!">链接 4</a></li>
+                    <li><a class="" href="/">招商引资</a></li>
+                    <li><a class="" href="/">加入我们</a></li>
                 </ul>
             </div>
         </div>
-        <div class="row" style="background-color: #111111; padding: 16px;">
-            <span class="text-muted">&copy; 2019 <a href="/"> 招聘</a></span>
+        <div class="row" style="background-color: #111111; padding: 12px 16px;">
+            <span>&copy; 2020 <a href="/"> 校园招聘网</a></span>
+            <div class="col text-right" style="padding: 0;"><a href="/">豫ICP备16029895号-6</a></div>
         </div>
     </div>
 </footer>

+ 8 - 9
src/main/resources/templates/fragments/header.html

@@ -5,12 +5,12 @@
     <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"/>
+    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
+    <link href="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/css/bootstrap-material-design.min.css" rel="stylesheet"/>
 </head>
 <body>
 <nav class="navbar navbar-expand-lg navbar-light fixed-top bg-light" th:fragment="header">
-    <a class="navbar-brand" href="/">招聘</a>
+    <a class="navbar-brand" href="/" style="padding: 4px 16px;">校园招聘</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>
@@ -26,8 +26,8 @@
             </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>">
-                    下载二维码
+                   title="<img src='https://files.minbb.cn/images/QQGroup.jpg' width='160' alt='校招QQ群二维码'/><br/><p>群号: 496061797</p>">
+                    校招QQ群
                 </a>
             </li>
         </ul>
@@ -46,7 +46,6 @@
                 </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>
@@ -59,9 +58,9 @@
     </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 src="https://files.minbb.cn/plugins/jquery/3.4.0/jquery.js"></script>
+<script src="https://files.minbb.cn/plugins/popper/1.12.6/popper.js"></script>
+<script src="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/js/bootstrap-material-design.js"></script>
 <script>
     $(document).ready(function () {
     });

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

@@ -5,14 +5,14 @@
       layout:decorate="~{layouts/layout}">
 <head>
     <meta charset="UTF-8"/>
-    <title>基于Java的校园智能求职招聘平台的设计与实现</title>
+    <title>首页 - [[${APP_NAME}]]</title>
 </head>
 <body>
 <th:block layout:fragment="content">
     <h5>基于Java的校园智能求职招聘平台的设计与实现</h5>
 
     <div class="container" style="margin-top: 24px;">
-        123
+        待实现...
     </div>
 </th:block>
 </body>

+ 18 - 7
src/main/resources/templates/layouts/layout.html

@@ -8,8 +8,9 @@
     <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"/>
+    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
+    <link href="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link href="../static/css/style.css" th:href="@{/css/style.css}" rel="stylesheet"/>
     <style>
         html {
             position: relative;
@@ -19,12 +20,22 @@
         a:hover {
             text-decoration: none;
         }
+
+        body {
+            display: flex;
+            min-height: 100vh;
+            flex-direction: column;
+        }
+
+        main {
+            flex: 1 0 auto;
+        }
     </style>
 </head>
-<body style="margin-bottom: 60px;">
+<body style="margin-bottom: 0;">
 <header th:replace="~{fragments/header :: header}"></header>
 
-<main style="margin-top: 57px; margin-bottom: 96px;">
+<main style="margin: 56px 0;">
     <div layout:fragment="content">
         <p>内容</p>
     </div>
@@ -32,9 +43,9 @@
 
 <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 src="https://files.minbb.cn/plugins/jquery/3.4.0/jquery.js"></script>
+<script src="https://files.minbb.cn/plugins/popper/1.12.6/popper.js"></script>
+<script src="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/js/bootstrap-material-design.js"></script>
 <script>
     $(document).ready(function () {
         $('body').bootstrapMaterialDesign();

+ 3 - 3
src/main/resources/templates/sign-in.html

@@ -7,8 +7,8 @@
     <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}"/>
+    <link href="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link href="../static/css/user-sign.css" th:href="@{/css/user-sign.css}" rel="stylesheet"/>
 </head>
 <body>
 <form class="form-login" method="post" action="/login">
@@ -38,7 +38,7 @@
         <a href="#password-modal" data-toggle="modal">忘记密码</a>
     </div>
     <button class="btn btn-lg btn-success btn-block" type="submit">登录</button>
-    <p class="text-center mt-5 mb-3 text-muted">&copy; 2020 招聘</p>
+    <p class="text-center mt-5 mb-3 text-muted">&copy; 2020 校园招聘</p>
 </form>
 
 <!-- Modal -->

+ 3 - 3
src/main/resources/templates/sign-up.html

@@ -8,8 +8,8 @@
     <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}"/>
+    <link href="https://files.minbb.cn/plugins/bootstrap-material-design/4.1.1/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link href="../static/css/user-sign.css" th:href="@{/css/user-sign.css}" rel="stylesheet"/>
 
     <style>
         .form-label-group {
@@ -46,7 +46,7 @@
 
     <div class="text-center mb-3"><a href="/login">我有账号? 去登录</a></div>
     <button class="btn btn-lg btn-success btn-block" type="button" onclick="return submit();">注册</button>
-    <p class="text-center mt-5 mb-3 text-muted">&copy; 2019 招聘</p>
+    <p class="text-center mt-5 mb-3 text-muted">&copy; 2020 校园招聘网</p>
 </div>
 
 <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>