├── src ├── main │ ├── resources │ │ ├── sensitive-words.txt │ │ ├── static │ │ │ ├── css │ │ │ │ ├── login.css │ │ │ │ ├── letter.css │ │ │ │ ├── discuss-detail.css │ │ │ │ └── global.css │ │ │ ├── img │ │ │ │ ├── 404.png │ │ │ │ ├── error.png │ │ │ │ └── captcha.png │ │ │ ├── js │ │ │ │ ├── register.js │ │ │ │ ├── letter.js │ │ │ │ ├── profile.js │ │ │ │ ├── index.js │ │ │ │ ├── setting.js │ │ │ │ ├── global.js │ │ │ │ └── discuss.js │ │ │ └── html │ │ │ │ └── student.html │ │ ├── document │ │ │ └── image │ │ │ │ └── index.jpg │ │ ├── application-develop.properties │ │ ├── application-produce.properties │ │ ├── application.properties │ │ ├── templates │ │ │ ├── demo │ │ │ │ └── view.html │ │ │ ├── mail │ │ │ │ ├── demo.html │ │ │ │ ├── forget.html │ │ │ │ └── activation.html │ │ │ └── error │ │ │ │ ├── 404.html │ │ │ │ └── 500.html │ │ ├── mapper │ │ │ ├── comment-mapper.xml │ │ │ ├── user-mapper.xml │ │ │ ├── discusspost-mapper.xml │ │ │ └── message-mapper.xml │ │ ├── logback-spring-produce.xml │ │ ├── logback-spring.xml │ │ └── logback-spring-develop.xml │ └── java │ │ └── com │ │ └── nowcoder │ │ └── community │ │ ├── dao │ │ ├── AlphaDao.java │ │ ├── AlphaDaoHibernateImpl.java │ │ ├── AlphaDaoMyBatisImpl.java │ │ ├── CommentMapper.java │ │ ├── elasticsearch │ │ │ └── DiscussPostRepository.java │ │ ├── UserMapper.java │ │ ├── DiscussPostMapper.java │ │ ├── LoginTicketMapper.java │ │ └── MessageMapper.java │ │ ├── config │ │ ├── AlphaConfig.java │ │ ├── ThreadPoolConfig.java │ │ ├── WkConfig.java │ │ ├── RedisConfig.java │ │ ├── KaptchaConfig.java │ │ ├── WebMvcConfig.java │ │ ├── QuartzConfig.java │ │ └── SecurityConfig.java │ │ ├── annotation │ │ └── LoginRequired.java │ │ ├── util │ │ ├── HostHolder.java │ │ ├── CookieUtil.java │ │ ├── MailClient.java │ │ ├── CommunityUtil.java │ │ ├── CommunityConstant.java │ │ ├── RedisKeyUtil.java │ │ └── SensitiveFilter.java │ │ ├── quartz │ │ ├── AlphaJob.java │ │ └── PostScoreRefreshJob.java │ │ ├── CommunityApplication.java │ │ ├── CommunityServletInitializer.java │ │ ├── event │ │ └── EventProducer.java │ │ ├── actuator │ │ └── DatabaseEndpoint.java │ │ ├── controller │ │ ├── interceptor │ │ │ ├── AlphaInterceptor.java │ │ │ ├── DataInterceptor.java │ │ │ ├── LoginRequiredInterceptor.java │ │ │ ├── MessageInterceptor.java │ │ │ └── LoginTicketInterceptor.java │ │ ├── advice │ │ │ └── ExceptionAdvice.java │ │ ├── DataController.java │ │ ├── SearchController.java │ │ ├── HomeController.java │ │ ├── LikeController.java │ │ ├── CommentController.java │ │ ├── ShareController.java │ │ └── FollowController.java │ │ ├── entity │ │ ├── LoginTicket.java │ │ ├── Event.java │ │ ├── Message.java │ │ ├── Page.java │ │ ├── Comment.java │ │ ├── User.java │ │ └── DiscussPost.java │ │ ├── aspect │ │ └── ServiceLogAspect.java │ │ └── service │ │ ├── CommentService.java │ │ ├── MessageService.java │ │ ├── LikeService.java │ │ ├── DataService.java │ │ ├── AlphaService.java │ │ └── DiscussPostService.java └── test │ └── java │ └── com │ └── nowcoder │ └── community │ ├── LoggerTests.java │ ├── QuartzTests.java │ ├── MailTests.java │ ├── KafkaTests.java │ ├── CaffeineTests.java │ ├── SpringBootTests.java │ ├── CommunityApplicationTests.java │ ├── ThreadPoolTests.java │ ├── MapperTests.java │ └── RedisTest.java ├── .images ├── arc.png ├── index.png └── message.png ├── .gitignore ├── README.md ├── sql └── init_schema.sql └── pom.xml /src/main/resources/sensitive-words.txt: -------------------------------------------------------------------------------- 1 | 赌博 2 | 嫖娼 3 | 吸毒 4 | 开票 -------------------------------------------------------------------------------- /.images/arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/.images/arc.png -------------------------------------------------------------------------------- /.images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/.images/index.png -------------------------------------------------------------------------------- /.images/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/.images/message.png -------------------------------------------------------------------------------- /src/main/resources/static/css/login.css: -------------------------------------------------------------------------------- 1 | .main .container { 2 | width: 720px; 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/static/img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/src/main/resources/static/img/404.png -------------------------------------------------------------------------------- /src/main/resources/static/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/src/main/resources/static/img/error.png -------------------------------------------------------------------------------- /src/main/resources/static/img/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/src/main/resources/static/img/captcha.png -------------------------------------------------------------------------------- /src/main/resources/document/image/index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/src/main/resources/document/image/index.jpg -------------------------------------------------------------------------------- /src/main/resources/application-develop.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/src/main/resources/application-develop.properties -------------------------------------------------------------------------------- /src/main/resources/application-produce.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosen1024/community/HEAD/src/main/resources/application-produce.properties -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/AlphaDao.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | public interface AlphaDao { 4 | 5 | String select(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #profile 2 | spring.profiles.active=develop 3 | 4 | # logback 5 | logging.cofig=classpath:logback-spring-${spring.profiles.active}.xml -------------------------------------------------------------------------------- /src/main/resources/static/css/letter.css: -------------------------------------------------------------------------------- 1 | .main .nav .badge { 2 | position: absolute; 3 | top: -3px; 4 | left: 68px; 5 | } 6 | 7 | .main .media .badge { 8 | position: absolute; 9 | top: 12px; 10 | left: -3px; 11 | } 12 | 13 | .toast { 14 | max-width: 100%; 15 | width: 80%; 16 | } -------------------------------------------------------------------------------- /src/main/resources/templates/demo/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Teacher 6 | 7 | 8 |

9 |

10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 邮件示例 6 | 7 | 8 |

欢迎你, !

9 | 10 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/AlphaDaoHibernateImpl.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import org.springframework.stereotype.Repository; 4 | 5 | @Repository("alphaHibernate") 6 | public class AlphaDaoHibernateImpl implements AlphaDao { 7 | @Override 8 | public String select() { 9 | return "Hibernate"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/static/css/discuss-detail.css: -------------------------------------------------------------------------------- 1 | .content { 2 | font-size: 16px; 3 | line-height: 2em; 4 | } 5 | 6 | .replyform textarea { 7 | width: 100%; 8 | height: 200px; 9 | } 10 | 11 | .floor { 12 | background: #dcdadc; 13 | padding: 4px 12px; 14 | border-radius: 3px; 15 | font-size: 14px; 16 | } 17 | 18 | .input-size { 19 | width: 100%; 20 | height: 35px; 21 | } -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/AlphaDaoMyBatisImpl.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import org.springframework.context.annotation.Primary; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | @Primary 8 | public class AlphaDaoMyBatisImpl implements AlphaDao{ 9 | @Override 10 | public String select() { 11 | return "MyBatis"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | /nbproject/private/ 20 | /nbbuild/ 21 | /dist/ 22 | /nbdist/ 23 | /.nb-gradle/ 24 | /build/ 25 | 26 | ### VS Code ### 27 | .vscode/ 28 | -------------------------------------------------------------------------------- /src/main/resources/static/js/register.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("form").submit(check_data); 3 | $("input").focus(clear_error); 4 | }); 5 | 6 | function check_data() { 7 | var pwd1 = $("#password").val(); 8 | var pwd2 = $("#confirm-password").val(); 9 | if(pwd1 != pwd2) { 10 | $("#confirm-password").addClass("is-invalid"); 11 | return false; 12 | } 13 | return true; 14 | } 15 | 16 | function clear_error() { 17 | $(this).removeClass("is-invalid"); 18 | } -------------------------------------------------------------------------------- /src/main/resources/templates/mail/forget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 牛客网-忘记密码 7 | 8 | 9 |
10 |

11 | xxx@xxx.com, 您好! 12 |

13 |

14 | 您正在找回牛客账号的密码, 本次操作的验证码为 u5s6dt , 15 | 有效时间5分钟, 请您及时进行操作! 16 |

17 |
18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/AlphaConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import java.text.SimpleDateFormat; 7 | 8 | @Configuration 9 | public class AlphaConfig { 10 | 11 | @Bean 12 | public SimpleDateFormat simpleDateFormat() { 13 | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/activation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 牛客网-激活账号 7 | 8 | 9 |
10 |

11 | xxx@xxx.com, 您好! 12 |

13 |

14 | 您正在注册牛客网, 这是一封激活邮件, 请点击 15 | 此链接, 16 | 激活您的牛客账号! 17 |

18 |
19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/static/html/student.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 增加学生 6 | 7 | 8 | 9 |
10 |

11 | 姓名: 12 |

13 |

14 | 年龄: 15 |

16 |

17 | 18 |

19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import com.nowcoder.community.entity.Comment; 4 | import org.apache.ibatis.annotations.Mapper; 5 | 6 | import java.util.List; 7 | 8 | @Mapper 9 | public interface CommentMapper { 10 | 11 | List selectCommentsByEntity(int entityType, int entityId, int offset, int limit); 12 | 13 | int selectCountByEntity(int entityType, int entityId); 14 | 15 | int insertComment(Comment comment); 16 | 17 | Comment selectCommentById(int id); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/annotation/LoginRequired.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author coolsen 10 | * @version 1.0.0 11 | * @ClassName LoginRequired.java 12 | * @Description 拦截器注解 13 | * @createTime 4/30/2020 7:24 PM 14 | */ 15 | 16 | @Target(ElementType.METHOD) 17 | @Retention(RetentionPolicy.RUNTIME) 18 | public @interface LoginRequired { 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/ThreadPoolConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.EnableAsync; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | /** 8 | * @author coolsen 9 | * @version 1.0.0 10 | * @ClassName ThreadPoolConfig.java 11 | * @Description Spring 线程池 Config 12 | * @createTime 2020/5/21 10:21 13 | */ 14 | 15 | @Configuration 16 | @EnableScheduling 17 | @EnableAsync 18 | public class ThreadPoolConfig { 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/HostHolder.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | import com.nowcoder.community.entity.User; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * 持有用户信息,用于代替session对象. 8 | */ 9 | @Component 10 | public class HostHolder { 11 | 12 | private ThreadLocal users = new ThreadLocal<>(); 13 | 14 | public void setUser(User user) { 15 | users.set(user); 16 | } 17 | 18 | public User getUser() { 19 | return users.get(); 20 | } 21 | 22 | public void clear() { 23 | users.remove(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/elasticsearch/DiscussPostRepository.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao.elasticsearch; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * @author coolsen 9 | * @version 1.0.0 10 | * @ClassName DiscussPostRepository.java 11 | * @Description DiscussPost Repository 12 | * @createTime 2020/5/19 14:14 13 | */ 14 | 15 | @Repository 16 | public interface DiscussPostRepository extends ElasticsearchRepository { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/quartz/AlphaJob.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.quartz; 2 | 3 | import org.quartz.Job; 4 | import org.quartz.JobExecutionContext; 5 | import org.quartz.JobExecutionException; 6 | 7 | /** 8 | * @author coolsen 9 | * @version 1.0.0 10 | * @ClassName AlphaJob.java 11 | * @Description demo job 12 | * @createTime 2020/5/21 10:44 13 | */ 14 | public class AlphaJob implements Job { 15 | @Override 16 | public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { 17 | System.out.println(Thread.currentThread().getName() + ": execute a quartz job."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import com.nowcoder.community.entity.User; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.springframework.stereotype.Repository; 6 | 7 | 8 | @Mapper 9 | public interface UserMapper { 10 | 11 | User selectById(int id); 12 | 13 | User selectByName(String username); 14 | 15 | User selectByEmail(String email); 16 | 17 | int insertUser(User user); 18 | 19 | int updateStatus(int id, int status); 20 | 21 | int updateHeader(int id, String headerUrl); 22 | 23 | int updatePassword(int id, String password); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/CommunityApplication.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | import javax.annotation.PostConstruct; 7 | 8 | @SpringBootApplication 9 | public class CommunityApplication { 10 | 11 | @PostConstruct 12 | public void init(){ 13 | // 解决netty启动冲突问题 14 | // see Netty4Utils.setAvailableProcessors() 15 | System.setProperty("es.set.netty.runtime.available.processors", "false"); 16 | } 17 | public static void main(String[] args) { 18 | SpringApplication.run(CommunityApplication.class, args); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/CommunityServletInitializer.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | /** 7 | * @author coolsen 8 | * @version 1.0.0 9 | * @ClassName CommunityServeltInitalizer.java 10 | * @Description 自定义tomcat 启动入口 11 | * @createTime 2020/6/25 20:39 12 | */ 13 | public class CommunityServletInitializer extends SpringBootServletInitializer { 14 | 15 | @Override 16 | protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { 17 | return builder.sources(CommunityApplication.class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | import javax.servlet.http.Cookie; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | public class CookieUtil { 7 | 8 | public static String getValue(HttpServletRequest request, String name) { 9 | if (request == null || name == null) { 10 | throw new IllegalArgumentException("参数为空!"); 11 | } 12 | 13 | Cookie[] cookies = request.getCookies(); 14 | if (cookies != null) { 15 | for (Cookie cookie : cookies) { 16 | if (cookie.getName().equals(name)) { 17 | return cookie.getValue(); 18 | } 19 | } 20 | } 21 | 22 | return null; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/static/js/letter.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#sendBtn").click(send_letter); 3 | $(".close").click(delete_msg); 4 | }); 5 | 6 | function send_letter() { 7 | $("#sendModal").modal("hide"); 8 | 9 | var toName = $("#recipient-name").val(); 10 | var content = $("#message-text").val(); 11 | $.post( 12 | CONTEXT_PATH + "/letter/send", 13 | {"toName":toName,"content":content}, 14 | function(data) { 15 | data = $.parseJSON(data); 16 | if(data.code == 0) { 17 | $("#hintBody").text("发送成功!"); 18 | } else { 19 | $("#hintBody").text(data.msg); 20 | } 21 | 22 | $("#hintModal").modal("show"); 23 | setTimeout(function(){ 24 | $("#hintModal").modal("hide"); 25 | location.reload(); 26 | }, 2000); 27 | } 28 | ); 29 | } 30 | 31 | function delete_msg() { 32 | // TODO 删除数据 33 | $(this).parents(".media").remove(); 34 | } -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/event/EventProducer.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.nowcoder.community.entity.Event; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.kafka.core.KafkaTemplate; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * @author coolsen 11 | * @version 1.0.0 12 | * @ClassName EventProducer.java 13 | * @Description 事件生产者 14 | * @createTime 2020/5/14 21:32 15 | */ 16 | 17 | @Component 18 | public class EventProducer { 19 | 20 | @Autowired 21 | private KafkaTemplate kafkaTemplate; 22 | 23 | // 处理事件 24 | public void fireEvent(Event event){ 25 | // 将事件发送到指定主题,其中把内容转换为json对象,消费者受到json后,可以将json转换成Event 26 | kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/DiscussPostMapper.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface DiscussPostMapper { 11 | 12 | List selectDiscussPosts(int userId, int offset, int limit,int orderMode); 13 | 14 | // @Param注解用于给参数取别名, 15 | // 如果只有一个参数,并且在里使用,则必须加别名. 16 | int selectDiscussPostRows(@Param("userId") int userId); 17 | 18 | int insertDiscussPost(DiscussPost discussPost); 19 | 20 | DiscussPost selectDiscussPostById(int id); 21 | 22 | int updateCommentCount(int id, int commentCount); 23 | 24 | int updateType(int id, int type); 25 | 26 | int updateStatus(int id, int status); 27 | 28 | int updateScore(int id, double score); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/static/js/profile.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $(".follow-btn").click(follow); 3 | }); 4 | 5 | function follow() { 6 | var btn = this; 7 | if($(btn).hasClass("btn-info")) { 8 | // 关注TA 9 | $.post( 10 | CONTEXT_PATH + "/follow", 11 | {"entityType":3,"entityId":$(btn).prev().val()}, 12 | function(data) { 13 | data = $.parseJSON(data); 14 | if(data.code == 0) { 15 | window.location.reload(); 16 | } else { 17 | alert(data.msg); 18 | } 19 | } 20 | ); 21 | // $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary"); 22 | } else { 23 | // 取消关注 24 | $.post( 25 | CONTEXT_PATH + "/unfollow", 26 | {"entityType":3,"entityId":$(btn).prev().val()}, 27 | function(data) { 28 | data = $.parseJSON(data); 29 | if(data.code == 0) { 30 | window.location.reload(); 31 | } else { 32 | alert(data.msg); 33 | } 34 | } 35 | ); 36 | //$(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info"); 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/LoggerTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | /** 12 | * @author shuaisen ma 13 | * @version 1.0.0 14 | * @ClassName LoggerTests.java 15 | * @Description Log test 16 | * @createTime 2020年04月28日 13:14:00 17 | */ 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest 20 | @ContextConfiguration(classes = CommunityApplication.class) 21 | public class LoggerTests { 22 | private static final Logger logger= LoggerFactory.getLogger(LoggerTests.class); 23 | 24 | @Test 25 | public void testLogger(){ 26 | logger.debug("debug log"); 27 | logger.info("info log"); 28 | logger.warn("warn log"); 29 | logger.error("error log"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/WkConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import javax.annotation.PostConstruct; 9 | import java.io.File; 10 | 11 | /** 12 | * @author coolsen 13 | * @version 1.0.0 14 | * @ClassName WkConfig.java 15 | * @Description wkhtmltopdf Config,生成图片 16 | * @createTime 2020/5/21 19:43 17 | */ 18 | 19 | @Configuration 20 | public class WkConfig { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(WkConfig.class); 23 | 24 | @Value("${wk.image.storage}") 25 | private String wkImageStorage; 26 | 27 | @PostConstruct 28 | public void init() { 29 | // 创建WK图片目录 30 | File file = new File(wkImageStorage); 31 | if (!file.exists()) { 32 | file.mkdir(); 33 | logger.info("创建WK图片目录: " + wkImageStorage); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/QuartzTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.quartz.JobKey; 6 | import org.quartz.Scheduler; 7 | import org.quartz.SchedulerException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | @RunWith(SpringRunner.class) 14 | @SpringBootTest 15 | @ContextConfiguration(classes = CommunityApplication.class) 16 | public class QuartzTests { 17 | 18 | @Autowired 19 | private Scheduler scheduler; 20 | 21 | // 删除任务 22 | @Test 23 | public void testDeleteJob() { 24 | try { 25 | boolean result = scheduler.deleteJob(new JobKey("alphaJob", "alphaJobGroup")); 26 | System.out.println(result); 27 | } catch (SchedulerException e) { 28 | e.printStackTrace(); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/static/js/index.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#publishBtn").click(publish); 3 | }); 4 | 5 | function publish() { 6 | $("#publishModal").modal("hide"); 7 | 8 | // 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中. 9 | // var token = $("meta[name='_csrf']").attr("content"); 10 | // var header = $("meta[name='_csrf_header']").attr("content"); 11 | // $(document).ajaxSend(function(e, xhr, options){ 12 | // xhr.setRequestHeader(header, token); 13 | // }); 14 | 15 | // 获取标题和内容 16 | var title = $("#recipient-name").val(); 17 | var content = $("#message-text").val(); 18 | // 发送异步请求(POST) 19 | $.post( 20 | CONTEXT_PATH + "/discuss/add", 21 | {"title":title,"content":content}, 22 | function(data) { 23 | data = $.parseJSON(data); 24 | // 在提示框中显示返回消息 25 | $("#hintBody").text(data.msg); 26 | // 显示提示框 27 | $("#hintModal").modal("show"); 28 | // 2秒后,自动隐藏提示框 29 | setTimeout(function(){ 30 | $("#hintModal").modal("hide"); 31 | // 刷新页面 32 | if(data.code == 0) { 33 | window.location.reload(); 34 | } 35 | }, 2000); 36 | } 37 | ); 38 | 39 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/setting.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#uploadForm").submit(upload); 3 | }); 4 | 5 | function upload() { 6 | $.ajax({ 7 | url: "http://upload-z1.qiniup.com", 8 | method: "post", 9 | processData: false, 10 | contentType: false, 11 | data: new FormData($("#uploadForm")[0]), 12 | success: function(data) { 13 | if(data && data.code == 0) { 14 | // 更新头像访问路径 15 | $.post( 16 | CONTEXT_PATH + "/user/header/url", 17 | {"fileName":$("input[name='key']").val()}, 18 | function(data) { 19 | data = $.parseJSON(data); 20 | if(data.code == 0) { 21 | window.location.reload(); 22 | } else { 23 | alert(data.msg); 24 | } 25 | } 26 | ); 27 | } else { 28 | alert("上传失败!"); 29 | } 30 | } 31 | }); 32 | return false; 33 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/global.js: -------------------------------------------------------------------------------- 1 | var CONTEXT_PATH = ""; 2 | 3 | window.alert = function(message) { 4 | if(!$(".alert-box").length) { 5 | $("body").append( 6 | '' 24 | ); 25 | } 26 | 27 | var h = $(".alert-box").height(); 28 | var y = h / 2 - 100; 29 | if(h > 600) y -= 100; 30 | $(".alert-box .modal-dialog").css("margin", (y < 0 ? 0 : y) + "px auto"); 31 | 32 | $(".alert-box .modal-body p").text(message); 33 | $(".alert-box").modal("show"); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/LoginTicketMapper.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import com.nowcoder.community.entity.LoginTicket; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | /** 7 | * @author coolsen 8 | * @version 1.0.0 9 | * @ClassName LoginTicketMapper.java 10 | * @Description 登录mapper,不推荐使用,目前使用redis存储ticket 11 | * @createTime 4/29/2020 6:16 PM 12 | */ 13 | 14 | @Mapper 15 | @Deprecated 16 | public interface LoginTicketMapper { 17 | 18 | @Insert({ 19 | "insert into login_ticket(user_id,ticket,status,expired) ", 20 | "values(#{userId},#{ticket},#{status},#{expired})" 21 | }) 22 | @Options(useGeneratedKeys = true, keyProperty = "id") 23 | int insertLoginTicket(LoginTicket LoginTicket); 24 | 25 | @Select({ 26 | "select id,user_id,ticket,status,expired ", 27 | "from login_ticket where ticket=#{ticket}" 28 | }) 29 | LoginTicket selectByTicket(String ticket); 30 | 31 | @Update({ 32 | "" 38 | }) 39 | int updateStatus(String ticket, int status); 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/MailTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import com.nowcoder.community.util.MailClient; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | import org.thymeleaf.TemplateEngine; 11 | import org.thymeleaf.context.Context; 12 | 13 | @RunWith(SpringRunner.class) 14 | @SpringBootTest 15 | @ContextConfiguration(classes = CommunityApplication.class) 16 | public class MailTests { 17 | 18 | @Autowired 19 | private MailClient mailClient; 20 | 21 | @Autowired 22 | private TemplateEngine templateEngine; 23 | 24 | @Test 25 | public void testTextMail() { 26 | mailClient.sendMail("anotherpony@163.com", "TEST", "Welcome."); 27 | } 28 | 29 | @Test 30 | public void testHtmlMail() { 31 | Context context = new Context(); 32 | context.setVariable("username", "sunday"); 33 | 34 | String content = templateEngine.process("/mail/demo", context); 35 | System.out.println(content); 36 | 37 | mailClient.sendMail("anotherpony@163.com", "HTML", content); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/actuator/DatabaseEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.actuator; 2 | 3 | import com.nowcoder.community.util.CommunityUtil; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 8 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.sql.DataSource; 12 | import java.sql.Connection; 13 | import java.sql.SQLException; 14 | 15 | /** 16 | * @author coolsen 17 | * @version 1.0.0 18 | * @ClassName DatabaseEndpoint.java 19 | * @Description Database Endpoint for actuator 20 | * @createTime 2020/5/22 16:37 21 | */ 22 | 23 | @Component 24 | @Endpoint(id = "database") 25 | public class DatabaseEndpoint { 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(DatabaseEndpoint.class); 28 | 29 | @Autowired 30 | private DataSource dataSource; 31 | 32 | @ReadOperation 33 | public String checkConnection(){ 34 | try ( 35 | Connection conn = dataSource.getConnection(); 36 | ) { 37 | return CommunityUtil.getJSONString(0, "获取连接成功!"); 38 | } catch (SQLException e) { 39 | logger.error("获取连接失败:" + e.getMessage()); 40 | return CommunityUtil.getJSONString(1, "获取连接失败!"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/interceptor/AlphaInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller.interceptor; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.servlet.HandlerInterceptor; 7 | import org.springframework.web.servlet.ModelAndView; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | @Component 13 | public class AlphaInterceptor implements HandlerInterceptor { 14 | 15 | private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class); 16 | 17 | // 在Controller之前执行 18 | @Override 19 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 20 | logger.debug("preHandle: " + handler.toString()); 21 | return true; 22 | } 23 | 24 | // 在Controller之后执行 25 | @Override 26 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 27 | logger.debug("postHandle: " + handler.toString()); 28 | } 29 | 30 | // 在TemplateEngine之后执行 31 | @Override 32 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 33 | logger.debug("afterCompletion: " + handler.toString()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.RedisSerializer; 8 | import org.springframework.data.redis.serializer.StringRedisSerializer; 9 | 10 | /** 11 | * @author coolsen 12 | * @version 1.0.0 13 | * @ClassName RedisConfig.java 14 | * @Description Redis Config 15 | * @createTime 5/9/2020 12:04 PM 16 | */ 17 | 18 | @Configuration 19 | public class RedisConfig { 20 | 21 | @Bean 22 | public RedisTemplate redisTemplate(RedisConnectionFactory factory){ 23 | //参考spring的RedisAutoConfiguration进行修改 24 | RedisTemplate template = new RedisTemplate(); 25 | template.setConnectionFactory(factory); 26 | 27 | // 设置key的序列化方式 28 | template.setKeySerializer(RedisSerializer.string()); 29 | // 设置value的序列化方式 30 | template.setValueSerializer(RedisSerializer.json()); 31 | // 设置hash的key的序列化方式 32 | template.setHashKeySerializer(RedisSerializer.string()); 33 | // 设置hash的value的序列化方式 34 | template.setHashValueSerializer(RedisSerializer.json()); 35 | 36 | template.afterPropertiesSet(); 37 | return template; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/dao/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.dao; 2 | 3 | import com.nowcoder.community.entity.Message; 4 | import org.apache.ibatis.annotations.Mapper; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author coolsen 10 | * @version 1.0.0 11 | * @ClassName MessageMapper.java 12 | * @Description MessageMapper 13 | * @createTime 5/8/2020 7:50 PM 14 | */ 15 | @Mapper 16 | public interface MessageMapper { 17 | 18 | // 查询当前用户的会话列表,针对每个会话只返回一条最新的私信. 19 | List selectConversations(int userId, int offset, int limit); 20 | 21 | // 查询当前用户的会话数量. 22 | int selectConversationCount(int userId); 23 | 24 | // 查询某个会话所包含的私信列表. 25 | List selectLetters(String conversationId, int offset, int limit); 26 | 27 | // 查询某个会话所包含的私信数量. 28 | int selectLetterCount(String conversationId); 29 | 30 | // 查询未读私信的数量 31 | int selectLetterUnreadCount(int userId, String conversationId); 32 | 33 | // 新增消息 34 | int insertMessage(Message message); 35 | 36 | // 修改消息的状态 37 | int updateStatus(List ids, int status); 38 | 39 | // 查询某个主题下最新的通知 40 | Message selectLatestNotice(int userId,String topic); 41 | 42 | // 查询某个主题下包含的通知数量 43 | int selectNoticeCount(int userId,String topic); 44 | 45 | // 查询未读通知的消息数量 46 | int selectNoticeUnreadCount(int userId,String topic); 47 | 48 | // 查询某个主题包含的通知列表 49 | List selectNotices(int userId,String topic,int offset,int limit); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/interceptor/DataInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller.interceptor; 2 | 3 | import ch.qos.logback.core.net.HardenedObjectInputStream; 4 | import com.nowcoder.community.entity.User; 5 | import com.nowcoder.community.service.DataService; 6 | import com.nowcoder.community.util.HostHolder; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | /** 15 | * @author coolsen 16 | * @version 1.0.0 17 | * @ClassName DataInterceptor.java 18 | * @Description 数据统计 Interceptor 19 | * @createTime 2020/5/20 21:35 20 | */ 21 | 22 | @Component 23 | public class DataInterceptor implements HandlerInterceptor { 24 | 25 | @Autowired 26 | private DataService dataService; 27 | 28 | @Autowired 29 | private HostHolder hostHolder; 30 | 31 | 32 | @Override 33 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 34 | // 统计UV 35 | String ip = request.getRemoteHost(); 36 | dataService.recordUV(ip); 37 | 38 | // 统计DAU 39 | User user = hostHolder.getUser(); 40 | if(user!=null){ 41 | dataService.recordDAU(user.getId()); 42 | } 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/KaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import com.google.code.kaptcha.Producer; 4 | import com.google.code.kaptcha.impl.DefaultKaptcha; 5 | import com.google.code.kaptcha.util.Config; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.util.Properties; 10 | 11 | /** 12 | * @author coolsen 13 | * @version 1.0.0 14 | * @ClassName KaptchaConfig.java 15 | * @Description 验证码插件Kaptcha配置 16 | * @createTime 4/29/2020 5:26 PM 17 | */ 18 | @Configuration 19 | public class KaptchaConfig { 20 | 21 | @Bean 22 | public Producer kaptchaProducer(){ 23 | Properties properties=new Properties(); 24 | properties.setProperty("kaptcha.image.width", "100"); 25 | properties.setProperty("kaptcha.image.height", "40"); 26 | properties.setProperty("kaptcha.textproducer.font.size", "32"); 27 | properties.setProperty("kaptcha.textproducer.font.color", "0,0,0"); 28 | properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ"); 29 | properties.setProperty("kaptcha.textproducer.char.length", "4"); 30 | properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); 31 | 32 | DefaultKaptcha kaptcha = new DefaultKaptcha(); 33 | Config config = new Config(properties); 34 | kaptcha.setConfig(config); 35 | return kaptcha; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/MailClient.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.mail.javamail.JavaMailSender; 8 | import org.springframework.mail.javamail.MimeMessageHelper; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.mail.MessagingException; 12 | import javax.mail.internet.MimeMessage; 13 | 14 | /** 15 | * @author coolsen 16 | * @version 1.0.0 17 | * MailClient.java 18 | * @Description 邮箱客户端 19 | * @createTime 4/28/2020 8:36 PM 20 | */ 21 | 22 | @Component 23 | public class MailClient { 24 | private static final Logger logger = LoggerFactory.getLogger(MailClient.class); 25 | 26 | @Autowired 27 | private JavaMailSender mailSender; 28 | 29 | @Value("${spring.mail.username}") 30 | private String from; 31 | 32 | public void sendMail(String to, String subject, String content) { 33 | try { 34 | MimeMessage message = mailSender.createMimeMessage(); 35 | MimeMessageHelper helper = new MimeMessageHelper(message); 36 | helper.setFrom(from); 37 | helper.setTo(to); 38 | helper.setSubject(subject); 39 | helper.setText(content, true); 40 | mailSender.send(helper.getMimeMessage()); 41 | } catch (MessagingException e) { 42 | logger.error("发送邮件失败:" + e.getMessage()); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/mapper/comment-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | id, user_id, entity_type, entity_id, target_id, content, status, create_time 9 | 10 | 11 | 12 | user_id, entity_type, entity_id, target_id, content, status, create_time 13 | 14 | 15 | 24 | 25 | 32 | 33 | 34 | insert into comment() 35 | values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime}) 36 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/interceptor/LoginRequiredInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller.interceptor; 2 | 3 | import com.nowcoder.community.annotation.LoginRequired; 4 | import com.nowcoder.community.util.HostHolder; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.method.HandlerMethod; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.lang.reflect.Method; 13 | 14 | /** 15 | * @author coolsen 16 | * @version 1.0.0 17 | * @ClassName LoginRequiredInterceptor.java 18 | * @Description 登录拦截器 19 | * @createTime 4/30/2020 7:29 PM 20 | */ 21 | @Component 22 | public class LoginRequiredInterceptor implements HandlerInterceptor { 23 | 24 | @Autowired 25 | private HostHolder hostHolder; 26 | 27 | @Override 28 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 29 | // 拦截 Method 30 | if (handler instanceof HandlerMethod) { 31 | HandlerMethod handlerMethod = (HandlerMethod) handler; 32 | Method method = handlerMethod.getMethod(); 33 | LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); 34 | if (loginRequired != null && hostHolder.getUser() == null) { 35 | response.sendRedirect(request.getContextPath() + "/login"); 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/LoginTicket.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author coolsen 7 | * @version 1.0.0 8 | * @ClassName LoginTicket.java 9 | * @Description 登录凭证 10 | * @createTime 4/29/2020 6:14 PM 11 | */ 12 | public class LoginTicket { 13 | private int id; 14 | private int userId; 15 | private String ticket; 16 | private int status; 17 | private Date expired; 18 | 19 | public int getId() { 20 | return id; 21 | } 22 | 23 | public void setId(int id) { 24 | this.id = id; 25 | } 26 | 27 | public int getUserId() { 28 | return userId; 29 | } 30 | 31 | public void setUserId(int userId) { 32 | this.userId = userId; 33 | } 34 | 35 | public String getTicket() { 36 | return ticket; 37 | } 38 | 39 | public void setTicket(String ticket) { 40 | this.ticket = ticket; 41 | } 42 | 43 | public int getStatus() { 44 | return status; 45 | } 46 | 47 | public void setStatus(int status) { 48 | this.status = status; 49 | } 50 | 51 | public Date getExpired() { 52 | return expired; 53 | } 54 | 55 | public void setExpired(Date expired) { 56 | this.expired = expired; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "LoginTicket{" + 62 | "id=" + id + 63 | ", userId=" + userId + 64 | ", ticket='" + ticket + '\'' + 65 | ", status=" + status + 66 | ", expired=" + expired + 67 | '}'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/CommunityUtil.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.util.DigestUtils; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.UUID; 10 | 11 | public class CommunityUtil { 12 | 13 | // 生成随机字符串 14 | public static String generateUUID() { 15 | return UUID.randomUUID().toString().replaceAll("-", ""); 16 | } 17 | 18 | // MD5加密 19 | public static String md5(String key) { 20 | if (StringUtils.isBlank(key)) { 21 | return null; 22 | } 23 | return DigestUtils.md5DigestAsHex(key.getBytes()); 24 | } 25 | 26 | public static String getJSONString(int code, String msg, Map map) { 27 | JSONObject json = new JSONObject(); 28 | json.put("code", code); 29 | json.put("msg", msg); 30 | if (map != null) { 31 | for (String key : map.keySet()) { 32 | json.put(key, map.get(key)); 33 | } 34 | } 35 | return json.toJSONString(); 36 | } 37 | 38 | public static String getJSONString(int code, String msg) { 39 | return getJSONString(code, msg, null); 40 | } 41 | 42 | public static String getJSONString(int code) { 43 | return getJSONString(code, null, null); 44 | } 45 | 46 | public static void main(String[] args) { 47 | Map map = new HashMap<>(); 48 | map.put("name", "zhangsan"); 49 | map.put("age", 25); 50 | System.out.println(getJSONString(0, "ok", map)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/interceptor/MessageInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller.interceptor; 2 | 3 | import com.nowcoder.community.entity.User; 4 | import com.nowcoder.community.service.MessageService; 5 | import com.nowcoder.community.util.HostHolder; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.HashOperations; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | import org.springframework.web.servlet.ModelAndView; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | 16 | /** 17 | * @author coolsen 18 | * @version 1.0.0 19 | * @ClassName MessageInterceptor.java 20 | * @Description 消息拦截器,为了在首页显示未读消息数量 21 | * @createTime 2020/5/15 15:28 22 | */ 23 | 24 | @Component 25 | public class MessageInterceptor implements HandlerInterceptor { 26 | 27 | @Autowired 28 | private HostHolder hostHolder; 29 | 30 | @Autowired 31 | private MessageService messageService; 32 | 33 | @Override 34 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 35 | User user = hostHolder.getUser(); 36 | if (user != null && modelAndView != null) { 37 | int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null); 38 | int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null); 39 | modelAndView.addObject("allUnreadCount", noticeUnreadCount + letterUnreadCount); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目介绍 2 | 一个仿照牛客网实现的讨论社区,不仅实现了基本的注册,登录,发帖,评论,点赞,回复功能,同时使用前缀树实现敏感词过滤,使用wkhtmltopdf生成长图和pdf,实现网站UV和DAU统计,并将用户头像等信息存于七牛云服务器。 3 | ## 其他项目 4 | * 计算机类电子书仓库:https://github.com/cosen1024/awesome-cs-books 5 | * Java面试题仓库:https://github.com/cosen1024/Java-Interview 6 | ## 项目演示 7 | 演示地址: http://coolsen.cn/
8 | 账号密码: aaa/aaa 9 | ## 技术选型 10 | ![](.images/arc.png) 11 | 12 | ## 功能简介 13 | * 使用Spring Security 做权限控制,替代拦截器的拦截控制,并使用自己的认证方案替代Security 认证流程,使权限认证和控制更加方便灵活。 14 | * 使用Redis的set实现点赞,zset实现关注,并使用Redis存储登录ticket和验证码,解决分布式session问题。 15 | * 使用Redis高级数据类型HyperLogLog统计UV(Unique Visitor),使用Bitmap统计DAU(Daily Active User)。 16 | * 使用Kafka处理发送评论、点赞和关注等系统通知,并使用事件进行封装,构建了强大的异步消息系统。 17 | * 使用Elasticsearch做全局搜索,并通过事件封装,增加关键词高亮显示等功能。 18 | * 对热帖排行模块,使用分布式缓存Redis和本地缓存Caffeine作为多级缓存,避免了缓存雪崩,将QPS提升了20倍(10-200),大大提升了网站访问速度。并使用Quartz定时更新热帖排行。 19 | 20 | 21 | ## 开发环境 22 | 23 | | 工具 | 版本号 | 下载 | 24 | | ------------- | ------ | --------------------------------- | 25 | | JDK | 11 | https://openjdk.java.net/install/ | 26 | | Mysql | 5.7 | https://www.mysql.com/ | 27 | | Redis | 3.2 | https://redis.io/download | 28 | | Elasticsearch | 6.4.3 | https://www.elastic.co/downloads | 29 | | Kafka | 2.3.0 | https://kafka.apache.org/downloads | 30 | | nginx | 1.10 | http://nginx.org/en/download.html | 31 | ## 运行效果展示 32 | * 首页 33 | ![](.images/index.png) 34 | * 消息 35 | ![](.images/message.png) 36 | ## 后续更新点 37 | * 增加收藏功能 38 | * 增强对话框功能 39 | ## 相关资源 40 | [相关的教程](https://pan.baidu.com/s/1LjYYwJVsqNBxq69udsXMvA):网盘提取码:wxdd 41 | 42 | 有项目相关、求职等问题,可以加我微信交流,个人微信:kusengo,image-20230427195311976 43 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/advice/ExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller.advice; 2 | 3 | import com.nowcoder.community.util.CommunityUtil; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.io.PrintWriter; 14 | 15 | /** 16 | * @author coolsen 17 | * @version 1.0.0 18 | * @ClassName ExceptionAdvice.java 19 | * @Description 统一处理异常的Controller配置类 20 | * @createTime 5/8/2020 9:34 PM 21 | */ 22 | 23 | @ControllerAdvice(annotations = Controller.class) 24 | public class ExceptionAdvice { 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class); 27 | 28 | @ExceptionHandler({Exception.class}) 29 | public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException { 30 | logger.error("服务器发生异常: " + e.getMessage()); 31 | for (StackTraceElement element : e.getStackTrace()) { 32 | logger.error(element.toString()); 33 | } 34 | 35 | String xRequestedWith = request.getHeader("x-requested-with"); 36 | //处理异步请求的方式 37 | if ("XMLHttpRequest".equals(xRequestedWith)) { 38 | response.setContentType("application/plain;charset=utf-8");//普通文本,浏览器会自动转换为json格式 39 | PrintWriter writer = response.getWriter(); 40 | writer.write(CommunityUtil.getJSONString(1, "服务器异常!")); 41 | } else { 42 | response.sendRedirect(request.getContextPath() + "/error"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/mapper/user-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | username, password, salt, email, type, status, activation_code, header_url, create_time 9 | 10 | 11 | 12 | id, username, password, salt, email, type, status, activation_code, header_url, create_time 13 | 14 | 15 | 20 | 21 | 26 | 27 | 32 | 33 | 34 | insert into user () 35 | values(#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime}) 36 | 37 | 38 | 39 | update user set status = #{status} where id = #{id} 40 | 41 | 42 | 43 | update user set header_url = #{headerUrl} where id = #{id} 44 | 45 | 46 | 47 | update user set password = #{password} where id = #{id} 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/aspect/ServiceLogAspect.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.aspect; 2 | 3 | import org.aspectj.lang.JoinPoint; 4 | import org.aspectj.lang.annotation.Aspect; 5 | import org.aspectj.lang.annotation.Before; 6 | import org.aspectj.lang.annotation.Pointcut; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.context.request.RequestContextHolder; 11 | import org.springframework.web.context.request.ServletRequestAttributes; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import java.text.SimpleDateFormat; 15 | import java.util.Date; 16 | 17 | /** 18 | * @author coolsen 19 | * @version 1.0.0 20 | * @ClassName ServiceLogAspect.java 21 | * @Description 使用AOP记录日志 22 | * @createTime 5/8/2020 10:09 PM 23 | */ 24 | 25 | @Component 26 | @Aspect 27 | public class ServiceLogAspect { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class); 30 | 31 | @Pointcut("execution(* com.nowcoder.community.service.*.*(..))") 32 | public void pointcut() { 33 | 34 | } 35 | 36 | @Before("pointcut()") 37 | public void before(JoinPoint joinPoint) { 38 | // 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()]. 39 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 40 | 41 | //TODO:此处逻辑处理实现简单,需要详细实现。 42 | if(attributes==null){ 43 | return; 44 | } 45 | HttpServletRequest request = attributes.getRequest(); 46 | String ip = request.getRemoteHost(); 47 | String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); 48 | String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); 49 | logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/KafkaTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerRecord; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.kafka.annotation.KafkaHandler; 10 | import org.springframework.kafka.annotation.KafkaListener; 11 | import org.springframework.kafka.core.KafkaTemplate; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | /** 17 | * @author coolsen 18 | * @version 1.0.0 19 | * @ClassName KafkaTests.java 20 | * @Description Kafka Test 21 | * @createTime 2020/5/14 13:14 22 | */ 23 | 24 | @RunWith(SpringRunner.class) 25 | @SpringBootTest 26 | @ContextConfiguration(classes = CommunityApplication.class) 27 | public class KafkaTests { 28 | 29 | @Autowired 30 | private KafkaProducer kafkaProducer; 31 | 32 | @Test 33 | public void testKafka() { 34 | kafkaProducer.sendMessage("test", "你好"); 35 | kafkaProducer.sendMessage("test", "在吗"); 36 | 37 | try { 38 | Thread.sleep(1000 * 10); 39 | } catch (InterruptedException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | 44 | } 45 | 46 | @Component 47 | class KafkaProducer { 48 | 49 | @Autowired 50 | private KafkaTemplate kafkaTemplate; 51 | 52 | public void sendMessage(String topic, String content) { 53 | kafkaTemplate.send(topic, content); 54 | } 55 | 56 | } 57 | 58 | @Component 59 | class KafkaConsumer { 60 | 61 | @KafkaListener(topics = {"test"}) 62 | public void handleMessage(ConsumerRecord record) { 63 | System.out.println(record.value()); 64 | } 65 | 66 | 67 | } -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/CaffeineTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import com.nowcoder.community.service.DiscussPostService; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | import java.util.Date; 13 | 14 | @RunWith(SpringRunner.class) 15 | @SpringBootTest 16 | @ContextConfiguration(classes = CommunityApplication.class) 17 | public class CaffeineTests { 18 | 19 | @Autowired 20 | private DiscussPostService postService; 21 | 22 | // 自造数据,进行压力测试 23 | @Test 24 | public void initDataForTest() { 25 | long startTime = System.currentTimeMillis(); 26 | for (int i = 0; i < 300000; i++) { 27 | DiscussPost post = new DiscussPost(); 28 | post.setUserId(111); 29 | post.setTitle("互联网求职暖春计划"); 30 | post.setContent("今年的就业形势,确实不容乐观。过了个年,仿佛跳水一般,整个讨论区哀鸿遍野!19届真的没人要了吗?!18届被优化真的没有出路了吗?!大家的“哀嚎”与“悲惨遭遇”牵动了每日潜伏于讨论区的牛客小哥哥小姐姐们的心,于是牛客决定:是时候为大家做点什么了!为了帮助大家度过“寒冬”,牛客网特别联合60+家企业,开启互联网求职暖春计划,面向18届&19届,拯救0 offer!"); 31 | post.setCreateTime(new Date()); 32 | post.setScore(Math.random() * 2000); 33 | postService.addDiscussPost(post); 34 | } 35 | long endTime = System.currentTimeMillis(); 36 | long totalTime=endTime-startTime; 37 | System.out.println(String.format("总时间%dmin",totalTime/(1000*60)));//39min 38 | } 39 | 40 | @Test 41 | public void testCache() { 42 | 43 | System.out.println(postService.findDiscussPosts(0, 0, 10, 1)); 44 | System.out.println(postService.findDiscussPosts(0, 0, 10, 1)); 45 | System.out.println(postService.findDiscussPosts(0, 0, 10, 1)); 46 | System.out.println(postService.findDiscussPosts(0, 0, 10, 0)); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/Event.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author coolsen 8 | * @version 1.0.0 9 | * @ClassName Event.java 10 | * @Description 事件,用于消息队列 11 | * @createTime 2020/5/14 21:19 12 | */ 13 | public class Event { 14 | 15 | private String topic; 16 | private int userId; 17 | private int entityType; 18 | private int entityId; 19 | //实体的作者 20 | private int entityUserId; 21 | 22 | /* 23 | 把其他额外的数据,存入map中,具有扩展性 24 | 目的:为了后期动态扩展,有些字段没有办法做出预判 25 | */ 26 | private Map data=new HashMap<>(); 27 | 28 | public String getTopic() { 29 | return topic; 30 | } 31 | 32 | /* 33 | 此处这样设计,是为了更灵活的设置属性,避免使用多个构造函数。 34 | 这样设计很灵活和方便 35 | */ 36 | public Event setTopic(String topic) { 37 | this.topic = topic; 38 | return this; 39 | } 40 | 41 | public int getUserId() { 42 | return userId; 43 | } 44 | 45 | public Event setUserId(int userId) { 46 | this.userId = userId; 47 | return this; 48 | } 49 | 50 | public int getEntityType() { 51 | return entityType; 52 | } 53 | 54 | public Event setEntityType(int entityType) { 55 | this.entityType = entityType; 56 | return this; 57 | } 58 | 59 | public int getEntityId() { 60 | return entityId; 61 | } 62 | 63 | public Event setEntityId(int entityId) { 64 | this.entityId = entityId; 65 | return this; 66 | } 67 | 68 | public int getEntityUserId() { 69 | return entityUserId; 70 | } 71 | 72 | public Event setEntityUserId(int entityUserId) { 73 | this.entityUserId = entityUserId; 74 | return this; 75 | } 76 | 77 | public Map getData() { 78 | return data; 79 | } 80 | 81 | public Event setData(String key,Object value) { 82 | this.data.put(key,value); 83 | return this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import com.nowcoder.community.annotation.LoginRequired; 4 | import com.nowcoder.community.controller.interceptor.*; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | @Configuration 11 | public class WebMvcConfig implements WebMvcConfigurer { 12 | 13 | @Autowired 14 | private AlphaInterceptor alphaInterceptor; 15 | 16 | @Autowired 17 | private LoginTicketInterceptor loginTicketInterceptor; 18 | 19 | /* 20 | 已废弃使用,现在使用spring security来做权限认证 21 | @Autowired 22 | private LoginRequiredInterceptor loginRequiredInterceptor; 23 | */ 24 | 25 | @Autowired 26 | private MessageInterceptor messageInterceptor; 27 | 28 | @Autowired 29 | private DataInterceptor dataInterceptor; 30 | 31 | @Override 32 | public void addInterceptors(InterceptorRegistry registry) { 33 | registry.addInterceptor(alphaInterceptor) 34 | .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg") 35 | .addPathPatterns("/register", "/login"); 36 | 37 | registry.addInterceptor(loginTicketInterceptor) 38 | .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); 39 | 40 | // registry.addInterceptor(loginRequiredInterceptor) 41 | // .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); 42 | 43 | registry.addInterceptor(messageInterceptor) 44 | .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); 45 | 46 | registry.addInterceptor(dataInterceptor) 47 | .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/static/js/discuss.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#topBtn").click(setTop); 3 | $("#wonderfulBtn").click(setWonderful); 4 | $("#deleteBtn").click(setDelete); 5 | }); 6 | 7 | function like(btn, entityType, entityId, entityUserId, postId) { 8 | $.post( 9 | CONTEXT_PATH + "/like", 10 | {"entityType":entityType,"entityId":entityId,"entityUserId":entityUserId,"postId":postId}, 11 | function(data) { 12 | data = $.parseJSON(data); 13 | if(data.code == 0) { 14 | $(btn).children("i").text(data.likeCount); 15 | $(btn).children("b").text(data.likeStatus==1?'已赞':"赞"); 16 | } else { 17 | alert(data.msg); 18 | } 19 | } 20 | ); 21 | } 22 | 23 | // 置顶 24 | function setTop() { 25 | $.post( 26 | CONTEXT_PATH + "/discuss/top", 27 | {"id":$("#postId").val()}, 28 | function(data) { 29 | data = $.parseJSON(data); 30 | if(data.code == 0) { 31 | $("#topBtn").attr("disabled", "disabled"); 32 | } else { 33 | alert(data.msg); 34 | } 35 | } 36 | ); 37 | } 38 | 39 | // 加精 40 | function setWonderful() { 41 | $.post( 42 | CONTEXT_PATH + "/discuss/wonderful", 43 | {"id":$("#postId").val()}, 44 | function(data) { 45 | data = $.parseJSON(data); 46 | if(data.code == 0) { 47 | $("#wonderfulBtn").attr("disabled", "disabled"); 48 | } else { 49 | alert(data.msg); 50 | } 51 | } 52 | ); 53 | } 54 | 55 | // 删除 56 | function setDelete() { 57 | $.post( 58 | CONTEXT_PATH + "/discuss/delete", 59 | {"id":$("#postId").val()}, 60 | function(data) { 61 | data = $.parseJSON(data); 62 | if(data.code == 0) { 63 | location.href = CONTEXT_PATH + "/index"; 64 | } else { 65 | alert(data.msg); 66 | } 67 | } 68 | ); 69 | } -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/CommunityConstant.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | /** 4 | * @author coolsen 5 | * @version 1.0.0 6 | * @ClassName CommunityConstant.java 7 | * @Description 常量 8 | * @createTime 4/29/2020 3:33 PM 9 | */ 10 | public interface CommunityConstant { 11 | 12 | /** 13 | * 激活成功 14 | */ 15 | int ACTIVATION_SUCCESS = 0; 16 | 17 | /** 18 | * 重复激活 19 | */ 20 | int ACTIVATION_REPEAT = 1; 21 | 22 | /** 23 | * 激活失败 24 | */ 25 | int ACTIVATION_FAILURE = 2; 26 | 27 | /** 28 | * 默认状态的登录凭证的超时时间 29 | */ 30 | int DEFAULT_EXPIRED_SECONDS = 3600 * 12; 31 | 32 | /** 33 | * 记住状态的登录凭证超时时间 34 | */ 35 | int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100; 36 | 37 | /** 38 | * 实体类型: 帖子 39 | */ 40 | int ENTITY_TYPE_POST = 1; 41 | 42 | /** 43 | * 实体类型: 评论 44 | */ 45 | int ENTITY_TYPE_COMMENT = 2; 46 | 47 | /** 48 | * 实体类型: 用户 49 | */ 50 | int ENTITY_TYPE_USER = 3; 51 | 52 | /** 53 | * 主题: 评论 54 | */ 55 | String TOPIC_COMMENT = "comment"; 56 | 57 | /** 58 | * 主题: 点赞 59 | */ 60 | String TOPIC_LIKE = "like"; 61 | 62 | /** 63 | * 主题: 关注 64 | */ 65 | String TOPIC_FOLLOW = "follow"; 66 | 67 | /** 68 | * 主题: 发帖 69 | */ 70 | String TOPIC_PUBLISH = "publish"; 71 | 72 | /** 73 | * 主题: 删帖 74 | */ 75 | String TOPIC_DELETE = "delete"; 76 | 77 | /** 78 | * 主题: 分享 79 | */ 80 | String TOPIC_SHARE = "share"; 81 | 82 | /** 83 | * 系统用户ID 84 | */ 85 | int SYSTEM_USER_ID = 1; 86 | 87 | /** 88 | * 权限: 普通用户 89 | */ 90 | String AUTHORITY_USER = "user"; 91 | 92 | /** 93 | * 权限: 管理员 94 | */ 95 | String AUTHORITY_ADMIN = "admin"; 96 | 97 | /** 98 | * 权限: 版主 99 | */ 100 | String AUTHORITY_MODERATOR = "moderator"; 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/Message.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author coolsen 7 | * @version 1.0.0 8 | * @ClassName Message.java 9 | * @Description 私信实体类 10 | * @createTime 5/8/2020 7:49 PM 11 | */ 12 | public class Message { 13 | private int id; 14 | private int fromId; 15 | private int toId; 16 | private String conversationId; 17 | private String content; 18 | private int status; 19 | private Date createTime; 20 | 21 | public int getId() { 22 | return id; 23 | } 24 | 25 | public void setId(int id) { 26 | this.id = id; 27 | } 28 | 29 | public int getFromId() { 30 | return fromId; 31 | } 32 | 33 | public void setFromId(int fromId) { 34 | this.fromId = fromId; 35 | } 36 | 37 | public int getToId() { 38 | return toId; 39 | } 40 | 41 | public void setToId(int toId) { 42 | this.toId = toId; 43 | } 44 | 45 | public String getConversationId() { 46 | return conversationId; 47 | } 48 | 49 | public void setConversationId(String conversationId) { 50 | this.conversationId = conversationId; 51 | } 52 | 53 | public String getContent() { 54 | return content; 55 | } 56 | 57 | public void setContent(String content) { 58 | this.content = content; 59 | } 60 | 61 | public int getStatus() { 62 | return status; 63 | } 64 | 65 | public void setStatus(int status) { 66 | this.status = status; 67 | } 68 | 69 | public Date getCreateTime() { 70 | return createTime; 71 | } 72 | 73 | public void setCreateTime(Date createTime) { 74 | this.createTime = createTime; 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return "Message{" + 80 | "id=" + id + 81 | ", fromId=" + fromId + 82 | ", toId=" + toId + 83 | ", conversationId='" + conversationId + '\'' + 84 | ", content='" + content + '\'' + 85 | ", status=" + status + 86 | ", createTime=" + createTime + 87 | '}'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/Page.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | /** 4 | * 封装分页相关的信息. 5 | */ 6 | public class Page { 7 | 8 | // 当前页码 9 | private int current = 1; 10 | // 显示上限 11 | private int limit = 10; 12 | // 数据总数(用于计算总页数) 13 | private int rows; 14 | // 查询路径(用于复用分页链接) 15 | private String path; 16 | 17 | public int getCurrent() { 18 | return current; 19 | } 20 | 21 | public void setCurrent(int current) { 22 | if (current >= 1) { 23 | this.current = current; 24 | } 25 | } 26 | 27 | public int getLimit() { 28 | return limit; 29 | } 30 | 31 | public void setLimit(int limit) { 32 | if (limit >= 1 && limit <= 100) { 33 | this.limit = limit; 34 | } 35 | } 36 | 37 | public int getRows() { 38 | return rows; 39 | } 40 | 41 | public void setRows(int rows) { 42 | if (rows >= 0) { 43 | this.rows = rows; 44 | } 45 | } 46 | 47 | public String getPath() { 48 | return path; 49 | } 50 | 51 | public void setPath(String path) { 52 | this.path = path; 53 | } 54 | 55 | /** 56 | * 获取当前页的起始行 57 | * 58 | * @return 59 | */ 60 | public int getOffset() { 61 | // current * limit - limit 62 | return (current - 1) * limit; 63 | } 64 | 65 | /** 66 | * 获取总页数 67 | * 68 | * @return 69 | */ 70 | public int getTotal() { 71 | // rows / limit [+1] 72 | if (rows % limit == 0) { 73 | return rows / limit; 74 | } else { 75 | return rows / limit + 1; 76 | } 77 | } 78 | 79 | /** 80 | * 获取起始页码 81 | * 82 | * @return 83 | */ 84 | public int getFrom() { 85 | int from = current - 2; 86 | return from < 1 ? 1 : from; 87 | } 88 | 89 | /** 90 | * 获取结束页码 91 | * 92 | * @return 93 | */ 94 | public int getTo() { 95 | int to = current + 2; 96 | int total = getTotal(); 97 | return to > total ? total : to; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/DataController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.nowcoder.community.service.DataService; 4 | import org.apache.kafka.common.network.Mode; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.format.annotation.DateTimeFormat; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | 12 | import java.util.Date; 13 | 14 | /** 15 | * @author coolsen 16 | * @version 1.0.0 17 | * @ClassName DataController.java 18 | * @Description 数据统计 Controller 19 | * @createTime 2020/5/20 21:41 20 | */ 21 | 22 | @Controller 23 | public class DataController { 24 | 25 | @Autowired 26 | private DataService dataService; 27 | 28 | // 统计页面 29 | @RequestMapping(path = "/data",method = {RequestMethod.GET,RequestMethod.POST}) 30 | public String getDataPage(){ 31 | return "/site/admin/data"; 32 | } 33 | 34 | // 统计网站UV 35 | @RequestMapping(path = "/data/uv",method = RequestMethod.POST) 36 | public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start, 37 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model){ 38 | long uv = dataService.calculateUV(start, end); 39 | model.addAttribute("uvResult",uv); 40 | model.addAttribute("uvStartDate", start); 41 | model.addAttribute("uvEndDate", end); 42 | 43 | // 此处与return "/site/admin/data"一样,只不过目前通过转发,可以复用getDataPage方法。这个也是getDataPage方法的method = {RequestMethod.GET,RequestMethod.POST}的原因,因为要接受来自getUV的request(为post方式) 44 | return "forward:/data"; 45 | } 46 | 47 | // 统计活跃用户 48 | @RequestMapping(path = "/data/dau", method = RequestMethod.POST) 49 | public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start, 50 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) { 51 | long dau = dataService.calculateDAU(start, end); 52 | model.addAttribute("dauResult", dau); 53 | model.addAttribute("dauStartDate", start); 54 | model.addAttribute("dauEndDate", end); 55 | return "forward:/data"; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.service; 2 | 3 | import com.nowcoder.community.dao.CommentMapper; 4 | import com.nowcoder.community.entity.Comment; 5 | import com.nowcoder.community.util.CommunityConstant; 6 | import com.nowcoder.community.util.SensitiveFilter; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Isolation; 10 | import org.springframework.transaction.annotation.Propagation; 11 | import org.springframework.transaction.annotation.Transactional; 12 | import org.springframework.web.util.HtmlUtils; 13 | 14 | import java.util.List; 15 | 16 | @Service 17 | public class CommentService implements CommunityConstant { 18 | 19 | @Autowired 20 | private CommentMapper commentMapper; 21 | 22 | @Autowired 23 | private SensitiveFilter sensitiveFilter; 24 | 25 | @Autowired 26 | private DiscussPostService discussPostService; 27 | 28 | public List findCommentsByEntity(int entityType, int entityId, int offset, int limit) { 29 | return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit); 30 | } 31 | 32 | public int findCommentCount(int entityType, int entityId) { 33 | return commentMapper.selectCountByEntity(entityType, entityId); 34 | } 35 | 36 | @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) 37 | public int addComment(Comment comment) { 38 | if (comment == null) { 39 | throw new IllegalArgumentException("参数不能为空!"); 40 | } 41 | 42 | // 添加评论 43 | comment.setContent(HtmlUtils.htmlEscape(comment.getContent())); 44 | comment.setContent(sensitiveFilter.filter(comment.getContent())); 45 | int rows = commentMapper.insertComment(comment); 46 | 47 | // 更新帖子评论数量 48 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 49 | int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId()); 50 | discussPostService.updateCommentCount(comment.getEntityId(), count); 51 | } 52 | 53 | return rows; 54 | } 55 | 56 | public Comment findCommentById(int id) { 57 | return commentMapper.selectCommentById(id); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/mapper/discusspost-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | id, user_id, title, content, type, status, create_time, comment_count, score 9 | 10 | 11 | 26 | 27 | 35 | 36 | 37 | user_id, title, content, type, status, create_time, comment_count, score 38 | 39 | 40 | 41 | insert into discuss_post() 42 | values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score}) 43 | 44 | 45 | 51 | 52 | 53 | update discuss_post set comment_count = #{commentCount} where id = #{id} 54 | 55 | 56 | 57 | update discuss_post set type = #{type} where id = #{id} 58 | 59 | 60 | 61 | update discuss_post set status = #{status} where id = #{id} 62 | 63 | 64 | 65 | update discuss_post set score = #{score} where id = #{id} 66 | 67 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.service; 2 | 3 | import com.nowcoder.community.dao.MessageMapper; 4 | import com.nowcoder.community.entity.Message; 5 | import com.nowcoder.community.util.SensitiveFilter; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.util.HtmlUtils; 9 | 10 | import java.util.List; 11 | 12 | @Service 13 | public class MessageService { 14 | 15 | @Autowired 16 | private MessageMapper messageMapper; 17 | 18 | @Autowired 19 | private SensitiveFilter sensitiveFilter; 20 | 21 | public List findConversations(int userId, int offset, int limit) { 22 | return messageMapper.selectConversations(userId, offset, limit); 23 | } 24 | 25 | public int findConversationCount(int userId) { 26 | return messageMapper.selectConversationCount(userId); 27 | } 28 | 29 | public List findLetters(String conversationId, int offset, int limit) { 30 | return messageMapper.selectLetters(conversationId, offset, limit); 31 | } 32 | 33 | public int findLetterCount(String conversationId) { 34 | return messageMapper.selectLetterCount(conversationId); 35 | } 36 | 37 | public int findLetterUnreadCount(int userId, String conversationId) { 38 | return messageMapper.selectLetterUnreadCount(userId, conversationId); 39 | } 40 | 41 | public int addMessage(Message message) { 42 | message.setContent(HtmlUtils.htmlEscape(message.getContent())); 43 | message.setContent(sensitiveFilter.filter(message.getContent())); 44 | return messageMapper.insertMessage(message); 45 | } 46 | 47 | public int readMessage(List ids) { 48 | return messageMapper.updateStatus(ids, 1); 49 | } 50 | 51 | public Message findLatestNotice(int userId, String topic) { 52 | return messageMapper.selectLatestNotice(userId, topic); 53 | } 54 | 55 | public int findNoticeCount(int userId, String topic) { 56 | return messageMapper.selectNoticeCount(userId, topic); 57 | } 58 | 59 | public int findNoticeUnreadCount(int userId, String topic) { 60 | return messageMapper.selectNoticeUnreadCount(userId, topic); 61 | } 62 | 63 | public List findNotices(int userId, String topic, int offset, int limit) { 64 | return messageMapper.selectNotices(userId, topic, offset, limit); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/resources/static/css/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | background: #eee; 7 | font-family: arial, STHeiti, 'Microsoft YaHei', \5b8b\4f53; 8 | font-size: 14px; 9 | height: 100%; 10 | } 11 | 12 | .nk-container { 13 | position: relative; 14 | height: auto; 15 | min-height: 100%; 16 | } 17 | 18 | .container { 19 | width: 960px; 20 | padding: 0; 21 | } 22 | 23 | header .navbar-brand { 24 | background: url('http://static.nowcoder.com/images/res/logo/logo-v3.png') no-repeat; 25 | background-size: 147px 42px; 26 | width: 147px; 27 | height: 42px; 28 | margin: 5px 15px 5px 0; 29 | } 30 | 31 | header .navbar { 32 | padding: 5px 0; 33 | font-size: 16px; 34 | } 35 | 36 | header .badge { 37 | position: absolute; 38 | top: -3px; 39 | left: 33px; 40 | } 41 | 42 | footer { 43 | padding: 20px 0; 44 | font-size: 12px; 45 | position: absolute; 46 | bottom: 0; 47 | width: 100%; 48 | } 49 | 50 | footer .qrcode { 51 | text-align: center; 52 | } 53 | 54 | footer .detail-info{ 55 | border-left: 1px solid #888; 56 | } 57 | 58 | footer .company-info li { 59 | padding-left: 16px; 60 | margin: 4px 0; 61 | } 62 | 63 | .main { 64 | padding: 20px 0; 65 | padding-bottom: 200px; 66 | } 67 | 68 | .main .container { 69 | background: #fff; 70 | padding: 20px; 71 | } 72 | 73 | i { 74 | font-style: normal; 75 | } 76 | 77 | u { 78 | text-decoration: none; 79 | } 80 | 81 | b { 82 | font-weight: normal; 83 | } 84 | 85 | a { 86 | color: #000; 87 | } 88 | 89 | a:hover { 90 | text-decoration: none; 91 | } 92 | 93 | .font-size-12 { 94 | font-size: 12px; 95 | } 96 | .font-size-14 { 97 | font-size: 14px; 98 | } 99 | .font-size-16 { 100 | font-size: 16px; 101 | } 102 | .font-size-18 { 103 | font-size: 18px; 104 | } 105 | .font-size-20 { 106 | font-size: 20px; 107 | } 108 | .font-size-22 { 109 | font-size: 20px; 110 | } 111 | .font-size-24 { 112 | font-size: 20px; 113 | } 114 | 115 | .hidden { 116 | display: none; 117 | } 118 | 119 | .rt-0 { 120 | right: 0; 121 | top: 0; 122 | } 123 | 124 | .square { 125 | display: inline-block; 126 | width: 7px; 127 | height: 7px; 128 | background: #ff6547; 129 | margin-bottom: 2px; 130 | margin-right: 3px; 131 | } 132 | 133 | .bg-gray { 134 | background: #eff0f2; 135 | } 136 | 137 | .user-header { 138 | width: 50px; 139 | height: 50px; 140 | } 141 | 142 | em { 143 | font-style: normal; 144 | color: red; 145 | } 146 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/SpringBootTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import com.nowcoder.community.service.DiscussPostService; 5 | import org.junit.*; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | import java.util.Date; 13 | 14 | @RunWith(SpringRunner.class) 15 | @SpringBootTest 16 | @ContextConfiguration(classes = CommunityApplication.class) 17 | public class SpringBootTests { 18 | 19 | @Autowired 20 | private DiscussPostService discussPostService; 21 | 22 | private DiscussPost data; 23 | 24 | @BeforeClass 25 | public static void beforeClass() { 26 | System.out.println("beforeClass"); 27 | } 28 | 29 | @AfterClass 30 | public static void afterClass() { 31 | System.out.println("afterClass"); 32 | } 33 | 34 | @Before 35 | public void before() { 36 | System.out.println("before"); 37 | 38 | // 初始化测试数据 39 | data = new DiscussPost(); 40 | data.setUserId(111); 41 | data.setTitle("Test Title"); 42 | data.setContent("Test Content"); 43 | data.setCreateTime(new Date()); 44 | discussPostService.addDiscussPost(data); 45 | } 46 | 47 | @After 48 | public void after() { 49 | System.out.println("after"); 50 | 51 | // 删除测试数据 52 | discussPostService.updateStatus(data.getId(), 2); 53 | } 54 | 55 | @Test 56 | public void test1() { 57 | System.out.println("test1"); 58 | } 59 | 60 | @Test 61 | public void test2() { 62 | System.out.println("test2"); 63 | } 64 | 65 | @Test 66 | public void testFindById() { 67 | DiscussPost post = discussPostService.findDiscussPostById(data.getId()); 68 | Assert.assertNotNull(post); 69 | Assert.assertEquals(data.getTitle(), post.getTitle()); 70 | Assert.assertEquals(data.getContent(), post.getContent()); 71 | } 72 | 73 | @Test 74 | public void testUpdateScore() { 75 | int rows = discussPostService.updateScore(data.getId(), 2000.00); 76 | Assert.assertEquals(1, rows); 77 | 78 | DiscussPost post = discussPostService.findDiscussPostById(data.getId()); 79 | Assert.assertEquals(2000.00, post.getScore(), 2); 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author coolsen 7 | * @version 1.0.0 8 | * @ClassName Comment.java 9 | * @Description 评论实体类 10 | * @createTime 5/7/2020 4:27 PM 11 | */ 12 | public class Comment { 13 | private int id; 14 | private int userId; 15 | private int entityType; 16 | private int entityId; 17 | private int targetId; 18 | private String content; 19 | private int status; 20 | private Date createTime; 21 | 22 | public int getId() { 23 | return id; 24 | } 25 | 26 | public void setId(int id) { 27 | this.id = id; 28 | } 29 | 30 | public int getUserId() { 31 | return userId; 32 | } 33 | 34 | public void setUserId(int userId) { 35 | this.userId = userId; 36 | } 37 | 38 | public int getEntityType() { 39 | return entityType; 40 | } 41 | 42 | public void setEntityType(int entityType) { 43 | this.entityType = entityType; 44 | } 45 | 46 | public int getEntityId() { 47 | return entityId; 48 | } 49 | 50 | public void setEntityId(int entityId) { 51 | this.entityId = entityId; 52 | } 53 | 54 | public int getTargetId() { 55 | return targetId; 56 | } 57 | 58 | public void setTargetId(int targetId) { 59 | this.targetId = targetId; 60 | } 61 | 62 | public String getContent() { 63 | return content; 64 | } 65 | 66 | public void setContent(String content) { 67 | this.content = content; 68 | } 69 | 70 | public int getStatus() { 71 | return status; 72 | } 73 | 74 | public void setStatus(int status) { 75 | this.status = status; 76 | } 77 | 78 | public Date getCreateTime() { 79 | return createTime; 80 | } 81 | 82 | public void setCreateTime(Date createTime) { 83 | this.createTime = createTime; 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return "Comment{" + 89 | "id=" + id + 90 | ", userId=" + userId + 91 | ", entityType=" + entityType + 92 | ", entityId=" + entityId + 93 | ", targetId=" + targetId + 94 | ", content='" + content + '\'' + 95 | ", status=" + status + 96 | ", createTime=" + createTime + 97 | '}'; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import com.nowcoder.community.entity.Page; 5 | import com.nowcoder.community.service.ElasticsearchService; 6 | import com.nowcoder.community.service.LikeService; 7 | import com.nowcoder.community.service.UserService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | 15 | import javax.management.modelmbean.ModelMBeanOperationInfo; 16 | import java.util.ArrayList; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author coolsen 23 | * @version 1.0.0 24 | * @ClassName SearchController.java 25 | * @Description Search Controller 26 | * @createTime 2020/5/19 16:41 27 | */ 28 | 29 | @Controller 30 | public class SearchController { 31 | 32 | @Autowired 33 | private ElasticsearchService elasticsearchService; 34 | 35 | @Autowired 36 | private UserService userService; 37 | 38 | @Autowired 39 | private LikeService likeService; 40 | 41 | // search?keyword=xxx 42 | @RequestMapping(path = "/search", method = RequestMethod.GET) 43 | public String search(String keyword, Page page, Model model) { 44 | // 搜索帖子 45 | org.springframework.data.domain.Page searchResults = elasticsearchService.searchDiscussPost(keyword, page.getCurrent() - 1, page.getLimit()); 46 | 47 | // 聚合数据 48 | List> discussPosts = new ArrayList<>(); 49 | if (searchResults != null) { 50 | for (DiscussPost post : searchResults) { 51 | Map map = new HashMap<>(); 52 | 53 | // 帖子 54 | map.put("post", post); 55 | // 作者 56 | map.put("user", userService.findUserById(post.getUserId())); 57 | // 点赞数量 58 | map.put("likeCount", likeService.findUserLikeCount(post.getUserId())); 59 | 60 | discussPosts.add(map); 61 | } 62 | } 63 | model.addAttribute("discussPosts", discussPosts); 64 | model.addAttribute("keyword", keyword); 65 | 66 | page.setPath("search?keyword=" + keyword); 67 | page.setRows(searchResults == null ? 0 : (int) searchResults.getTotalElements()); 68 | 69 | return "/site/search"; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/CommunityApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import com.nowcoder.community.dao.AlphaDao; 4 | import com.nowcoder.community.service.AlphaService; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.context.ApplicationContextAware; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import java.text.SimpleDateFormat; 17 | import java.util.Date; 18 | 19 | @RunWith(SpringRunner.class) 20 | @SpringBootTest 21 | @ContextConfiguration(classes = CommunityApplication.class) 22 | public class CommunityApplicationTests implements ApplicationContextAware { 23 | 24 | private ApplicationContext applicationContext; 25 | 26 | @Override 27 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 28 | this.applicationContext = applicationContext; 29 | } 30 | 31 | @Test 32 | public void testApplicationContext() { 33 | System.out.println(applicationContext); 34 | 35 | AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class); 36 | System.out.println(alphaDao.select()); 37 | 38 | alphaDao = applicationContext.getBean("alphaHibernate", AlphaDao.class); 39 | System.out.println(alphaDao.select()); 40 | } 41 | 42 | @Test 43 | public void testBeanManagement() { 44 | AlphaService alphaService = applicationContext.getBean(AlphaService.class); 45 | System.out.println(alphaService); 46 | 47 | alphaService = applicationContext.getBean(AlphaService.class); 48 | System.out.println(alphaService); 49 | } 50 | 51 | @Test 52 | public void testBeanConfig() { 53 | SimpleDateFormat simpleDateFormat = 54 | applicationContext.getBean(SimpleDateFormat.class); 55 | System.out.println(simpleDateFormat.format(new Date())); 56 | } 57 | 58 | @Autowired 59 | @Qualifier("alphaHibernate") 60 | private AlphaDao alphaDao; 61 | 62 | @Autowired 63 | private AlphaService alphaService; 64 | 65 | @Autowired 66 | private SimpleDateFormat simpleDateFormat; 67 | 68 | @Test 69 | public void testDI() { 70 | System.out.println(alphaDao); 71 | System.out.println(alphaService); 72 | System.out.println(simpleDateFormat); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class User { 6 | 7 | private int id; 8 | private String username; 9 | private String password; 10 | private String salt; 11 | private String email; 12 | private int type; 13 | private int status; 14 | private String activationCode; 15 | private String headerUrl; 16 | private Date createTime; 17 | 18 | public int getId() { 19 | return id; 20 | } 21 | 22 | public void setId(int id) { 23 | this.id = id; 24 | } 25 | 26 | public String getUsername() { 27 | return username; 28 | } 29 | 30 | public void setUsername(String username) { 31 | this.username = username; 32 | } 33 | 34 | public String getPassword() { 35 | return password; 36 | } 37 | 38 | public void setPassword(String password) { 39 | this.password = password; 40 | } 41 | 42 | public String getSalt() { 43 | return salt; 44 | } 45 | 46 | public void setSalt(String salt) { 47 | this.salt = salt; 48 | } 49 | 50 | public String getEmail() { 51 | return email; 52 | } 53 | 54 | public void setEmail(String email) { 55 | this.email = email; 56 | } 57 | 58 | public int getType() { 59 | return type; 60 | } 61 | 62 | public void setType(int type) { 63 | this.type = type; 64 | } 65 | 66 | public int getStatus() { 67 | return status; 68 | } 69 | 70 | public void setStatus(int status) { 71 | this.status = status; 72 | } 73 | 74 | public String getActivationCode() { 75 | return activationCode; 76 | } 77 | 78 | public void setActivationCode(String activationCode) { 79 | this.activationCode = activationCode; 80 | } 81 | 82 | public String getHeaderUrl() { 83 | return headerUrl; 84 | } 85 | 86 | public void setHeaderUrl(String headerUrl) { 87 | this.headerUrl = headerUrl; 88 | } 89 | 90 | public Date getCreateTime() { 91 | return createTime; 92 | } 93 | 94 | public void setCreateTime(Date createTime) { 95 | this.createTime = createTime; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "User{" + 101 | "id=" + id + 102 | ", username='" + username + '\'' + 103 | ", password='" + password + '\'' + 104 | ", salt='" + salt + '\'' + 105 | ", email='" + email + '\'' + 106 | ", type=" + type + 107 | ", status=" + status + 108 | ", activationCode='" + activationCode + '\'' + 109 | ", headerUrl='" + headerUrl + '\'' + 110 | ", createTime=" + createTime + 111 | '}'; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/QuartzConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import com.nowcoder.community.quartz.AlphaJob; 4 | import com.nowcoder.community.quartz.PostScoreRefreshJob; 5 | import org.quartz.JobDataMap; 6 | import org.quartz.JobDetail; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.scheduling.quartz.JobDetailFactoryBean; 10 | import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; 11 | 12 | /** 13 | * @author coolsen 14 | * @version 1.0.0 15 | * @ClassName QuartzConfig.java 16 | * @Description Quartz Config 17 | * @createTime 2020/5/21 10:46 18 | */ 19 | 20 | // 配置(仅在第一次使用到) -> 初始化到数据库 -> 从数据库调用 21 | @Configuration 22 | public class QuartzConfig { 23 | 24 | // FactoryBean可简化Bean的实例化过程: 25 | // 1.通过FactoryBean封装Bean的实例化过程. 26 | // 2.将FactoryBean装配到Spring容器里. 27 | // 3.将FactoryBean注入给其他的Bean. 28 | // 4.该Bean得到的是FactoryBean所管理的对象实例. 29 | 30 | 31 | // 配置JobDetail 32 | // 此处为demo,之后不再使用,所以注释了@Bean 33 | // @Bean 34 | public JobDetailFactoryBean alphaJobDetail() { 35 | JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); 36 | factoryBean.setJobClass(AlphaJob.class); 37 | factoryBean.setName("alphaJob"); 38 | factoryBean.setGroup("alphaJobGroup"); 39 | factoryBean.setDurability(true); 40 | factoryBean.setRequestsRecovery(true);//任务出现问题时是否恢复 41 | return factoryBean; 42 | } 43 | 44 | // 配置Trigger 45 | // @Bean 46 | public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail){ 47 | SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); 48 | factoryBean.setJobDetail(alphaJobDetail); 49 | factoryBean.setName("alphaTrigger"); 50 | factoryBean.setGroup("alphaTriggerGroup"); 51 | factoryBean.setRepeatInterval(3000); 52 | factoryBean.setJobDataMap(new JobDataMap());//存储当前Job状态 53 | return factoryBean; 54 | } 55 | 56 | // 刷新帖子分数任务 57 | @Bean 58 | public JobDetailFactoryBean postScoreRefreshJobDetail() { 59 | JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); 60 | factoryBean.setJobClass(PostScoreRefreshJob.class); 61 | factoryBean.setName("postScoreRefreshJob"); 62 | factoryBean.setGroup("communityJobGroup"); 63 | factoryBean.setDurability(true); 64 | factoryBean.setRequestsRecovery(true); 65 | return factoryBean; 66 | } 67 | 68 | @Bean 69 | public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail){ 70 | SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); 71 | factoryBean.setJobDetail(postScoreRefreshJobDetail); 72 | factoryBean.setName("postScoreRefreshTrigger"); 73 | factoryBean.setGroup("communityJobGroup"); 74 | factoryBean.setRepeatInterval(1000*60*5); 75 | factoryBean.setJobDataMap(new JobDataMap()); 76 | return factoryBean; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import com.nowcoder.community.entity.Page; 5 | import com.nowcoder.community.entity.User; 6 | import com.nowcoder.community.service.DiscussPostService; 7 | import com.nowcoder.community.service.LikeService; 8 | import com.nowcoder.community.service.UserService; 9 | import com.nowcoder.community.util.CommunityConstant; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.ui.Model; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | @Controller 23 | public class HomeController implements CommunityConstant { 24 | 25 | @Autowired 26 | private DiscussPostService discussPostService; 27 | 28 | @Autowired 29 | private UserService userService; 30 | 31 | @Autowired 32 | private LikeService likeService; 33 | 34 | @RequestMapping(path = "/", method = RequestMethod.GET) 35 | public String root(){ 36 | return "forward:/index"; 37 | } 38 | @RequestMapping(path = "/index", method = RequestMethod.GET) 39 | public String getIndexPage(Model model, Page page, 40 | @RequestParam(name = "orderMode", defaultValue = "1") int orderMode) { 41 | // 方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model. 42 | // 所以,在thymeleaf中可以直接访问Page对象中的数据. 43 | page.setRows(discussPostService.findDiscussPostRows(0)); 44 | page.setPath("/index?orderMode=" + orderMode); 45 | List list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit(),orderMode); 46 | List> discussPosts = new ArrayList<>(); 47 | if (list != null) { 48 | for (DiscussPost post : list) { 49 | Map map = new HashMap<>(); 50 | map.put("post", post); 51 | User user = userService.findUserById(post.getUserId()); 52 | map.put("user", user); 53 | //点赞数量 54 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()); 55 | map.put("likeCount", likeCount); 56 | 57 | discussPosts.add(map); 58 | } 59 | } 60 | model.addAttribute("discussPosts", discussPosts); 61 | model.addAttribute("orderMode", orderMode); 62 | 63 | return "/index"; 64 | } 65 | 66 | @RequestMapping(path = "/error", method = RequestMethod.GET) 67 | public String getErrorPage() { 68 | return "/error/500"; 69 | } 70 | 71 | //拒绝访问的提示页面 72 | @RequestMapping( 73 | path = {"/denied"}, 74 | method = {RequestMethod.GET} 75 | ) 76 | public String getDeniedPage() { 77 | return "/error/404"; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/LikeController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.google.code.kaptcha.Producer; 4 | import com.nowcoder.community.entity.Event; 5 | import com.nowcoder.community.entity.User; 6 | import com.nowcoder.community.event.EventProducer; 7 | import com.nowcoder.community.service.LikeService; 8 | import com.nowcoder.community.util.CommunityConstant; 9 | import com.nowcoder.community.util.CommunityUtil; 10 | import com.nowcoder.community.util.HostHolder; 11 | import com.nowcoder.community.util.RedisKeyUtil; 12 | import org.apache.ibatis.ognl.ObjectElementsAccessor; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.web.bind.annotation.RequestBody; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RequestMethod; 19 | import org.springframework.web.bind.annotation.ResponseBody; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | /** 25 | * @author coolsen 26 | * @version 1.0.0 27 | * @ClassName LikeController.java 28 | * @Description 点赞 Controller 29 | * @createTime 5/9/2020 4:30 PM 30 | */ 31 | 32 | @Controller 33 | public class LikeController implements CommunityConstant { 34 | 35 | @Autowired 36 | private LikeService likeService; 37 | 38 | @Autowired 39 | private HostHolder hostHolder; 40 | 41 | @Autowired 42 | private EventProducer eventProducer; 43 | 44 | @Autowired 45 | private RedisTemplate redisTemplate; 46 | 47 | @RequestMapping(path = "/like", method = RequestMethod.POST) 48 | @ResponseBody 49 | public String like(int entityType, int entityId, int entityUserId, int postId) { 50 | User user = hostHolder.getUser(); 51 | //点赞 52 | likeService.like(user.getId(), entityType, entityId, entityUserId); 53 | //点赞数量 54 | long likeCount = likeService.findEntityLikeCount(entityType, entityId); 55 | //点赞状态 56 | int likeStatus = likeService.findEntityLikeStatus(entityType, entityId, user.getId()); 57 | //返回的结果 58 | Map map = new HashMap<>(); 59 | map.put("likeCount", likeCount); 60 | map.put("likeStatus", likeStatus); 61 | 62 | // 触发点赞事件,仅点赞需要通知 63 | if (likeStatus == 1) { 64 | Event event = new Event() 65 | .setTopic(TOPIC_LIKE) 66 | .setUserId(hostHolder.getUser().getId()) 67 | .setEntityType(entityType) 68 | .setEntityId(entityId) 69 | .setEntityUserId(entityUserId) 70 | .setData("postId", postId); 71 | eventProducer.fireEvent(event); 72 | 73 | if (entityType == ENTITY_TYPE_POST) { 74 | // 计算帖子分数 75 | String redisKey = RedisKeyUtil.getPostScoreKey(); 76 | redisTemplate.opsForSet().add(redisKey, postId); 77 | } 78 | } 79 | return CommunityUtil.getJSONString(0, null, map); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/interceptor/LoginTicketInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller.interceptor; 2 | 3 | import com.nowcoder.community.entity.LoginTicket; 4 | import com.nowcoder.community.entity.User; 5 | import com.nowcoder.community.service.UserService; 6 | import com.nowcoder.community.util.CookieUtil; 7 | import com.nowcoder.community.util.HostHolder; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContextHolder; 12 | import org.springframework.security.core.context.SecurityContextImpl; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.servlet.HandlerInterceptor; 15 | import org.springframework.web.servlet.ModelAndView; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.util.Date; 20 | 21 | @Component 22 | public class LoginTicketInterceptor implements HandlerInterceptor { 23 | 24 | @Autowired 25 | private UserService userService; 26 | 27 | @Autowired 28 | private HostHolder hostHolder; 29 | 30 | @Override 31 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 32 | // 从cookie中获取凭证 33 | String ticket = CookieUtil.getValue(request, "ticket"); 34 | 35 | if (ticket != null) { 36 | // 查询凭证 37 | LoginTicket loginTicket = userService.findLoginTicket(ticket); 38 | // 检查凭证是否有效 39 | if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { 40 | // 根据凭证查询用户 41 | User user = userService.findUserById(loginTicket.getUserId()); 42 | // 在本次请求中持有用户 43 | hostHolder.setUser(user); 44 | 45 | // 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权. 46 | // principal: 主要信息; credentials: 证书; authorities: 权限; 47 | Authentication authentication = new UsernamePasswordAuthenticationToken( 48 | user, user.getPassword(), userService.getAuthorities(user.getId())); 49 | // 存入SecurityContext 50 | SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); 51 | 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | /* 59 | 在模板引擎之前使用 60 | */ 61 | @Override 62 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 63 | User user = hostHolder.getUser(); 64 | if (user != null && modelAndView != null) { 65 | modelAndView.addObject("loginUser", user); 66 | } 67 | } 68 | 69 | /* 70 | 最后清除user数据 71 | */ 72 | @Override 73 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 74 | hostHolder.clear(); 75 | // 清除保存权限验证的结果 76 | SecurityContextHolder.clearContext(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author coolsen 7 | * @version 1.0.0 8 | * @ClassName Redis.java 9 | * @Description Redis Key 生成工具 10 | * @createTime 5/9/2020 3:54 PM 11 | */ 12 | public class RedisKeyUtil { 13 | 14 | private static final String SPLIT = ":"; 15 | private static final String PREFIX_ENTITY_LIKE = "like:entity"; 16 | private static final String PREFIX_USER_LIKE = "like:user"; 17 | private static final String PREFIX_FOLLOWEE = "followee"; 18 | private static final String PREFIX_FOLLOWER = "follower"; 19 | 20 | //验证码 21 | private static final String PREFIX_KAPTCHA = "kaptcha"; 22 | 23 | //登录凭证 24 | private static final String PREFIX_TICKET = "ticket"; 25 | private static final String PREFIX_USER = "user"; 26 | 27 | // 网站数据统计 28 | private static final String PREFIX_UV = "uv"; 29 | private static final String PREFIX_DAU = "dau"; 30 | 31 | private static final String PREFIX_POST = "post"; 32 | 33 | 34 | //某个实体收到的赞,如帖子,评论 35 | //like:entity:entityType:entityId -> set(userId) 对应set,存入userId 36 | public static String getEntityLikeKey(int entityType, int entityId) { 37 | return PREFIX_ENTITY_LIKE + entityType + SPLIT + entityId; 38 | } 39 | 40 | //某个用户收到的总赞数 41 | //like:user:userId ->int 42 | public static String getUserLikeKey(int userId) { 43 | return PREFIX_USER_LIKE + SPLIT + userId; 44 | } 45 | 46 | //某个用户关注的实体 47 | //followee:userId:entityType ->zset(entityId,date),用有序集合存,存的是关注的哪个实体,分数是当前时间。 48 | //为了后期统计方便,统计关注了哪些,进行排序列举 49 | public static String getFolloweeKey(int userId, int entityType) { 50 | return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; 51 | } 52 | 53 | //某个实体拥有的粉丝,实体可能是用户,或者是帖子 54 | //follower:entityType:entityId ->zset(userId,date),存入用户Id 55 | public static String getFollowerKey(int entityType, int entityId) { 56 | return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityType; 57 | } 58 | 59 | //登录验证码 60 | //owner是指随机生成的uuid 61 | public static String getKaptchaKey(String owner) { 62 | return PREFIX_KAPTCHA + SPLIT + owner; 63 | } 64 | 65 | // 登录的凭证 66 | public static String getTicketKey(String ticket) { 67 | return PREFIX_TICKET + SPLIT + ticket; 68 | } 69 | 70 | // 用户 71 | public static String getUserKey(int userId) { 72 | return PREFIX_USER + SPLIT + userId; 73 | } 74 | 75 | // 单日uv 76 | public static String getUVKey(String date) { 77 | return PREFIX_UV + SPLIT + date; 78 | } 79 | 80 | // 区间UV 81 | public static String getUVKey(String startDate, String endData) { 82 | return PREFIX_UV + SPLIT + startDate + SPLIT + endData; 83 | } 84 | 85 | // 单日DAU 86 | public static String getDAUKey(String date) { 87 | return PREFIX_DAU + SPLIT + date; 88 | } 89 | 90 | // 区间DAU 91 | public static String getDAUKey(String startDate, String endDate) { 92 | return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate; 93 | } 94 | 95 | // 帖子分数 96 | public static String getPostScoreKey() { 97 | return PREFIX_POST + SPLIT + "score"; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/service/LikeService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.service; 2 | 3 | import com.nowcoder.community.util.RedisKeyUtil; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.dao.DataAccessException; 6 | import org.springframework.data.redis.core.RedisOperations; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.data.redis.core.SessionCallback; 9 | import org.springframework.stereotype.Service; 10 | 11 | /** 12 | * @author coolsen 13 | * @version 1.0.0 14 | * @ClassName LikeService.java 15 | * @Description 点赞Service 16 | * @createTime 5/9/2020 4:01 PM 17 | */ 18 | 19 | @Service 20 | public class LikeService { 21 | 22 | @Autowired 23 | private RedisTemplate redisTemplate; 24 | 25 | /** 26 | * @Description: 对实体点赞,对相应的用户加赞 27 | * @param userId :点赞的人 28 | * @param entityType 29 | * @param entityId 30 | * @param entityUserId :被赞的人,即作者 31 | * @return: void 32 | * @Date 5/12/2020 33 | **/ 34 | public void like(int userId,int entityType,int entityId,int entityUserId){ 35 | redisTemplate.execute(new SessionCallback() { 36 | @Override 37 | public Object execute(RedisOperations operations) throws DataAccessException { 38 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 39 | String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId); 40 | boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId); 41 | //多个更新操作,需要事务 42 | operations.multi(); 43 | if (isMember) { 44 | //取消赞 45 | redisTemplate.opsForSet().remove(entityLikeKey, userId); 46 | redisTemplate.opsForValue().decrement(userLikeKey); 47 | } else { 48 | //点赞 49 | redisTemplate.opsForSet().add(entityLikeKey, userId); 50 | redisTemplate.opsForValue().increment(userLikeKey); 51 | } 52 | return operations.exec(); 53 | } 54 | }); 55 | 56 | } 57 | 58 | /** 59 | * @Description: 查询某实体(帖子,评论等)点赞数量 60 | * @param entityType 61 | * @param entityId 62 | * @return: long 63 | * @Date 5/9/2020 64 | **/ 65 | public long findEntityLikeCount(int entityType,int entityId){ 66 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 67 | return redisTemplate.opsForSet().size(entityLikeKey); 68 | } 69 | 70 | /** 71 | * @Description:查询某人对某实体的点赞状态 72 | * @param entityType 73 | * @param entityId 74 | * @param userId 75 | * @return: int 76 | * @Date 5/9/2020 77 | **/ 78 | public int findEntityLikeStatus(int entityType,int entityId,int userId){ 79 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 80 | //此处返回int,是为了进行扩展。比如扩展踩,为止2.等等情况 81 | return redisTemplate.opsForSet().isMember(entityLikeKey,userId)?1:0; 82 | } 83 | 84 | /** 85 | * @Description: 查询某个用户获得赞,用于在个人主页查看收获了多少赞 86 | * @param userId 87 | * @return: int 88 | * @Date 2020/5/12 89 | **/ 90 | public int findUserLikeCount(int userId){ 91 | String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); 92 | Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey); 93 | return count==null?0:count.intValue();// count.intValue()数据的整数形式; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/entity/DiscussPost.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.entity; 2 | 3 | import org.elasticsearch.indices.recovery.RecoveryState; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.elasticsearch.annotations.Document; 6 | import org.springframework.data.elasticsearch.annotations.Field; 7 | import org.springframework.data.elasticsearch.annotations.FieldType; 8 | 9 | import java.util.Date; 10 | 11 | @Document(indexName = "discusspost",type = "_doc",shards = 6,replicas = 3) 12 | public class DiscussPost { 13 | 14 | @Id 15 | private int id; 16 | 17 | @Field(type = FieldType.Integer) 18 | private int userId; 19 | 20 | @Field(type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_smart") 21 | private String title; 22 | 23 | @Field(type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_smart") 24 | private String content; 25 | 26 | @Field(type=FieldType.Integer) 27 | private int type; 28 | 29 | @Field(type= FieldType.Integer) 30 | private int status; 31 | 32 | @Field(type = FieldType.Date) 33 | private Date createTime; 34 | 35 | @Field(type= FieldType.Integer) 36 | private int commentCount; 37 | 38 | @Field(type= FieldType.Double) 39 | private double score; 40 | 41 | public int getId() { 42 | return id; 43 | } 44 | 45 | public void setId(int id) { 46 | this.id = id; 47 | } 48 | 49 | public int getUserId() { 50 | return userId; 51 | } 52 | 53 | public void setUserId(int userId) { 54 | this.userId = userId; 55 | } 56 | 57 | public String getTitle() { 58 | return title; 59 | } 60 | 61 | public void setTitle(String title) { 62 | this.title = title; 63 | } 64 | 65 | public String getContent() { 66 | return content; 67 | } 68 | 69 | public void setContent(String content) { 70 | this.content = content; 71 | } 72 | 73 | public int getType() { 74 | return type; 75 | } 76 | 77 | public void setType(int type) { 78 | this.type = type; 79 | } 80 | 81 | public int getStatus() { 82 | return status; 83 | } 84 | 85 | public void setStatus(int status) { 86 | this.status = status; 87 | } 88 | 89 | public Date getCreateTime() { 90 | return createTime; 91 | } 92 | 93 | public void setCreateTime(Date createTime) { 94 | this.createTime = createTime; 95 | } 96 | 97 | public int getCommentCount() { 98 | return commentCount; 99 | } 100 | 101 | public void setCommentCount(int commentCount) { 102 | this.commentCount = commentCount; 103 | } 104 | 105 | public double getScore() { 106 | return score; 107 | } 108 | 109 | public void setScore(double score) { 110 | this.score = score; 111 | } 112 | 113 | @Override 114 | public String toString() { 115 | return "DiscussPost{" + 116 | "id=" + id + 117 | ", userId=" + userId + 118 | ", title='" + title + '\'' + 119 | ", content='" + content + '\'' + 120 | ", type=" + type + 121 | ", status=" + status + 122 | ", createTime=" + createTime + 123 | ", commentCount=" + commentCount + 124 | ", score=" + score + 125 | '}'; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.google.code.kaptcha.Producer; 4 | import com.nowcoder.community.entity.Comment; 5 | import com.nowcoder.community.entity.DiscussPost; 6 | import com.nowcoder.community.entity.Event; 7 | import com.nowcoder.community.event.EventProducer; 8 | import com.nowcoder.community.service.CommentService; 9 | import com.nowcoder.community.service.DiscussPostService; 10 | import com.nowcoder.community.util.CommunityConstant; 11 | import com.nowcoder.community.util.HostHolder; 12 | import com.nowcoder.community.util.RedisKeyUtil; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RequestMethod; 19 | 20 | import java.util.Date; 21 | 22 | /** 23 | * @author coolsen 24 | * @version 1.0.0 25 | * @ClassName CommentController.java 26 | * @Description Comment 27 | * @createTime 5/7/2020 5:47 PM 28 | */ 29 | 30 | @Controller 31 | @RequestMapping("/comment") 32 | public class CommentController implements CommunityConstant { 33 | 34 | @Autowired 35 | private CommentService commentService; 36 | 37 | @Autowired 38 | private HostHolder hostHolder; 39 | 40 | @Autowired 41 | private EventProducer eventProducer; 42 | 43 | @Autowired 44 | private DiscussPostService discussPostService; 45 | 46 | @Autowired 47 | private RedisTemplate redisTemplate; 48 | 49 | @RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST) 50 | public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) { 51 | comment.setUserId(hostHolder.getUser().getId()); 52 | comment.setStatus(0); 53 | comment.setCreateTime(new Date()); 54 | commentService.addComment(comment); 55 | 56 | // 触发评论事件 57 | Event event = new Event() 58 | .setTopic(TOPIC_COMMENT) 59 | .setUserId(hostHolder.getUser().getId()) 60 | .setEntityType(comment.getEntityType()) 61 | .setEntityUserId(comment.getEntityId()) 62 | .setData("postId", discussPostId); 63 | 64 | if (comment.getEntityType() == ENTITY_TYPE_COMMENT) { 65 | DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId()); 66 | event.setEntityUserId(target.getUserId()); 67 | } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) { 68 | Comment target = commentService.findCommentById(comment.getEntityId()); 69 | event.setEntityUserId(target.getUserId()); 70 | } 71 | eventProducer.fireEvent(event); 72 | 73 | // 触发发帖事件,因为评论帖子时,帖子的评论数量就更改了,需要更新elasticsearch中的数据 74 | if(comment.getEntityType()==ENTITY_TYPE_POST){ 75 | event=new Event() 76 | .setTopic(TOPIC_PUBLISH) 77 | .setUserId(hostHolder.getUser().getId()) 78 | .setEntityType(ENTITY_TYPE_POST) 79 | .setEntityId(discussPostId); 80 | eventProducer.fireEvent(event); 81 | 82 | // 计算帖子分数 83 | String redisKey = RedisKeyUtil.getPostScoreKey(); 84 | redisTemplate.opsForSet().add(redisKey,discussPostId); 85 | } 86 | 87 | return "redirect:/discuss/detail/" + discussPostId; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/service/DataService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.service; 2 | 3 | import com.nowcoder.community.util.RedisKeyUtil; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.dao.DataAccessException; 6 | import org.springframework.data.redis.connection.RedisConnection; 7 | import org.springframework.data.redis.connection.RedisStringCommands; 8 | import org.springframework.data.redis.core.RedisCallback; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.text.SimpleDateFormat; 13 | import java.util.ArrayList; 14 | import java.util.Calendar; 15 | import java.util.Date; 16 | import java.util.List; 17 | 18 | /** 19 | * @author coolsen 20 | * @version 1.0.0 21 | * @ClassName DataService.java 22 | * @Description 数据统计 23 | * @createTime 2020/5/20 21:13 24 | */ 25 | 26 | @Service 27 | public class DataService { 28 | 29 | @Autowired 30 | private RedisTemplate redisTemplate; 31 | 32 | private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd"); 33 | 34 | // 将指定ip计入uv 35 | public void recordUV(String ip) { 36 | String redisKey = RedisKeyUtil.getUVKey(df.format(new Date())); 37 | redisTemplate.opsForHyperLogLog().add(redisKey, ip); 38 | } 39 | 40 | // 统计指定日期范围内的uv 41 | public long calculateUV(Date start, Date end) { 42 | if (start == null || end == null) { 43 | throw new IllegalArgumentException("参数不能为空!"); 44 | } 45 | 46 | // 整理该日期范围内的key 47 | List keyList = new ArrayList<>(); 48 | Calendar calendar = Calendar.getInstance(); 49 | calendar.setTime(start); 50 | while (!calendar.getTime().after(end)) { 51 | String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime())); 52 | keyList.add(key); 53 | calendar.add(Calendar.DATE, 1); 54 | } 55 | 56 | // 合并这些日期内的数据 57 | String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end)); 58 | redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray()); 59 | 60 | // 返回统计后的结果 61 | return redisTemplate.opsForHyperLogLog().size(redisKey); 62 | } 63 | 64 | // 将指定用户计入DAU 65 | public void recordDAU(int userId) { 66 | String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date())); 67 | redisTemplate.opsForValue().setBit(redisKey, userId, true); 68 | } 69 | 70 | // 统计指定日期范围内的DAU 71 | public long calculateDAU(Date start, Date end) { 72 | if (start == null || end == null) { 73 | throw new IllegalArgumentException("参数不能为空!"); 74 | } 75 | 76 | // 整理该日期范围内的key 77 | List keyList = new ArrayList<>(); 78 | Calendar calendar = Calendar.getInstance(); 79 | calendar.setTime(start); 80 | while (!calendar.getTime().after(end)) { 81 | String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime())); 82 | keyList.add(key.getBytes()); 83 | calendar.add(Calendar.DATE, 1); 84 | } 85 | 86 | // 进行OR 运算 87 | return (long) redisTemplate.execute(new RedisCallback() { 88 | @Override 89 | public Object doInRedis(RedisConnection connection) throws DataAccessException { 90 | String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end)); 91 | connection.bitOp(RedisStringCommands.BitOperation.OR, 92 | redisKey.getBytes(), keyList.toArray(new byte[0][0])); 93 | return connection.bitCount(redisKey.getBytes()); 94 | } 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/quartz/PostScoreRefreshJob.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.quartz; 2 | 3 | import com.nowcoder.community.entity.DiscussPost; 4 | import com.nowcoder.community.service.DiscussPostService; 5 | import com.nowcoder.community.service.ElasticsearchService; 6 | import com.nowcoder.community.service.LikeService; 7 | import com.nowcoder.community.util.CommunityConstant; 8 | import com.nowcoder.community.util.RedisKeyUtil; 9 | import org.quartz.Job; 10 | import org.quartz.JobExecutionContext; 11 | import org.quartz.JobExecutionException; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.data.redis.core.BoundSetOperations; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | 18 | import java.text.ParseException; 19 | import java.text.SimpleDateFormat; 20 | import java.util.Date; 21 | 22 | /** 23 | * @author coolsen 24 | * @version 1.0.0 25 | * @ClassName PostScoreRefreshJob.java 26 | * @Description 热帖排行 Job 27 | * @createTime 2020/5/21 14:45 28 | */ 29 | 30 | public class PostScoreRefreshJob implements Job, CommunityConstant { 31 | private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class); 32 | 33 | @Autowired 34 | private RedisTemplate redisTemplate; 35 | 36 | @Autowired 37 | private DiscussPostService discussPostService; 38 | 39 | @Autowired 40 | private LikeService likeService; 41 | 42 | @Autowired 43 | private ElasticsearchService elasticsearchService; 44 | 45 | // 论坛纪元 46 | private static final Date epoch; 47 | 48 | // 静态代码块:用staitc声明,jvm加载类时执行,仅执行一次 49 | static { 50 | try { 51 | epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2018-08-01 00:00:00"); 52 | } catch (ParseException e) { 53 | throw new RuntimeException("初始化论坛纪元失败!", e); 54 | } 55 | } 56 | 57 | @Override 58 | public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { 59 | String redisKey = RedisKeyUtil.getPostScoreKey(); 60 | BoundSetOperations operations = redisTemplate.boundSetOps(redisKey); 61 | 62 | if (operations == null) { 63 | logger.info("[任务取消] 没有要刷新的帖子!"); 64 | return; 65 | } 66 | 67 | logger.info("[任务开始] 正在刷新帖子分数:", operations.size()); 68 | while (operations.size() > 0) { 69 | refresh((Integer) operations.pop()); 70 | } 71 | logger.info("[任务结束] 帖子分数刷新完毕!"); 72 | } 73 | 74 | private void refresh(int postId) { 75 | DiscussPost post = discussPostService.findDiscussPostById(postId); 76 | if (post == null) { 77 | logger.info("该帖子不存在:id=" + postId); 78 | } 79 | 80 | // 是否精华 81 | boolean wonderful = post.getStatus() == 1; 82 | // 评论数量 83 | int commentCount = post.getCommentCount(); 84 | // 点赞数量 85 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId); 86 | 87 | // 计算权重 88 | double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2; 89 | // 分数=帖子权重+距离天数 90 | // w可能小于1,因为log存在,所以送入log的最小值应该为0 91 | // getTime()单位为ms 92 | double score = Math.log10(Math.max(1, w)) + 93 | (post.getCreateTime().getTime() - epoch.getTime()) / (3600 * 60 * 24); 94 | 95 | // 更新帖子分数 96 | discussPostService.updateScore(postId, score); 97 | // 更新elasticsearch 98 | post.setScore(score); 99 | elasticsearchService.saveDiscussPost(post); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/ShareController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.nowcoder.community.entity.Event; 4 | import com.nowcoder.community.event.EventProducer; 5 | import com.nowcoder.community.util.CommunityConstant; 6 | import com.nowcoder.community.util.CommunityUtil; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestMethod; 16 | import org.springframework.web.bind.annotation.ResponseBody; 17 | 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | /** 27 | * @author coolsen 28 | * @version 1.0.0 29 | * @ClassName ShareController.java 30 | * @Description Share Controller eg.images 31 | * @createTime 2020/5/21 19:44 32 | */ 33 | 34 | @Controller 35 | public class ShareController implements CommunityConstant { 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(ShareController.class); 38 | 39 | @Autowired 40 | private EventProducer eventProducer; 41 | 42 | @Value("${community.path.domain}") 43 | private String domain; 44 | 45 | @Value("${server.servlet.context-path}") 46 | private String contextPath; 47 | 48 | @Value("${wk.image.storage}") 49 | private String wkImageStorage; 50 | 51 | @Value("${qiniu.bucket.share.url}") 52 | private String shareBucketUrl; 53 | 54 | @RequestMapping(path = "/share", method = RequestMethod.GET) 55 | @ResponseBody 56 | public String share(String htmlUrl) { 57 | // 文件名 58 | String fileName = CommunityUtil.generateUUID(); 59 | 60 | // 异步生成长图 61 | Event event = new Event() 62 | .setTopic(TOPIC_SHARE) 63 | .setData("htmlUrl", htmlUrl) 64 | .setData("fileName", fileName) 65 | .setData("suffix", ".png"); 66 | eventProducer.fireEvent(event); 67 | 68 | // 返回访问路径 69 | Map map = new HashMap<>(); 70 | 71 | // 废弃,这是原来本地的方式 72 | // map.put("shareUrl", domain + contextPath + "/share/image/" + fileName); 73 | 74 | map.put("shareUrl", shareBucketUrl + "/" + fileName); 75 | 76 | return CommunityUtil.getJSONString(0, null, map); 77 | } 78 | 79 | // 废弃,这是访问本地pc的文件,目前改用七牛云 80 | // 获取长图 81 | @RequestMapping(path = "/share/image/{fileName}", method = RequestMethod.GET) 82 | public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response) { 83 | if (StringUtils.isBlank(fileName)) { 84 | throw new IllegalArgumentException("文件名不能为空!"); 85 | } 86 | 87 | response.setContentType("image/png"); 88 | File file = new File(wkImageStorage + "/" + fileName + ".png"); 89 | try { 90 | OutputStream os = response.getOutputStream(); 91 | FileInputStream fis = new FileInputStream(file); 92 | byte[] buffer = new byte[1024]; 93 | int b = 0; 94 | while ((b = fis.read(buffer)) != -1) { 95 | os.write(buffer, 0, b); 96 | } 97 | } catch (IOException e) { 98 | logger.error("获取长图失败: " + e.getMessage()); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/ThreadPoolTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import com.nowcoder.community.service.AlphaService; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.scheduling.annotation.Async; 11 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 12 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import java.util.Date; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.ScheduledExecutorService; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | @RunWith(SpringRunner.class) 23 | @SpringBootTest 24 | @ContextConfiguration(classes = CommunityApplication.class) 25 | public class ThreadPoolTests { 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class); 28 | 29 | // JDK普通线程池 30 | private ExecutorService executorService = Executors.newFixedThreadPool(5); 31 | 32 | // JDK可执行定时任务的线程池 33 | private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); 34 | 35 | // Spring普通线程池 36 | @Autowired 37 | private ThreadPoolTaskExecutor taskExecutor; 38 | 39 | // Spring可执行定时任务的线程池 40 | @Autowired 41 | private ThreadPoolTaskScheduler taskScheduler; 42 | 43 | @Autowired 44 | private AlphaService alphaService; 45 | 46 | private void sleep(long m) { 47 | try { 48 | Thread.sleep(m); 49 | } catch (InterruptedException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | 54 | // 1.JDK普通线程池 55 | @Test 56 | public void testExecutorService() { 57 | Runnable task = new Runnable() { 58 | @Override 59 | public void run() { 60 | logger.debug("Hello ExecutorService"); 61 | } 62 | }; 63 | 64 | for (int i = 0; i < 10; i++) { 65 | executorService.submit(task); 66 | } 67 | 68 | sleep(10000); 69 | } 70 | 71 | // 2.JDK定时任务线程池 72 | @Test 73 | public void testScheduledExecutorService() { 74 | Runnable task = new Runnable() { 75 | @Override 76 | public void run() { 77 | logger.debug("Hello ScheduledExecutorService"); 78 | } 79 | }; 80 | 81 | scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS); 82 | 83 | sleep(30000); 84 | } 85 | 86 | // 3.Spring普通线程池 87 | @Test 88 | public void testThreadPoolTaskExecutor() { 89 | Runnable task = new Runnable() { 90 | @Override 91 | public void run() { 92 | logger.debug("Hello ThreadPoolTaskExecutor"); 93 | } 94 | }; 95 | 96 | for (int i = 0; i < 10; i++) { 97 | taskExecutor.submit(task); 98 | } 99 | 100 | sleep(10000); 101 | } 102 | 103 | // 4.Spring定时任务线程池 104 | @Test 105 | public void testThreadPoolTaskScheduler() { 106 | Runnable task = new Runnable() { 107 | @Override 108 | public void run() { 109 | logger.debug("Hello ThreadPoolTaskScheduler"); 110 | } 111 | }; 112 | 113 | Date startTime = new Date(System.currentTimeMillis() + 10000); 114 | taskScheduler.scheduleAtFixedRate(task, startTime, 1000); 115 | 116 | sleep(30000); 117 | } 118 | 119 | // 5.Spring普通线程池(简化) 120 | @Test 121 | public void testThreadPoolTaskExecutorSimple() { 122 | for (int i = 0; i < 10; i++) { 123 | alphaService.execute1(); 124 | } 125 | 126 | sleep(10000); 127 | } 128 | 129 | // 6.Spring定时任务线程池(简化) 130 | @Test 131 | public void testThreadPoolTaskSchedulerSimple() { 132 | sleep(30000); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/resources/mapper/message-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | id, from_id, to_id, conversation_id, content, status, create_time 9 | 10 | 11 | 12 | from_id, to_id, conversation_id, content, status, create_time 13 | 14 | 15 | 29 | 30 | 39 | 40 | 50 | 51 | 58 | 59 | 69 | 70 | 71 | insert into message() 72 | values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime}) 73 | 74 | 75 | 76 | update message set status = #{status} 77 | where id in 78 | 79 | #{id} 80 | 81 | 82 | 83 | 95 | 96 | 103 | 104 | 105 | 114 | 115 | 126 | 127 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring-produce.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | community 4 | 5 | 6 | 7 | 8 | 9 | ${LOG_PATH}/${APPDIR}/log_error.log 10 | 11 | ${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log 12 | 13 | 5MB 14 | 15 | 30 16 | 17 | true 18 | 19 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 20 | utf-8 21 | 22 | 23 | error 24 | ACCEPT 25 | DENY 26 | 27 | 28 | 29 | 30 | 31 | ${LOG_PATH}/${APPDIR}/log_warn.log 32 | 33 | ${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log 34 | 35 | 5MB 36 | 37 | 30 38 | 39 | true 40 | 41 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 42 | utf-8 43 | 44 | 45 | warn 46 | ACCEPT 47 | DENY 48 | 49 | 50 | 51 | 52 | 53 | ${LOG_PATH}/${APPDIR}/log_info.log 54 | 55 | ${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log 56 | 57 | 5MB 58 | 59 | 30 60 | 61 | true 62 | 63 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 64 | utf-8 65 | 66 | 67 | info 68 | ACCEPT 69 | DENY 70 | 71 | 72 | 73 | 74 | 75 | 76 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 77 | utf-8 78 | 79 | 80 | debug 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | community 4 | 5 | 6 | 7 | 8 | 9 | ${LOG_PATH}/${APPDIR}/log_error.log 10 | 11 | ${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log 12 | 13 | 5MB 14 | 15 | 30 16 | 17 | true 18 | 19 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 20 | utf-8 21 | 22 | 23 | error 24 | ACCEPT 25 | DENY 26 | 27 | 28 | 29 | 30 | 31 | ${LOG_PATH}/${APPDIR}/log_warn.log 32 | 33 | ${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log 34 | 35 | 5MB 36 | 37 | 30 38 | 39 | true 40 | 41 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 42 | utf-8 43 | 44 | 45 | warn 46 | ACCEPT 47 | DENY 48 | 49 | 50 | 51 | 52 | 53 | ${LOG_PATH}/${APPDIR}/log_info.log 54 | 55 | ${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log 56 | 57 | 5MB 58 | 59 | 30 60 | 61 | true 62 | 63 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 64 | utf-8 65 | 66 | 67 | info 68 | ACCEPT 69 | DENY 70 | 71 | 72 | 73 | 74 | 75 | 76 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 77 | utf-8 78 | 79 | 80 | debug 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring-develop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | community 4 | 5 | 6 | 7 | 8 | 9 | ${LOG_PATH}/${APPDIR}/log_error.log 10 | 11 | ${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log 12 | 13 | 5MB 14 | 15 | 30 16 | 17 | true 18 | 19 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 20 | utf-8 21 | 22 | 23 | error 24 | ACCEPT 25 | DENY 26 | 27 | 28 | 29 | 30 | 31 | ${LOG_PATH}/${APPDIR}/log_warn.log 32 | 33 | ${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log 34 | 35 | 5MB 36 | 37 | 30 38 | 39 | true 40 | 41 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 42 | utf-8 43 | 44 | 45 | warn 46 | ACCEPT 47 | DENY 48 | 49 | 50 | 51 | 52 | 53 | ${LOG_PATH}/${APPDIR}/log_info.log 54 | 55 | ${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log 56 | 57 | 5MB 58 | 59 | 30 60 | 61 | true 62 | 63 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 64 | utf-8 65 | 66 | 67 | info 68 | ACCEPT 69 | DENY 70 | 71 | 72 | 73 | 74 | 75 | 76 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 77 | utf-8 78 | 79 | 80 | debug 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/MapperTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import com.nowcoder.community.dao.DiscussPostMapper; 4 | import com.nowcoder.community.dao.LoginTicketMapper; 5 | import com.nowcoder.community.dao.MessageMapper; 6 | import com.nowcoder.community.dao.UserMapper; 7 | import com.nowcoder.community.entity.DiscussPost; 8 | import com.nowcoder.community.entity.LoginTicket; 9 | import com.nowcoder.community.entity.Message; 10 | import com.nowcoder.community.entity.User; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.test.context.ContextConfiguration; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | 18 | import java.util.Date; 19 | import java.util.List; 20 | 21 | @RunWith(SpringRunner.class) 22 | @SpringBootTest 23 | @ContextConfiguration(classes = CommunityApplication.class) 24 | public class MapperTests { 25 | 26 | @Autowired 27 | private UserMapper userMapper; 28 | 29 | @Autowired 30 | private DiscussPostMapper discussPostMapper; 31 | 32 | // @Autowired 33 | // private LoginTicketMapper loginTicketMapper; 34 | 35 | @Autowired 36 | private MessageMapper messageMapper; 37 | 38 | @Test 39 | public void testSelectUser() { 40 | User user = userMapper.selectById(101); 41 | System.out.println(user); 42 | 43 | user = userMapper.selectByName("liubei"); 44 | System.out.println(user); 45 | 46 | user = userMapper.selectByEmail("nowcoder101@sina.com"); 47 | System.out.println(user); 48 | } 49 | 50 | @Test 51 | public void testInsertUser() { 52 | User user = new User(); 53 | user.setUsername("test"); 54 | user.setPassword("123456"); 55 | user.setSalt("abc"); 56 | user.setEmail("test@qq.com"); 57 | user.setHeaderUrl("http://www.nowcoder.com/101.png"); 58 | user.setCreateTime(new Date()); 59 | 60 | int rows = userMapper.insertUser(user); 61 | System.out.println(rows); 62 | System.out.println(user.getId()); 63 | } 64 | 65 | @Test 66 | public void updateUser() { 67 | int rows = userMapper.updateStatus(150, 1); 68 | System.out.println(rows); 69 | 70 | rows = userMapper.updateHeader(150, "http://www.nowcoder.com/102.png"); 71 | System.out.println(rows); 72 | 73 | rows = userMapper.updatePassword(150, "hello"); 74 | System.out.println(rows); 75 | } 76 | 77 | @Test 78 | public void testSelectPosts() { 79 | List list = discussPostMapper.selectDiscussPosts(149, 0, 10,0); 80 | for (DiscussPost post : list) { 81 | System.out.println(post); 82 | } 83 | 84 | int rows = discussPostMapper.selectDiscussPostRows(149); 85 | System.out.println(rows); 86 | } 87 | 88 | @Test 89 | public void testInsertLoginTicket() { 90 | LoginTicket loginTicket = new LoginTicket(); 91 | loginTicket.setUserId(101); 92 | loginTicket.setTicket("abc"); 93 | loginTicket.setStatus(0); 94 | loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10)); 95 | 96 | // loginTicketMapper.insertLoginTicket(loginTicket); 97 | } 98 | 99 | @Test 100 | public void testSelectLoginTicket() { 101 | // LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc"); 102 | // System.out.println(loginTicket); 103 | // 104 | // loginTicketMapper.updateStatus("abc", 1); 105 | // loginTicket = loginTicketMapper.selectByTicket("abc"); 106 | // System.out.println(loginTicket); 107 | } 108 | 109 | @Test 110 | public void testSelectLetters() { 111 | List list = messageMapper.selectConversations(111, 0, 20); 112 | for (Message message : list) { 113 | System.out.println(message); 114 | } 115 | 116 | int count = messageMapper.selectConversationCount(111); 117 | System.out.println(count); 118 | 119 | list = messageMapper.selectLetters("111_112", 0, 10); 120 | for (Message message : list) { 121 | System.out.println(message); 122 | } 123 | 124 | count = messageMapper.selectLetterCount("111_112"); 125 | System.out.println(count); 126 | 127 | count = messageMapper.selectLetterUnreadCount(131, "111_131"); 128 | System.out.println(count); 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/controller/FollowController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.controller; 2 | 3 | import com.nowcoder.community.entity.Event; 4 | import com.nowcoder.community.entity.Page; 5 | import com.nowcoder.community.entity.User; 6 | import com.nowcoder.community.event.EventProducer; 7 | import com.nowcoder.community.service.FollowService; 8 | import com.nowcoder.community.service.UserService; 9 | import com.nowcoder.community.util.CommunityConstant; 10 | import com.nowcoder.community.util.CommunityUtil; 11 | import com.nowcoder.community.util.HostHolder; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * @author coolsen 22 | * @version 1.0.0 23 | * @ClassName FollowController.java 24 | * @Description 关注 Controller 25 | * @createTime 2020/5/13 20:24 26 | */ 27 | 28 | @Controller 29 | public class FollowController implements CommunityConstant { 30 | 31 | @Autowired 32 | private FollowService followService; 33 | 34 | @Autowired 35 | private HostHolder hostHolder; 36 | 37 | @Autowired 38 | private UserService userService; 39 | 40 | @Autowired 41 | private EventProducer eventProducer; 42 | 43 | 44 | @RequestMapping(path = "/follow", method = RequestMethod.POST) 45 | @ResponseBody 46 | public String follow(int entityType, int entityId) { 47 | User user = hostHolder.getUser(); 48 | 49 | followService.follow(user.getId(), entityType, entityId); 50 | 51 | // 注意:目前只实现了关注人,所以setEntityUserId为entityId 52 | Event event = new Event() 53 | .setTopic(TOPIC_FOLLOW) 54 | .setUserId(hostHolder.getUser().getId()) 55 | .setEntityType(entityType) 56 | .setEntityId(entityId) 57 | .setEntityUserId(entityId); 58 | eventProducer.fireEvent(event); 59 | return CommunityUtil.getJSONString(0, "已关注!"); 60 | } 61 | 62 | @RequestMapping(path = "/unfollow", method = RequestMethod.POST) 63 | @ResponseBody 64 | public String unfollow(int entityType, int entityId) { 65 | User user = hostHolder.getUser(); 66 | 67 | followService.unfollow(user.getId(), entityType, entityId); 68 | 69 | return CommunityUtil.getJSONString(0, "已取消关注!"); 70 | } 71 | 72 | @RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET) 73 | public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) { 74 | User user = userService.findUserById(userId); 75 | if (user == null) { 76 | throw new RuntimeException("该用户不存在!"); 77 | } 78 | model.addAttribute("user", user); 79 | 80 | page.setLimit(5); 81 | page.setPath("/followees/" + userId); 82 | page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER)); 83 | 84 | List> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit()); 85 | if (userList != null) { 86 | for (Map map : userList) { 87 | User u = (User) map.get("user"); 88 | map.put("hasFollowed", hasFollowed(u.getId())); 89 | } 90 | } 91 | model.addAttribute("users", userList); 92 | 93 | return "/site/followee"; 94 | } 95 | 96 | @RequestMapping(path = "/followers/{userId}", method = RequestMethod.GET) 97 | public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) { 98 | User user = userService.findUserById(userId); 99 | if (user == null) { 100 | throw new RuntimeException("该用户不存在!"); 101 | } 102 | model.addAttribute("user", user); 103 | 104 | page.setLimit(5); 105 | page.setPath("/followers/" + userId); 106 | page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId)); 107 | 108 | List> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit()); 109 | if (userList != null) { 110 | for (Map map : userList) { 111 | User u = (User) map.get("user"); 112 | map.put("hasFollowed", hasFollowed(u.getId())); 113 | } 114 | } 115 | model.addAttribute("users", userList); 116 | 117 | return "/site/follower"; 118 | } 119 | 120 | private boolean hasFollowed(int userId) { 121 | if (hostHolder.getUser() == null) { 122 | return false; 123 | } 124 | 125 | return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/service/AlphaService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.service; 2 | 3 | import com.nowcoder.community.dao.AlphaDao; 4 | import com.nowcoder.community.dao.DiscussPostMapper; 5 | import com.nowcoder.community.dao.UserMapper; 6 | import com.nowcoder.community.entity.DiscussPost; 7 | import com.nowcoder.community.entity.User; 8 | import com.nowcoder.community.util.CommunityUtil; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.context.annotation.Scope; 13 | import org.springframework.scheduling.annotation.Async; 14 | import org.springframework.scheduling.annotation.Scheduled; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.TransactionDefinition; 17 | import org.springframework.transaction.TransactionStatus; 18 | import org.springframework.transaction.annotation.Isolation; 19 | import org.springframework.transaction.annotation.Propagation; 20 | import org.springframework.transaction.annotation.Transactional; 21 | import org.springframework.transaction.support.TransactionCallback; 22 | import org.springframework.transaction.support.TransactionTemplate; 23 | 24 | import javax.annotation.PostConstruct; 25 | import javax.annotation.PreDestroy; 26 | import java.util.Date; 27 | 28 | @Service 29 | //@Scope("prototype") 30 | public class AlphaService { 31 | 32 | private static final Logger logger = LoggerFactory.getLogger(AlphaService.class); 33 | 34 | @Autowired 35 | private AlphaDao alphaDao; 36 | 37 | @Autowired 38 | private UserMapper userMapper; 39 | 40 | @Autowired 41 | private DiscussPostMapper discussPostMapper; 42 | 43 | @Autowired 44 | private TransactionTemplate transactionTemplate; 45 | 46 | public AlphaService() { 47 | // System.out.println("实例化AlphaService"); 48 | } 49 | 50 | @PostConstruct 51 | public void init() { 52 | // System.out.println("初始化AlphaService"); 53 | } 54 | 55 | @PreDestroy 56 | public void destroy() { 57 | // System.out.println("销毁AlphaService"); 58 | } 59 | 60 | public String find() { 61 | return alphaDao.select(); 62 | } 63 | 64 | // REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务. 65 | // REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务). 66 | // NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样. 67 | @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) 68 | public Object save1() { 69 | // 新增用户 70 | User user = new User(); 71 | user.setUsername("alpha"); 72 | user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); 73 | user.setPassword(CommunityUtil.md5("123" + user.getSalt())); 74 | user.setEmail("alpha@qq.com"); 75 | user.setHeaderUrl("http://image.nowcoder.com/head/99t.png"); 76 | user.setCreateTime(new Date()); 77 | userMapper.insertUser(user); 78 | 79 | // 新增帖子 80 | DiscussPost post = new DiscussPost(); 81 | post.setUserId(user.getId()); 82 | post.setTitle("Hello"); 83 | post.setContent("新人报道!"); 84 | post.setCreateTime(new Date()); 85 | discussPostMapper.insertDiscussPost(post); 86 | 87 | Integer.valueOf("abc"); 88 | 89 | return "ok"; 90 | } 91 | 92 | public Object save2() { 93 | transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); 94 | transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); 95 | 96 | return transactionTemplate.execute(new TransactionCallback() { 97 | @Override 98 | public Object doInTransaction(TransactionStatus status) { 99 | // 新增用户 100 | User user = new User(); 101 | user.setUsername("beta"); 102 | user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); 103 | user.setPassword(CommunityUtil.md5("123" + user.getSalt())); 104 | user.setEmail("beta@qq.com"); 105 | user.setHeaderUrl("http://image.nowcoder.com/head/999t.png"); 106 | user.setCreateTime(new Date()); 107 | userMapper.insertUser(user); 108 | 109 | // 新增帖子 110 | DiscussPost post = new DiscussPost(); 111 | post.setUserId(user.getId()); 112 | post.setTitle("你好"); 113 | post.setContent("我是新人!"); 114 | post.setCreateTime(new Date()); 115 | discussPostMapper.insertDiscussPost(post); 116 | 117 | Integer.valueOf("abc"); 118 | 119 | return "ok"; 120 | } 121 | }); 122 | } 123 | 124 | // 让该方法在多线程环境下,被异步的调用. 125 | @Async 126 | public void execute1() { 127 | logger.debug("execute1"); 128 | } 129 | 130 | /*@Scheduled(initialDelay = 10000, fixedRate = 1000)*/ 131 | public void execute2() { 132 | logger.debug("execute2"); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.config; 2 | 3 | import com.nowcoder.community.util.CommunityConstant; 4 | import com.nowcoder.community.util.CommunityUtil; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.access.AccessDeniedException; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.web.AuthenticationEntryPoint; 13 | import org.springframework.security.web.access.AccessDeniedHandler; 14 | 15 | import javax.servlet.ServletException; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | import java.io.PrintWriter; 20 | 21 | /** 22 | * @author coolsen 23 | * @version 1.0.0 24 | * @ClassName SecurityConfig.java 25 | * @Description Spring Security Config 26 | * @createTime 2020/5/20 11:27 27 | */ 28 | 29 | @Configuration 30 | public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant { 31 | 32 | @Override 33 | public void configure(WebSecurity web) throws Exception { 34 | // 忽略静态资源的访问 35 | web.ignoring().antMatchers("/resources/**"); 36 | } 37 | 38 | // 授权 39 | @Override 40 | protected void configure(HttpSecurity http) throws Exception { 41 | // 授权 42 | // TODO: 所有异步请求要开启csrf 43 | http.authorizeRequests() 44 | .antMatchers( 45 | "/user/setting", 46 | "/user/upload", 47 | "/discuss/add", 48 | "/comment/add/**", 49 | "/letter/**", 50 | "/notice/**", 51 | "/like", 52 | "/follow", 53 | "/unfollow" 54 | ) 55 | .hasAnyAuthority( 56 | AUTHORITY_USER, 57 | AUTHORITY_ADMIN, 58 | AUTHORITY_MODERATOR 59 | ) 60 | .antMatchers( 61 | "/discuss/top", 62 | "/discuss/wonderful" 63 | ) 64 | .hasAnyAuthority( 65 | AUTHORITY_MODERATOR 66 | ) 67 | .antMatchers( 68 | "/discuss/delete", 69 | "/data/**", 70 | "/actuator/**" 71 | ) 72 | .hasAnyAuthority( 73 | AUTHORITY_ADMIN 74 | ) 75 | .anyRequest().permitAll() 76 | .and().csrf().disable(); 77 | 78 | // 权限不够时的处理 79 | http.exceptionHandling() 80 | .authenticationEntryPoint(new AuthenticationEntryPoint() { 81 | // 没有登录 82 | @Override 83 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { 84 | String xRequestedWith = request.getHeader("x-requested-with"); 85 | // 判断是普通请求还是异步请求,进行不同的处理 86 | if ("XMLHttpRequest".equals(xRequestedWith)) { 87 | response.setContentType("application/plain;charset=utf-8"); 88 | PrintWriter writer = response.getWriter(); 89 | writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!")); 90 | } else { 91 | response.sendRedirect(request.getContextPath() + "/login"); 92 | } 93 | } 94 | }) 95 | .accessDeniedHandler(new AccessDeniedHandler() { 96 | // 权限不足 97 | @Override 98 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { 99 | String xRequestedWith = request.getHeader("x-requested-with"); 100 | if ("XMLHttpRequest".equals(xRequestedWith)) { 101 | response.setContentType("application/plain;charset=utf-8"); 102 | PrintWriter writer = response.getWriter(); 103 | writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!")); 104 | } else { 105 | response.sendRedirect(request.getContextPath() + "/denied"); 106 | } 107 | } 108 | }); 109 | // 为了执行自己的logout 110 | // Security底层默认会拦截/logout请求,进行退出处理. 111 | // 覆盖它默认的逻辑,才能执行我们自己的退出代码. 112 | // 此处为一个欺骗,程序中没有"/securitylogout",拦截到这个路径不会处理 113 | http.logout().logoutUrl("/securitylogout"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/util/SensitiveFilter.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.util; 2 | 3 | import org.apache.commons.lang3.CharUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * @author coolsen 19 | * @version 1.0.0 20 | * @ClassName SensetiveFilter.java 21 | * @Description 敏感词过滤 22 | * @createTime 5/6/2020 12:40 PM 23 | */ 24 | 25 | @Component 26 | public class SensitiveFilter { 27 | private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class); 28 | 29 | // 替换符 30 | private static final String REPLACEMENT = "***"; 31 | 32 | // 根节点 33 | private TrieNode rootNode = new TrieNode(); 34 | 35 | 36 | 37 | @PostConstruct 38 | public void init() { 39 | try ( 40 | InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); 41 | BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 42 | ) { 43 | String keyword; 44 | while ((keyword = reader.readLine()) != null) { 45 | // 添加到前缀树 46 | this.addKeyword(keyword); 47 | } 48 | } catch (IOException e) { 49 | logger.error("加载敏感词文件失败: " + e.getMessage()); 50 | } 51 | } 52 | 53 | // 将一个敏感词添加到前缀树中 54 | private void addKeyword(String keyword) { 55 | TrieNode tempNode = rootNode; 56 | for (int i = 0; i < keyword.length(); i++) { 57 | char c = keyword.charAt(i); 58 | TrieNode subNode = tempNode.getSubNode(c); 59 | 60 | if (subNode == null) { 61 | // 初始化子节点 62 | subNode = new TrieNode(); 63 | tempNode.addSubNode(c, subNode); 64 | } 65 | 66 | // 指向子节点,进入下一轮循环 67 | tempNode = subNode; 68 | 69 | // 设置结束标识 70 | if (i == keyword.length() - 1) { 71 | tempNode.setKeywordEnd(true); 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * 过滤敏感词 78 | * 79 | * @param text 待过滤的文本 80 | * @return 过滤后的文本 81 | */ 82 | public String filter(String text) { 83 | if (StringUtils.isBlank(text)) { 84 | return null; 85 | } 86 | 87 | // 指针1 88 | TrieNode tempNode = rootNode; 89 | // 指针2 90 | int begin = 0; 91 | // 指针3 92 | int position = 0; 93 | // 结果 94 | StringBuilder sb = new StringBuilder(); 95 | 96 | while (position < text.length()) { 97 | char c = text.charAt(position); 98 | 99 | // 跳过符号 100 | if (isSymbol(c)) { 101 | // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步 102 | if (tempNode == rootNode) { 103 | sb.append(c); 104 | begin++; 105 | } 106 | // 无论符号在开头或中间,指针3都向下走一步 107 | position++; 108 | continue; 109 | } 110 | 111 | // 检查下级节点 112 | tempNode = tempNode.getSubNode(c); 113 | if (tempNode == null) { 114 | // 以begin开头的字符串不是敏感词 115 | sb.append(text.charAt(begin)); 116 | // 进入下一个位置 117 | position = ++begin; 118 | // 重新指向根节点 119 | tempNode = rootNode; 120 | } else if (tempNode.isKeywordEnd()) { 121 | // 发现敏感词,将begin~position字符串替换掉 122 | sb.append(REPLACEMENT); 123 | // 进入下一个位置 124 | begin = ++position; 125 | // 重新指向根节点 126 | tempNode = rootNode; 127 | } else { 128 | // 检查下一个字符 129 | position++; 130 | } 131 | } 132 | 133 | // 将最后一批字符计入结果 134 | sb.append(text.substring(begin)); 135 | 136 | return sb.toString(); 137 | } 138 | 139 | // 判断是否为符号 140 | private boolean isSymbol(Character c) { 141 | // 0x2E80~0x9FFF 是东亚文字范围 142 | return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF); 143 | } 144 | 145 | // 前缀树 146 | private class TrieNode { 147 | 148 | // 关键词结束标识 149 | private boolean isKeywordEnd = false; 150 | 151 | // 子节点(key是下级字符,value是下级节点) 152 | private Map subNodes = new HashMap<>(); 153 | 154 | public boolean isKeywordEnd() { 155 | return isKeywordEnd; 156 | } 157 | 158 | public void setKeywordEnd(boolean keywordEnd) { 159 | isKeywordEnd = keywordEnd; 160 | } 161 | 162 | // 添加子节点 163 | public void addSubNode(Character c, TrieNode node) { 164 | subNodes.put(c, node); 165 | } 166 | 167 | // 获取子节点 168 | public TrieNode getSubNode(Character c) { 169 | return subNodes.get(c); 170 | } 171 | 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /sql/init_schema.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 8.0.15, for Win64 (x86_64) 2 | -- 3 | -- Host: localhost Database: community 4 | -- ------------------------------------------------------ 5 | -- Server version 8.0.15 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | SET NAMES utf8 ; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `comment` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `comment`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | SET character_set_client = utf8mb4 ; 25 | CREATE TABLE `comment` ( 26 | `id` int(11) NOT NULL AUTO_INCREMENT, 27 | `user_id` int(11) DEFAULT NULL, 28 | `entity_type` int(11) DEFAULT NULL, 29 | `entity_id` int(11) DEFAULT NULL, 30 | `target_id` int(11) DEFAULT NULL, 31 | `content` text, 32 | `status` int(11) DEFAULT NULL, 33 | `create_time` timestamp NULL DEFAULT NULL, 34 | PRIMARY KEY (`id`), 35 | KEY `index_user_id` (`user_id`) /*!80000 INVISIBLE */, 36 | KEY `index_entity_id` (`entity_id`) 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 38 | /*!40101 SET character_set_client = @saved_cs_client */; 39 | 40 | -- 41 | -- Table structure for table `discuss_post` 42 | -- 43 | 44 | DROP TABLE IF EXISTS `discuss_post`; 45 | /*!40101 SET @saved_cs_client = @@character_set_client */; 46 | SET character_set_client = utf8mb4 ; 47 | CREATE TABLE `discuss_post` ( 48 | `id` int(11) NOT NULL AUTO_INCREMENT, 49 | `user_id` varchar(45) DEFAULT NULL, 50 | `title` varchar(100) DEFAULT NULL, 51 | `content` text, 52 | `type` int(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;', 53 | `status` int(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;', 54 | `create_time` timestamp NULL DEFAULT NULL, 55 | `comment_count` int(11) DEFAULT NULL, 56 | `score` double DEFAULT NULL, 57 | PRIMARY KEY (`id`), 58 | KEY `index_user_id` (`user_id`) 59 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 60 | /*!40101 SET character_set_client = @saved_cs_client */; 61 | 62 | -- 63 | -- Table structure for table `login_ticket` 64 | -- 65 | 66 | DROP TABLE IF EXISTS `login_ticket`; 67 | /*!40101 SET @saved_cs_client = @@character_set_client */; 68 | SET character_set_client = utf8mb4 ; 69 | CREATE TABLE `login_ticket` ( 70 | `id` int(11) NOT NULL AUTO_INCREMENT, 71 | `user_id` int(11) NOT NULL, 72 | `ticket` varchar(45) NOT NULL, 73 | `status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;', 74 | `expired` timestamp NOT NULL, 75 | PRIMARY KEY (`id`), 76 | KEY `index_ticket` (`ticket`(20)) 77 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 78 | /*!40101 SET character_set_client = @saved_cs_client */; 79 | 80 | -- 81 | -- Table structure for table `message` 82 | -- 83 | 84 | DROP TABLE IF EXISTS `message`; 85 | /*!40101 SET @saved_cs_client = @@character_set_client */; 86 | SET character_set_client = utf8mb4 ; 87 | CREATE TABLE `message` ( 88 | `id` int(11) NOT NULL AUTO_INCREMENT, 89 | `from_id` int(11) DEFAULT NULL, 90 | `to_id` int(11) DEFAULT NULL, 91 | `conversation_id` varchar(45) NOT NULL, 92 | `content` text, 93 | `status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;', 94 | `create_time` timestamp NULL DEFAULT NULL, 95 | PRIMARY KEY (`id`), 96 | KEY `index_from_id` (`from_id`), 97 | KEY `index_to_id` (`to_id`), 98 | KEY `index_conversation_id` (`conversation_id`) 99 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 100 | /*!40101 SET character_set_client = @saved_cs_client */; 101 | 102 | -- 103 | -- Table structure for table `user` 104 | -- 105 | 106 | DROP TABLE IF EXISTS `user`; 107 | /*!40101 SET @saved_cs_client = @@character_set_client */; 108 | SET character_set_client = utf8mb4 ; 109 | CREATE TABLE `user` ( 110 | `id` int(11) NOT NULL AUTO_INCREMENT, 111 | `username` varchar(50) DEFAULT NULL, 112 | `password` varchar(50) DEFAULT NULL, 113 | `salt` varchar(50) DEFAULT NULL, 114 | `email` varchar(100) DEFAULT NULL, 115 | `type` int(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;', 116 | `status` int(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;', 117 | `activation_code` varchar(100) DEFAULT NULL, 118 | `header_url` varchar(200) DEFAULT NULL, 119 | `create_time` timestamp NULL DEFAULT NULL, 120 | PRIMARY KEY (`id`), 121 | KEY `index_username` (`username`(20)), 122 | KEY `index_email` (`email`(20)) 123 | ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8; 124 | /*!40101 SET character_set_client = @saved_cs_client */; 125 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 126 | 127 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 128 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 129 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 130 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 131 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 132 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 133 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 134 | 135 | -- Dump completed on 2019-05-05 19:37:53 136 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.5.RELEASE 9 | 10 | 11 | com.nowcoder.community 12 | community 13 | 0.0.1-SNAPSHOT 14 | community 15 | nowcoder community 16 | war 17 | 18 | 19 | 11 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-aop 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-thymeleaf 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-devtools 39 | runtime 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | 48 | mysql 49 | mysql-connector-java 50 | 8.0.19 51 | 52 | 53 | org.mybatis.spring.boot 54 | mybatis-spring-boot-starter 55 | 2.0.1 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-mail 61 | 2.1.5.RELEASE 62 | 63 | 64 | org.apache.commons 65 | commons-lang3 66 | 3.9 67 | 68 | 69 | 70 | 71 | com.github.penggle 72 | kaptcha 73 | 2.3.2 74 | 75 | 76 | 77 | com.alibaba 78 | fastjson 79 | 1.2.58 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-data-redis 85 | 86 | 87 | 88 | org.springframework.kafka 89 | spring-kafka 90 | 91 | 92 | 93 | org.springframework.boot 94 | spring-boot-starter-data-elasticsearch 95 | 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-starter-security 100 | 101 | 102 | 103 | org.thymeleaf.extras 104 | thymeleaf-extras-springsecurity5 105 | 106 | 107 | 108 | org.springframework.boot 109 | spring-boot-starter-quartz 110 | 111 | 112 | 113 | com.qiniu 114 | qiniu-java-sdk 115 | 7.2.23 116 | 117 | 118 | 119 | com.github.ben-manes.caffeine 120 | caffeine 121 | 2.7.0 122 | 123 | 124 | 125 | org.springframework.boot 126 | spring-boot-starter-actuator 127 | 128 | 129 | 130 | 131 | 132 | ROOT 133 | 134 | 135 | org.springframework.boot 136 | spring-boot-maven-plugin 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/community/RedisTest.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.dao.DataAccessException; 8 | import org.springframework.data.redis.core.BoundValueOperations; 9 | import org.springframework.data.redis.core.RedisOperations; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | import org.springframework.data.redis.core.SessionCallback; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import javax.swing.*; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * @author coolsen 20 | * @version 1.0.0 21 | * @ClassName RedisTest.java 22 | * @Description RedisTest 23 | * @createTime 5/9/2020 12:48 PM 24 | */ 25 | 26 | @RunWith(SpringRunner.class) 27 | @SpringBootTest 28 | @ContextConfiguration(classes = CommunityApplication.class) 29 | public class RedisTest { 30 | @Autowired 31 | private RedisTemplate redisTemplate; 32 | 33 | @Test 34 | public void testStrings() { 35 | String redisKey = "test:count"; 36 | 37 | redisTemplate.opsForValue().set(redisKey, 1); 38 | 39 | System.out.println(redisTemplate.opsForValue().get(redisKey)); 40 | System.out.println(redisTemplate.opsForValue().increment(redisKey)); 41 | System.out.println(redisTemplate.opsForValue().decrement(redisKey)); 42 | } 43 | 44 | @Test 45 | public void testHashes() { 46 | String redisKey = "test:user"; 47 | 48 | redisTemplate.opsForHash().put(redisKey, "id", 1); 49 | redisTemplate.opsForHash().put(redisKey, "username", "zhangsan"); 50 | 51 | System.out.println(redisTemplate.opsForHash().get(redisKey, "id")); 52 | System.out.println(redisTemplate.opsForHash().get(redisKey, "username")); 53 | } 54 | 55 | @Test 56 | public void testLists() { 57 | String redisKey = "test:ids"; 58 | 59 | redisTemplate.opsForList().leftPush(redisKey, 101); 60 | redisTemplate.opsForList().leftPush(redisKey, 102); 61 | redisTemplate.opsForList().leftPush(redisKey, 103); 62 | 63 | System.out.println(redisTemplate.opsForList().size(redisKey)); 64 | System.out.println(redisTemplate.opsForList().index(redisKey, 0)); 65 | System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2)); 66 | 67 | System.out.println(redisTemplate.opsForList().leftPop(redisKey)); 68 | System.out.println(redisTemplate.opsForList().leftPop(redisKey)); 69 | System.out.println(redisTemplate.opsForList().leftPop(redisKey)); 70 | } 71 | 72 | @Test 73 | public void testSets() { 74 | String redisKey = "test:teachers"; 75 | 76 | redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮"); 77 | 78 | System.out.println(redisTemplate.opsForSet().size(redisKey)); 79 | System.out.println(redisTemplate.opsForSet().pop(redisKey)); 80 | System.out.println(redisTemplate.opsForSet().members(redisKey)); 81 | } 82 | 83 | @Test 84 | public void testSortedSets() { 85 | String redisKey = "test:students"; 86 | 87 | redisTemplate.opsForZSet().add(redisKey, "唐僧", 80); 88 | redisTemplate.opsForZSet().add(redisKey, "悟空", 90); 89 | redisTemplate.opsForZSet().add(redisKey, "八戒", 50); 90 | redisTemplate.opsForZSet().add(redisKey, "沙僧", 70); 91 | redisTemplate.opsForZSet().add(redisKey, "白龙马", 60); 92 | 93 | System.out.println(redisTemplate.opsForZSet().zCard(redisKey)); 94 | System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒")); 95 | System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒")); 96 | System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2)); 97 | } 98 | 99 | @Test 100 | public void testKeys() { 101 | redisTemplate.delete("test:user"); 102 | 103 | System.out.println(redisTemplate.hasKey("test:user")); 104 | 105 | redisTemplate.expire("test:students", 10, TimeUnit.SECONDS); 106 | } 107 | 108 | // 多次访问同一个key 109 | @Test 110 | public void testBoundOperations() { 111 | String redisKey = "test:count"; 112 | BoundValueOperations operations = redisTemplate.boundValueOps(redisKey); 113 | operations.increment(); 114 | operations.increment(); 115 | operations.increment(); 116 | operations.increment(); 117 | operations.increment(); 118 | System.out.println(operations.get()); 119 | } 120 | 121 | // 编程式事务 122 | @Test 123 | public void testTransactional() { 124 | Object obj = redisTemplate.execute(new SessionCallback() { 125 | @Override 126 | public Object execute(RedisOperations operations) throws DataAccessException { 127 | String redisKey = "test:tx"; 128 | 129 | // 标记一个事务块的开始。 130 | operations.multi(); 131 | 132 | operations.opsForSet().add(redisKey, "zhangsan"); 133 | operations.opsForSet().add(redisKey, "lisi"); 134 | operations.opsForSet().add(redisKey, "wangwu"); 135 | 136 | System.out.println(operations.opsForSet().members(redisKey)); 137 | 138 | // 执行所有事务块内的命令。 139 | return operations.exec(); 140 | } 141 | }); 142 | System.out.println(obj); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/community/service/DiscussPostService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.community.service; 2 | 3 | import com.github.benmanes.caffeine.cache.CacheLoader; 4 | import com.github.benmanes.caffeine.cache.Caffeine; 5 | import com.github.benmanes.caffeine.cache.LoadingCache; 6 | import com.nowcoder.community.dao.DiscussPostMapper; 7 | import com.nowcoder.community.entity.DiscussPost; 8 | import com.nowcoder.community.util.SensitiveFilter; 9 | import org.checkerframework.checker.nullness.qual.NonNull; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.web.util.HtmlUtils; 17 | 18 | import javax.annotation.PostConstruct; 19 | import java.util.List; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | @Service 23 | public class DiscussPostService { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(DiscussPostService.class); 26 | 27 | @Autowired 28 | private DiscussPostMapper discussPostMapper; 29 | 30 | @Autowired 31 | private SensitiveFilter sensitiveFilter; 32 | 33 | @Value("${caffeine.posts.max-size}") 34 | private int maxSize; 35 | 36 | @Value("${caffeine.posts.expire-seconds}") 37 | private int expireSeconds; 38 | 39 | // Caffeine核心接口: Cache, LoadingCache, AsyncLoadingCache 40 | 41 | // 帖子列表缓存 42 | private LoadingCache> postListCache; 43 | 44 | // 帖子总数缓存 45 | private LoadingCache postRowsCache; 46 | 47 | @PostConstruct 48 | public void init() { 49 | // 初始化帖子列表缓存 50 | postListCache=Caffeine.newBuilder() 51 | .maximumSize(maxSize) 52 | .expireAfterWrite(expireSeconds, TimeUnit.SECONDS) 53 | .build(new CacheLoader>() { 54 | @Nullable 55 | @Override 56 | public List load(@NonNull String key) throws Exception { 57 | if (key == null || key.length() == 0) { 58 | throw new IllegalArgumentException("参数错误!"); 59 | } 60 | String[] params = key.split(":"); 61 | if (params == null || params.length != 2) { 62 | throw new IllegalArgumentException("参数错误!"); 63 | } 64 | int offset = Integer.valueOf(params[0]); 65 | int limit = Integer.valueOf(params[1]); 66 | 67 | // TODO:二级缓存:redis->mysql 68 | 69 | logger.debug("load post list from DB."); 70 | return discussPostMapper.selectDiscussPosts(0, offset, limit, 1); 71 | } 72 | }); 73 | 74 | // 初始化帖子总数缓存 75 | postRowsCache=Caffeine.newBuilder() 76 | .maximumSize(maxSize) 77 | .expireAfterWrite(expireSeconds, TimeUnit.SECONDS) 78 | .build(new CacheLoader() { 79 | @Nullable 80 | @Override 81 | public Integer load(@NonNull Integer key) throws Exception { 82 | 83 | // TODO:二级缓存:redis->mysql 84 | 85 | logger.debug("load post list from DB."); 86 | return discussPostMapper.selectDiscussPostRows(key); 87 | } 88 | }); 89 | } 90 | 91 | public List findDiscussPosts(int userId, int offset, int limit, int orderMode) { 92 | // 只有热门帖子(访问首页时,userId=0) 93 | if (userId == 0 && orderMode == 1) { 94 | return postListCache.get(offset + ":" + limit); 95 | } 96 | logger.debug("load post list from DB."); 97 | return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode); 98 | } 99 | 100 | public int findDiscussPostRows(int userId) { 101 | // 首页查询走缓存 102 | if (userId == 0) { 103 | return postRowsCache.get(userId); 104 | } 105 | logger.debug("load post list from DB."); 106 | return discussPostMapper.selectDiscussPostRows(userId); 107 | } 108 | 109 | public int addDiscussPost(DiscussPost post) { 110 | if (post == null) { 111 | throw new IllegalArgumentException("参数不能为空!"); 112 | } 113 | 114 | // 转义HTML标记 115 | post.setTitle(HtmlUtils.htmlEscape(post.getTitle())); 116 | post.setContent(HtmlUtils.htmlEscape(post.getContent())); 117 | // 过滤敏感词 118 | post.setTitle(sensitiveFilter.filter(post.getTitle())); 119 | post.setContent(sensitiveFilter.filter(post.getContent())); 120 | 121 | return discussPostMapper.insertDiscussPost(post); 122 | } 123 | 124 | public DiscussPost findDiscussPostById(int id) { 125 | return discussPostMapper.selectDiscussPostById(id); 126 | } 127 | 128 | public int updateCommentCount(int id, int commentCount) { 129 | return discussPostMapper.updateCommentCount(id, commentCount); 130 | } 131 | 132 | public int updateType(int id, int type) { 133 | return discussPostMapper.updateType(id, type); 134 | } 135 | 136 | public int updateStatus(int id, int status) { 137 | return discussPostMapper.updateStatus(id, status); 138 | } 139 | 140 | public int updateScore(int id, double score) { 141 | return discussPostMapper.updateScore(id, score); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-404 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 | 103 |
104 |
105 |
106 |
107 | 123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-500 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 | 103 |
104 |
105 |
106 |
107 | 123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | --------------------------------------------------------------------------------