├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── java │ └── me │ │ └── wangao │ │ └── community │ │ ├── CommunityApplication.java │ │ ├── actuator │ │ └── DatabaseEndpoint.java │ │ ├── annotation │ │ └── LoginRequired.java │ │ ├── aspect │ │ └── ServiceLogAspect.java │ │ ├── config │ │ ├── KaptchaConfig.java │ │ ├── QuartzConfig.java │ │ ├── RedisConfig.java │ │ ├── SecurityConfig.java │ │ ├── WebMvcConfig.java │ │ └── databaseInitializer.java │ │ ├── controller │ │ ├── CommentController.java │ │ ├── DataController.java │ │ ├── DiscussPostController.java │ │ ├── FollowController.java │ │ ├── HomeController.java │ │ ├── LikeController.java │ │ ├── LoginController.java │ │ ├── MessageController.java │ │ ├── SearchController.java │ │ ├── UserController.java │ │ ├── advice │ │ │ └── ExceptionAdvice.java │ │ └── interceptor │ │ │ ├── DataInterceptor.java │ │ │ ├── LoginRequiredInterceptor.java │ │ │ ├── LoginTicketInterceptor.java │ │ │ └── MessageInterceptor.java │ │ ├── dao │ │ ├── CommentMapper.java │ │ ├── DiscussPostMapper.java │ │ ├── LoginTicketMapper.java │ │ ├── MessageMapper.java │ │ ├── NodeMapper.java │ │ ├── UserMapper.java │ │ └── elasticsearch │ │ │ └── DiscussPostRepository.java │ │ ├── entity │ │ ├── Comment.java │ │ ├── DiscussPost.java │ │ ├── Event.java │ │ ├── LoginTicket.java │ │ ├── Message.java │ │ ├── Node.java │ │ ├── Page.java │ │ ├── SearchPage.java │ │ └── User.java │ │ ├── event │ │ ├── EventConsumer.java │ │ └── EventProducer.java │ │ ├── quartz │ │ └── PostScoreRefreshJob.java │ │ ├── service │ │ ├── CommentService.java │ │ ├── CounterService.java │ │ ├── DataService.java │ │ ├── DiscussPostService.java │ │ ├── ElasticsearchService.java │ │ ├── FollowService.java │ │ ├── InitialService.java │ │ ├── LikeService.java │ │ ├── MessageService.java │ │ ├── NodeService.java │ │ └── UserService.java │ │ └── util │ │ ├── CommunityConstant.java │ │ ├── CommunityUtil.java │ │ ├── CookieUtil.java │ │ ├── HostHolder.java │ │ ├── MailClient.java │ │ ├── RedisKeyUtil.java │ │ └── SensitiveFilter.java └── resources │ ├── application.yml │ ├── mapper │ ├── CommentMapper.xml │ ├── DiscussPostMapper.xml │ ├── MessageMapper.xml │ └── UserMapper.xml │ ├── sensitive-words.txt │ ├── sql │ ├── data.sql │ ├── quartz.sql │ └── schema.sql │ ├── static │ ├── css │ │ ├── data.css │ │ ├── discuss-detail.css │ │ ├── global.css │ │ ├── letter.css │ │ └── login.css │ ├── html │ │ └── student.html │ ├── img │ │ ├── 404.svg │ │ ├── calendar.svg │ │ ├── captcha.png │ │ ├── comments.svg │ │ ├── error.svg │ │ ├── flow.png │ │ ├── like.svg │ │ ├── report.svg │ │ ├── thumbs-up.svg │ │ └── trend.svg │ └── js │ │ ├── data.js │ │ ├── discuss.js │ │ ├── global.js │ │ ├── index.js │ │ ├── letter.js │ │ ├── profile.js │ │ ├── register.js │ │ └── setting.js │ └── templates │ ├── error │ ├── 404.html │ └── 500.html │ ├── index.html │ ├── mail │ ├── activation.html │ └── forget.html │ └── site │ ├── admin │ └── data.html │ ├── discuss-detail.html │ ├── followee.html │ ├── follower.html │ ├── forget.html │ ├── letter-detail.html │ ├── letter.html │ ├── login.html │ ├── my-post.html │ ├── my-reply.html │ ├── notice-detail.html │ ├── notice.html │ ├── operate-result.html │ ├── profile.html │ ├── register.html │ ├── search.html │ └── setting.html └── test └── java └── me └── wangao └── community ├── CommunityApplicationTests.java ├── dao ├── DiscussPostMapperTest.java ├── LoginTicketMapperTest.java ├── MessageMapperTest.java ├── UserMapperTest.java └── elasticsearch │ └── DiscussPostRepositoryTest.java ├── other └── MyTest.java ├── service └── DiscussPostServiceTest.java └── util ├── MailClientTest.java └── SensitiveFilterTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | *.env 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 | 5 | ## 项目简介 6 | 7 | Flow 是一个基于 Spring 技术栈的开源社区系统,页面风格参考技术社区 [V2EX](https://www.v2ex.com/) 。主要实现了现代社区系统必要的发帖、评论、私信、点赞、关注、用户管理、数据统计等模块。 8 | 9 | 10 | 11 | ## 架构图 12 | 13 | ![架构图](https://i.loli.net/2021/03/14/CmqKW4pXUj9FNZ3.png) 14 | 15 | ## 截图预览 16 | > 动图,可能加载有点慢 17 | 18 | ![截图](https://i.loli.net/2021/03/14/OLuFaJTCfe3Bq6t.gif) 19 | 20 | 21 | 22 | ## 功能 23 | 24 | - 注册,登录 25 | - 用户信息 26 | - 修改头像 27 | - 修改密码 28 | - 前缀树敏感词过滤 29 | - 权限管理 30 | - 访客可以浏览主题、回复 31 | - 普通用户除以上功能外,还可以私信、点赞主题和回复、发表主题和回复 32 | - 管理员除以上功能外,还可以置顶主题、加精主题、删除主题 33 | - 站长除以上功能外,还可以查看站点数据统计 34 | - 主题模块 35 | - 发表主题 36 | - 支持查看单个节点下的主题 37 | - 支持按全站热度排行主题 38 | - 评论模块 39 | - 发表评论 40 | - 二级评论功能 41 | - 私信模块 42 | - 发送私信 43 | - 统计未读私信数量 44 | - 查看私信列表和私信详情 45 | - 点赞模块 46 | - 支持点赞主题、评论、二级评论 47 | - 统计主题点赞数量 48 | - 统计用户获赞数量 49 | - 关注模块 50 | - 关注用户 51 | - 统计用户的关注数量和粉丝数量 52 | - 支持查看用户的关注列表和粉丝列表 53 | - 通知模块 54 | - 统计未读通知数量 55 | - 支持评论、点赞、关注三种通知 56 | - 搜索模块 57 | - 关键词高亮 58 | - 近实时搜索 59 | - 数据统计 60 | - 统计今日 UV(访客)和 DAU(日活跃用户) 61 | - 图表展示日期范围 UV 和 DAU 62 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/CommunityApplication.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | 7 | import javax.annotation.PostConstruct; 8 | import javax.annotation.Resource; 9 | 10 | @SpringBootApplication 11 | public class CommunityApplication { 12 | 13 | @PostConstruct 14 | public void init() { 15 | // 解决 es netty 启动冲突问题 16 | // Netty4Utils.setAvailableProcessors() 17 | System.setProperty("es.set.netty.runtime.available.processors", "false"); 18 | } 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(CommunityApplication.class, args); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/actuator/DatabaseEndpoint.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.actuator; 2 | 3 | import me.wangao.community.util.CommunityUtil; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 7 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Resource; 11 | import javax.sql.DataSource; 12 | import java.sql.Connection; 13 | import java.sql.SQLException; 14 | 15 | @Component 16 | @Endpoint(id = "database") 17 | public class DatabaseEndpoint { 18 | 19 | private static final Logger logger = LoggerFactory.getLogger(DatabaseEndpoint.class); 20 | 21 | @Resource 22 | private DataSource dataSource; 23 | 24 | @ReadOperation 25 | public String checkConnection() { 26 | try (Connection connection = dataSource.getConnection()) { 27 | return CommunityUtil.getJSONString(0, "获取连接成功!"); 28 | } catch (SQLException e) { 29 | logger.error("获取连接失败" + e.getMessage()); 30 | return CommunityUtil.getJSONString(1, "获取连接失败!"); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/annotation/LoginRequired.java: -------------------------------------------------------------------------------- 1 | package me.wangao.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 | @Target(ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface LoginRequired { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/aspect/ServiceLogAspect.java: -------------------------------------------------------------------------------- 1 | package me.wangao.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 | @Component 18 | @Aspect 19 | public class ServiceLogAspect { 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class); 22 | 23 | @Pointcut("execution(* me.wangao.community.service.*.*(..))") 24 | public void pointcut() { 25 | 26 | } 27 | 28 | @Before("pointcut()") 29 | // 记录service日志 30 | public void before(JoinPoint joinPoint) { 31 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 32 | if (attributes != null) { 33 | HttpServletRequest request = attributes.getRequest(); 34 | String ip = request.getRemoteHost(); 35 | String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); 36 | String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); 37 | logger.info(String.format("用户[%s],在[%s],访问了[%s]。", ip, now, target)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/config/KaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package me.wangao.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 | @Configuration 12 | public class KaptchaConfig { 13 | 14 | @Bean 15 | public Producer kaptchaProducer() { 16 | Properties properties = new Properties(); 17 | properties.setProperty("kaptcha.image.width", "100"); 18 | properties.setProperty("kaptcha.image.height", "40"); 19 | properties.setProperty("kaptcha.textproducer.font.size", "32"); 20 | properties.setProperty("kaptcha.textproducer.font.color", "0,0,0"); 21 | properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 22 | properties.setProperty("kaptcha.textproducer.char.length", "4"); 23 | properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); 24 | 25 | DefaultKaptcha kaptcha = new DefaultKaptcha(); 26 | Config config = new Config(properties); 27 | kaptcha.setConfig(config); 28 | 29 | return kaptcha; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/config/QuartzConfig.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.config; 2 | 3 | import me.wangao.community.quartz.PostScoreRefreshJob; 4 | import org.quartz.JobDataMap; 5 | import org.quartz.JobDetail; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.scheduling.quartz.JobDetailFactoryBean; 9 | import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; 10 | 11 | @Configuration 12 | public class QuartzConfig { 13 | 14 | // 刷新帖子分数任务 15 | @Bean 16 | public JobDetailFactoryBean postScoreRefreshJobDetail() { 17 | JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); 18 | factoryBean.setJobClass(PostScoreRefreshJob.class); 19 | factoryBean.setName("postScoreRefreshJob"); 20 | factoryBean.setGroup("communityJobGroup"); 21 | factoryBean.setDurability(true); 22 | factoryBean.setRequestsRecovery(true); 23 | return factoryBean; 24 | } 25 | 26 | @Bean 27 | public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) { 28 | SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); 29 | factoryBean.setJobDetail(postScoreRefreshJobDetail); 30 | factoryBean.setName("postScoreRefreshTrigger"); 31 | factoryBean.setGroup("communityTriggerGroup"); 32 | factoryBean.setRepeatInterval(1000 * 60 * 5); // 暂时设置为5分钟执行1次 33 | factoryBean.setJobDataAsMap(new JobDataMap()); 34 | return factoryBean; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Primary; 6 | import org.springframework.data.redis.connection.RedisConnectionFactory; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.data.redis.serializer.RedisSerializer; 9 | 10 | @Configuration 11 | public class RedisConfig { 12 | 13 | @Bean 14 | @Primary 15 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) { 16 | RedisTemplate template = new RedisTemplate<>(); 17 | template.setConnectionFactory(factory); 18 | 19 | // 设置key的序列化方式 20 | template.setKeySerializer(RedisSerializer.string()); 21 | 22 | // 设置value序列化方式 23 | template.setValueSerializer(RedisSerializer.json()); 24 | 25 | // 设置hash的key的序列化方式 26 | template.setHashKeySerializer(RedisSerializer.string()); 27 | 28 | // 设置hash的value的序列化方式 29 | template.setHashValueSerializer(RedisSerializer.json()); 30 | 31 | template.afterPropertiesSet(); 32 | return template; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.config; 2 | 3 | import me.wangao.community.util.CommunityConstant; 4 | import me.wangao.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.web.AuthenticationEntryPoint; 12 | import org.springframework.security.web.access.AccessDeniedHandler; 13 | 14 | import javax.servlet.ServletException; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.io.IOException; 18 | 19 | @Configuration 20 | public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant { 21 | 22 | @Override 23 | public void configure(WebSecurity web) throws Exception { 24 | web.ignoring().antMatchers("/resources/**"); 25 | } 26 | 27 | @Override 28 | protected void configure(HttpSecurity http) throws Exception { 29 | // 授权 30 | http.authorizeRequests() 31 | .antMatchers( 32 | "/user/setting", 33 | "/user/upload", 34 | "/discuss/add", 35 | "/comment/add/**", 36 | "/letter/**", 37 | "/notice/**", 38 | "/like", 39 | "/follow", 40 | "/unfollow", 41 | "/header/url" 42 | ) 43 | .hasAnyAuthority( 44 | AUTHORITY_USER, 45 | AUTHORITY_ADMIN, 46 | AUTHORITY_MODERATOR 47 | ) 48 | .antMatchers( 49 | "/discuss/top", 50 | "/discuss/wonderful", 51 | "/discuss/cancelTop", 52 | "/discuss/cancelWonderful", 53 | "/discuss/delete" 54 | ).hasAnyAuthority(AUTHORITY_MODERATOR, AUTHORITY_ADMIN) 55 | .antMatchers( 56 | "/data/**", 57 | "/actuator/**").hasAnyAuthority(AUTHORITY_ADMIN) 58 | .anyRequest().permitAll(); 59 | 60 | // 权限不够时的处理 61 | http.exceptionHandling() 62 | .authenticationEntryPoint(new AuthenticationEntryPoint() { 63 | // 认证异常处理(没有登录) 64 | @Override 65 | public void commence(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException { 66 | String xRequestWith = req.getHeader("X-Requested-With"); 67 | if ("XMLHttpRequest".equals(xRequestWith)) { 68 | // ajax请求 69 | res.setContentType("application/plain; charset=utf-8"); 70 | res.getWriter().write(CommunityUtil.getJSONString(403, "您还没有登录")); 71 | } else { 72 | // 普通请求 73 | res.sendRedirect(req.getContextPath() + "/login"); 74 | } 75 | } 76 | }) 77 | .accessDeniedHandler(new AccessDeniedHandler() { 78 | // 没有权限的异常处理 79 | @Override 80 | public void handle(HttpServletRequest req, HttpServletResponse res, AccessDeniedException e) throws IOException, ServletException { 81 | String xRequestWith = req.getHeader("X-Requested-With"); 82 | if ("XMLHttpRequest".equals(xRequestWith)) { 83 | // ajax请求 84 | res.setContentType("application/plain; charset=utf-8"); 85 | res.getWriter().write(CommunityUtil.getJSONString(403, "您没有访问此功能的权限")); 86 | } else { 87 | // 普通请求 88 | res.sendRedirect(req.getContextPath() + "/denied"); 89 | } 90 | } 91 | }); 92 | 93 | // security 默认会拦截logout进行退出的处理,需要覆盖其逻辑,执行自己的退出逻辑 94 | http.logout().logoutUrl("/security_logout"); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.config; 2 | 3 | import me.wangao.community.controller.interceptor.DataInterceptor; 4 | import me.wangao.community.controller.interceptor.LoginRequiredInterceptor; 5 | import me.wangao.community.controller.interceptor.LoginTicketInterceptor; 6 | import me.wangao.community.controller.interceptor.MessageInterceptor; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | 12 | import javax.annotation.Resource; 13 | 14 | @Configuration 15 | public class WebMvcConfig implements WebMvcConfigurer { 16 | 17 | @Resource 18 | private LoginTicketInterceptor loginTicketInterceptor; 19 | 20 | // @Resource 21 | // private LoginRequiredInterceptor loginRequiredInterceptor; 22 | 23 | @Resource 24 | private MessageInterceptor messageInterceptor; 25 | 26 | @Resource 27 | private DataInterceptor dataInterceptor; 28 | 29 | @Override 30 | public void addInterceptors(InterceptorRegistry registry) { 31 | registry.addInterceptor(loginTicketInterceptor) 32 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 33 | 34 | // registry.addInterceptor(loginRequiredInterceptor) 35 | // .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 36 | 37 | registry.addInterceptor(messageInterceptor) 38 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 39 | 40 | registry.addInterceptor(dataInterceptor) 41 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/config/databaseInitializer.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.core.io.Resource; 11 | import org.springframework.jdbc.datasource.init.DataSourceInitializer; 12 | import org.springframework.jdbc.datasource.init.DatabasePopulator; 13 | import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; 14 | 15 | import javax.sql.DataSource; 16 | 17 | @Configuration 18 | @ConditionalOnProperty(prefix = "db", name = "initial", havingValue = "true") 19 | public class databaseInitializer { 20 | 21 | /** 22 | * 构建Resource对象 23 | */ 24 | @Value("classpath:sql/schema.sql") 25 | private Resource schema; 26 | 27 | @Value("classpath:sql/data.sql") 28 | private Resource data; 29 | 30 | /** 31 | * 填充数据库 32 | */ 33 | @Bean 34 | public DataSourceInitializer dataSourceInitializer(final DataSource dataSource) { 35 | final DataSourceInitializer initializer = new DataSourceInitializer(); 36 | // 设置数据源 37 | initializer.setDataSource(dataSource); 38 | initializer.setDatabasePopulator(databasePopulator()); 39 | return initializer; 40 | } 41 | 42 | private DatabasePopulator databasePopulator() { 43 | final ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); 44 | populator.addScripts(schema); 45 | populator.addScripts(data); 46 | return populator; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller; 2 | 3 | import me.wangao.community.entity.Comment; 4 | import me.wangao.community.entity.DiscussPost; 5 | import me.wangao.community.entity.Event; 6 | import me.wangao.community.event.EventProducer; 7 | import me.wangao.community.service.CommentService; 8 | import me.wangao.community.service.DiscussPostService; 9 | import me.wangao.community.util.CommunityConstant; 10 | import me.wangao.community.util.HostHolder; 11 | import me.wangao.community.util.RedisKeyUtil; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.PostMapping; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | 18 | import javax.annotation.Resource; 19 | import java.util.Date; 20 | 21 | @Controller 22 | @RequestMapping("/comment") 23 | public class CommentController implements CommunityConstant { 24 | 25 | @Resource 26 | private CommentService commentService; 27 | 28 | @Resource 29 | private HostHolder hostHolder; 30 | 31 | @Resource 32 | private EventProducer eventProducer; 33 | 34 | @Resource 35 | private DiscussPostService discussPostService; 36 | 37 | @Resource 38 | private RedisTemplate redisTemplate; 39 | 40 | @PostMapping("/add/{discussPostId}") 41 | public String addComment(@PathVariable int discussPostId, Comment comment) { 42 | comment.setUserId(hostHolder.getUser().getId()) 43 | .setStatus(0) 44 | .setCreateTime(new Date()); 45 | commentService.addComment(comment); 46 | 47 | // 触发评论事件 48 | Event event = new Event() 49 | .setTopic(TOPIC_COMMENT) 50 | .setUserId(hostHolder.getUser().getId()) 51 | .setEntityType(comment.getEntityType()) 52 | .setEntityId(comment.getEntityId()) 53 | .setData("postId", discussPostId); 54 | 55 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 56 | DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId()); 57 | event.setEntityUserId(target.getUserId()); 58 | } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT){ 59 | Comment target = commentService.findCommentById(comment.getEntityId()); 60 | event.setEntityUserId(target.getUserId()); 61 | } 62 | eventProducer.fireEvent(event); 63 | 64 | // 评论帖子,会修改回帖数量,触发 elasticsearch 修改帖子事件 65 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 66 | Event updatePostEvent = new Event() 67 | .setTopic(TOPIC_PUBLISH) 68 | .setUserId(comment.getUserId()) 69 | .setEntityType(ENTITY_TYPE_POST) 70 | .setEntityId(discussPostId); 71 | eventProducer.fireEvent(event); 72 | 73 | // 计算帖子分数 74 | String scoreKey = RedisKeyUtil.getPostScoreKey(); 75 | redisTemplate.opsForSet().add(scoreKey, discussPostId); 76 | } 77 | 78 | 79 | return "redirect:/discuss/detail/" + discussPostId; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/DataController.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller; 2 | 3 | import me.wangao.community.service.DataService; 4 | import me.wangao.community.service.DiscussPostService; 5 | import me.wangao.community.service.UserService; 6 | import me.wangao.community.util.CommunityUtil; 7 | import org.springframework.format.annotation.DateTimeFormat; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.ResponseBody; 14 | 15 | import javax.annotation.Resource; 16 | import java.util.Date; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | @Controller 22 | public class DataController { 23 | 24 | @Resource 25 | private DataService dataService; 26 | 27 | @Resource 28 | private UserService userService; 29 | 30 | @Resource 31 | private DiscussPostService discussPostService; 32 | 33 | @RequestMapping("/data") 34 | public String getDataPage(Model model) { 35 | int todayRegisterCount = userService.findTodayRegisterCount(); 36 | int todayPostCount = discussPostService.findTodayPostCount(); 37 | model.addAttribute("todayRegisterCount",todayRegisterCount); 38 | model.addAttribute("todayPostCount",todayPostCount); 39 | 40 | 41 | long todayUV = dataService.getTodayUV(); 42 | long todayDAU = dataService.getTodayDAU(); 43 | model.addAttribute("todayUV", todayUV); 44 | model.addAttribute("todayDAU", todayDAU); 45 | 46 | return "/site/admin/data"; 47 | } 48 | 49 | // 统计网站uv 50 | @PostMapping("/data/uv") 51 | @ResponseBody 52 | public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start, 53 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) { 54 | long count = dataService.calculateUV(start, end); 55 | List> list = dataService.listUV(start, end); 56 | 57 | Map result = new HashMap<>(); 58 | result.put("count", count); 59 | result.put("list", list); 60 | 61 | return CommunityUtil.getJSONString(0, "获取成功", result); 62 | } 63 | 64 | // 统计网站dau 65 | @PostMapping("/data/dau") 66 | @ResponseBody 67 | public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start, 68 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) { 69 | long count = dataService.calculateDAU(start, end); 70 | List> list = dataService.listDAU(start, end); 71 | 72 | Map result = new HashMap<>(); 73 | result.put("count", count); 74 | result.put("list", list); 75 | 76 | return CommunityUtil.getJSONString(0, "获取成功", result); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/FollowController.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller; 2 | 3 | import me.wangao.community.annotation.LoginRequired; 4 | import me.wangao.community.entity.Event; 5 | import me.wangao.community.entity.Page; 6 | import me.wangao.community.entity.User; 7 | import me.wangao.community.event.EventProducer; 8 | import me.wangao.community.service.FollowService; 9 | import me.wangao.community.service.UserService; 10 | import me.wangao.community.util.CommunityConstant; 11 | import me.wangao.community.util.CommunityUtil; 12 | import me.wangao.community.util.HostHolder; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.PostMapping; 18 | import org.springframework.web.bind.annotation.ResponseBody; 19 | 20 | import javax.annotation.Resource; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | @Controller 25 | public class FollowController implements CommunityConstant{ 26 | 27 | @Resource 28 | private FollowService followService; 29 | 30 | @Resource 31 | private HostHolder hostHolder; 32 | 33 | @Resource 34 | private UserService userService; 35 | 36 | @Resource 37 | private EventProducer eventProducer; 38 | 39 | @PostMapping("/follow") 40 | @ResponseBody 41 | @LoginRequired 42 | public String follow(int entityType, int entityId) { 43 | User user = hostHolder.getUser(); 44 | 45 | followService.follow(user.getId(), entityType, entityId); 46 | 47 | Event event = new Event() 48 | .setTopic(TOPIC_FOLLOW) 49 | .setUserId(hostHolder.getUser().getId()) 50 | .setEntityType(entityType) 51 | .setEntityId(entityId) 52 | .setEntityUserId(entityId); 53 | eventProducer.fireEvent(event); 54 | 55 | return CommunityUtil.getJSONString(0, "已关注"); 56 | } 57 | 58 | @PostMapping("/unfollow") 59 | @ResponseBody 60 | @LoginRequired 61 | public String unfollow(int entityType, int entityId) { 62 | User user = hostHolder.getUser(); 63 | 64 | followService.unfollow(user.getId(), entityType, entityId); 65 | 66 | return CommunityUtil.getJSONString(0, "已取消关注"); 67 | } 68 | 69 | @GetMapping("/followees/{userId}") 70 | public String getFollowees(@PathVariable int userId, Page page, Model model) { 71 | User user = userService.findUserById(userId); 72 | 73 | if (user == null) { 74 | throw new RuntimeException("该用户不存在"); 75 | } 76 | model.addAttribute("user", user); 77 | 78 | page.setLimit(5); 79 | page.setPath("/followees/userId"); 80 | page.setRows((int)followService.findFolloweeCount(userId, CommunityConstant.ENTITY_TYPE_USER)); 81 | 82 | List> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit()); 83 | 84 | if (userList != null) { 85 | userList.forEach(map -> { 86 | User u = (User)map.get("user"); 87 | map.put("hasFollowed", hasFollowed(u.getId())); 88 | }); 89 | } 90 | 91 | model.addAttribute("users", userList); 92 | 93 | return "/site/followee"; 94 | } 95 | 96 | @GetMapping("/followers/{userId}") 97 | public String getFollowers(@PathVariable int userId, Page page, Model model) { 98 | User user = userService.findUserById(userId); 99 | 100 | if (user == null) { 101 | throw new RuntimeException("该用户不存在"); 102 | } 103 | model.addAttribute("user", user); 104 | 105 | page.setLimit(5); 106 | page.setPath("/followers/userId"); 107 | page.setRows((int)followService.findFollowerCount(CommunityConstant.ENTITY_TYPE_USER, userId)); 108 | 109 | List> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit()); 110 | 111 | if (userList != null) { 112 | userList.forEach(map -> { 113 | User u = (User)map.get("user"); 114 | map.put("hasFollowed", hasFollowed(u.getId())); 115 | }); 116 | } 117 | 118 | model.addAttribute("users", userList); 119 | 120 | return "/site/follower"; 121 | } 122 | 123 | private boolean hasFollowed(int userId) { 124 | if (hostHolder.getUser() == null) { 125 | return false; 126 | } 127 | 128 | return followService.hasFollowed(hostHolder.getUser().getId(), CommunityConstant.ENTITY_TYPE_USER, userId); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import me.wangao.community.entity.Node; 5 | import me.wangao.community.entity.Page; 6 | import me.wangao.community.entity.User; 7 | import me.wangao.community.service.*; 8 | import me.wangao.community.util.CommunityConstant; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | 16 | import javax.annotation.Resource; 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 | @Resource 26 | private DiscussPostService discussPostService; 27 | @Resource 28 | private UserService userService; 29 | 30 | @Resource 31 | private LikeService likeService; 32 | 33 | @Resource 34 | private NodeService nodeService; 35 | 36 | @Resource 37 | private CounterService counterService; 38 | 39 | @GetMapping({"/", "/index", "/index.html"}) 40 | public String getIndexPage(Model model, Page page, @RequestParam(name = "orderMode", defaultValue = "0") int orderMode) { 41 | page.setRows(discussPostService.findDiscussPostRows(null)); 42 | page.setPath("/index?orderMode=" + orderMode); 43 | 44 | model.addAttribute("orderMode", orderMode); 45 | 46 | List list = discussPostService.findDiscussPosts(null, page.getOffset(), page.getLimit(), orderMode); 47 | List> discussPosts = getPostMapList(list); 48 | model.addAttribute("discussPosts", discussPosts); 49 | 50 | List nodes = nodeService.findAllNodes(); 51 | model.addAttribute("nodes", nodes); 52 | 53 | return "index"; 54 | } 55 | 56 | @GetMapping("/node/{id}") 57 | public String getNodePage(Model model, Page page, @PathVariable("id") int nodeId) { 58 | page.setRows(discussPostService.findDiscussPostRowsByNodeId(nodeId)); 59 | page.setPath("/node/" + nodeId); 60 | model.addAttribute("nodeId", nodeId); 61 | //model.addAttribute("orderMode", -1); 62 | 63 | List list = discussPostService.findDiscussPostByNodeId(nodeId, page.getOffset(), page.getLimit()); 64 | List> discussPosts = getPostMapList(list); 65 | 66 | model.addAttribute("discussPosts", discussPosts); 67 | 68 | List nodes = nodeService.findAllNodes(); 69 | model.addAttribute("nodes", nodes); 70 | 71 | return "index"; 72 | } 73 | 74 | private List> getPostMapList(List list) { 75 | List> discussPosts = new ArrayList<>(); 76 | if(list != null) { 77 | list.forEach(post -> { 78 | Map map = new HashMap<>(); 79 | map.put("post", post); 80 | 81 | User user = userService.findUserById(post.getUserId()); 82 | map.put("user", user); 83 | 84 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()); 85 | map.put("likeCount", likeCount); 86 | 87 | Node node = nodeService.findNodeById(post.getNodeId()); 88 | // do { 89 | // node = nodeService.findNodeById(post.getNodeId()); 90 | // } while (node == null); 91 | map.put("nodeName", node.getName()); 92 | 93 | discussPosts.add(map); 94 | }); 95 | } 96 | return discussPosts; 97 | } 98 | 99 | @GetMapping("/error") 100 | public String getErrorPage() { 101 | return "/error/500"; 102 | } 103 | 104 | @GetMapping("/denied") 105 | public String getDeniedPage() { 106 | return "/error/404"; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/LikeController.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller; 2 | 3 | import me.wangao.community.annotation.LoginRequired; 4 | import me.wangao.community.entity.Event; 5 | import me.wangao.community.entity.User; 6 | import me.wangao.community.event.EventProducer; 7 | import me.wangao.community.service.LikeService; 8 | import me.wangao.community.util.CommunityConstant; 9 | import me.wangao.community.util.CommunityUtil; 10 | import me.wangao.community.util.HostHolder; 11 | import me.wangao.community.util.RedisKeyUtil; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.ResponseBody; 16 | 17 | import javax.annotation.Resource; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | @Controller 22 | public class LikeController implements CommunityConstant { 23 | 24 | @Resource 25 | private LikeService likeService; 26 | 27 | @Resource 28 | private HostHolder hostHolder; 29 | 30 | @Resource 31 | private EventProducer eventProducer; 32 | 33 | @Resource 34 | private RedisTemplate redisTemplate; 35 | 36 | @PostMapping("/like") 37 | @ResponseBody 38 | @LoginRequired 39 | public String like(int entityType, int entityId, int entityUserId, int postId) { 40 | User user = hostHolder.getUser(); 41 | 42 | likeService.like(user.getId(), entityType, entityId, entityUserId); 43 | long likeCount = likeService.findEntityLikeCount(entityType, entityId); 44 | int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId); 45 | 46 | Map map = new HashMap<>(); 47 | map.put("likeCount", likeCount); 48 | map.put("likeStatus", likeStatus); 49 | 50 | if (likeStatus == 1) { 51 | Event event = new Event() 52 | .setTopic(TOPIC_LIKE) 53 | .setUserId(hostHolder.getUser().getId()) 54 | .setEntityType(entityType) 55 | .setEntityId(entityId) 56 | .setEntityUserId(entityUserId) 57 | .setData("postId", postId); 58 | eventProducer.fireEvent(event); 59 | } 60 | 61 | // 计算帖子分数 62 | if (entityType == ENTITY_TYPE_POST) { 63 | String scoreKey = RedisKeyUtil.getPostScoreKey(); 64 | redisTemplate.opsForSet().add(scoreKey, postId); 65 | } 66 | 67 | 68 | return CommunityUtil.getJSONString(0, null, map); 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import me.wangao.community.entity.Page; 5 | import me.wangao.community.entity.SearchPage; 6 | import me.wangao.community.service.ElasticsearchService; 7 | import me.wangao.community.service.LikeService; 8 | import me.wangao.community.service.UserService; 9 | import me.wangao.community.util.CommunityConstant; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | 14 | import javax.annotation.Resource; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | @Controller 21 | public class SearchController implements CommunityConstant { 22 | 23 | @Resource 24 | private ElasticsearchService elasticsearchService; 25 | 26 | @Resource 27 | private UserService userService; 28 | 29 | @Resource 30 | private LikeService likeService; 31 | 32 | @GetMapping("/search") 33 | public String search(String keyword, Page page, Model model) { 34 | SearchPage result = elasticsearchService.searchDiscussPost( 35 | keyword, page.getCurrent() - 1, page.getLimit()); 36 | // 聚合数据 37 | List> discussPosts = new ArrayList<>(); 38 | 39 | if (result != null && !result.getList().isEmpty()) { 40 | result.getList().forEach(post -> { 41 | Map map = new HashMap<>(); 42 | map.put("post", post); 43 | map.put("user", userService.findUserById(post.getUserId())); 44 | map.put("likeCount", likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId())); 45 | 46 | discussPosts.add(map); 47 | }); 48 | } 49 | model.addAttribute("discussPosts", discussPosts); 50 | model.addAttribute("keyword",keyword); 51 | 52 | page.setPath("/search?keyword=" + keyword); 53 | if (result == null) { 54 | page.setRows(0); 55 | } else { 56 | page.setRows((int) result.getTotal()); 57 | } 58 | 59 | return "/site/search"; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/advice/ExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller.advice; 2 | 3 | import me.wangao.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 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | @ControllerAdvice(annotations = {Controller.class, RestController.class}) 16 | public class ExceptionAdvice { 17 | 18 | private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class); 19 | 20 | @ExceptionHandler({ Exception.class }) 21 | public void handleException(Exception e, HttpServletRequest req, HttpServletResponse res) throws IOException { 22 | logger.error("服务器异常:" + e.getMessage()); 23 | for (StackTraceElement element : e.getStackTrace()) { 24 | logger.error(element.toString()); 25 | } 26 | 27 | String xReq = req.getHeader("x-requested-with"); 28 | // 处理AJAX请求 29 | if (xReq.equals("XMLHttpRequest")) { 30 | res.setContentType("application/plain; charset=utf-8"); 31 | res.getWriter().println(CommunityUtil.getJSONString(500, "服务器异常!")); 32 | } else { 33 | res.sendRedirect(req.getContextPath() + "/error"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/interceptor/DataInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller.interceptor; 2 | 3 | import me.wangao.community.entity.User; 4 | import me.wangao.community.service.DataService; 5 | import me.wangao.community.util.HostHolder; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | 9 | import javax.annotation.Resource; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | 13 | @Component 14 | public class DataInterceptor implements HandlerInterceptor { 15 | 16 | @Resource 17 | private DataService dataService; 18 | 19 | @Resource 20 | private HostHolder hostHolder; 21 | 22 | @Override 23 | public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception { 24 | // 统计UV 25 | String ip = req.getRemoteHost(); 26 | dataService.recordUV(ip); 27 | // 统计DAU 28 | User user = hostHolder.getUser(); 29 | if (user != null) { 30 | dataService.recordDAU(user.getId()); 31 | } 32 | 33 | return true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/interceptor/LoginRequiredInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller.interceptor; 2 | 3 | import me.wangao.community.annotation.LoginRequired; 4 | import me.wangao.community.util.CommunityUtil; 5 | import me.wangao.community.util.HostHolder; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.method.HandlerMethod; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | 10 | import javax.annotation.Resource; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.lang.reflect.Method; 14 | 15 | @Component 16 | public class LoginRequiredInterceptor implements HandlerInterceptor { 17 | 18 | @Resource 19 | HostHolder hostHolder; 20 | 21 | @Override 22 | public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception { 23 | if (handler instanceof HandlerMethod) { 24 | HandlerMethod handlerMethod = (HandlerMethod) handler; 25 | Method method = handlerMethod.getMethod(); 26 | LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); 27 | 28 | if (loginRequired != null && hostHolder.getUser() == null) { 29 | String xReq = req.getHeader("x-requested-with"); 30 | // 处理AJAX请求 31 | if (xReq.equals("XMLHttpRequest")) { 32 | res.setContentType("application/plain; charset=utf-8"); 33 | res.getWriter().println(CommunityUtil.getJSONString(403, "请先登录")); 34 | } else { 35 | res.sendRedirect(req.getContextPath() + "/login"); 36 | } 37 | return false; 38 | } 39 | } 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/interceptor/LoginTicketInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller.interceptor; 2 | 3 | import me.wangao.community.entity.LoginTicket; 4 | import me.wangao.community.entity.User; 5 | import me.wangao.community.service.UserService; 6 | import me.wangao.community.util.CookieUtil; 7 | import me.wangao.community.util.HostHolder; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.security.core.context.SecurityContextImpl; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.web.servlet.HandlerInterceptor; 14 | import org.springframework.web.servlet.ModelAndView; 15 | 16 | import javax.annotation.Resource; 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 | @Resource 25 | private UserService userService; 26 | 27 | @Resource 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 | if (ticket != null) { 35 | // 查询凭证 36 | LoginTicket loginTicket = userService.findLoginTicket(ticket); 37 | // 检查凭证 38 | if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { 39 | // 根据凭证查询用户 40 | User user = userService.findUserById(loginTicket.getUserId()); 41 | // 在本次请求中持有用户 42 | hostHolder.setUser(user); 43 | 44 | // 构建用户认证结果并存入SecurityContext,以便于security授权 45 | Authentication authentication = new UsernamePasswordAuthenticationToken( 46 | user, user.getPassword(), userService.getAuthorities(user.getId())); 47 | SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); 48 | } 49 | } 50 | return true; 51 | } 52 | 53 | @Override 54 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 55 | User user = hostHolder.getUser(); 56 | if (user != null && modelAndView != null) { 57 | modelAndView.addObject("loginUser", user); 58 | } 59 | } 60 | 61 | @Override 62 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 63 | hostHolder.clear(); 64 | SecurityContextHolder.clearContext(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/controller/interceptor/MessageInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.controller.interceptor; 2 | 3 | import me.wangao.community.entity.User; 4 | import me.wangao.community.service.*; 5 | import me.wangao.community.util.CommunityConstant; 6 | import me.wangao.community.util.HostHolder; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | import javax.annotation.Resource; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | 15 | @Component 16 | public class MessageInterceptor implements HandlerInterceptor, CommunityConstant { 17 | 18 | @Resource 19 | private HostHolder hostHolder; 20 | 21 | @Resource 22 | private MessageService messageService; 23 | 24 | @Resource 25 | private LikeService likeService; 26 | 27 | @Resource 28 | private FollowService followService; 29 | 30 | @Resource 31 | private CounterService counterService; 32 | 33 | @Override 34 | public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView modelAndView) throws Exception { 35 | User user = hostHolder.getUser(); 36 | if (user != null && modelAndView != null) { 37 | int userId = user.getId(); 38 | // 未读消息 39 | int letterUnreadCount = messageService.findLetterUnreadCount(userId, null); 40 | int noticeUnreadCount = messageService.findNoticeUnreadCount(userId, null); 41 | modelAndView.addObject("allUnreadCount", letterUnreadCount + noticeUnreadCount); 42 | 43 | // 点赞数量 44 | int likeCount = likeService.findUserLikeCount(userId); 45 | modelAndView.addObject("myLikeCount", likeCount); 46 | 47 | // 关注数量 48 | long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER); 49 | modelAndView.addObject("myFolloweeCount", followeeCount); 50 | 51 | // 粉丝数量 52 | long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId); 53 | modelAndView.addObject("myFollowerCount", followerCount); 54 | } 55 | 56 | if (modelAndView != null) { 57 | // 侧边计数器 58 | int userCount = counterService.getUserCount(); 59 | int commentCount = counterService.getCommentCount(); 60 | int postCount = counterService.getPostCount(); 61 | modelAndView.addObject("userCount", userCount); 62 | modelAndView.addObject("commentCount", commentCount); 63 | modelAndView.addObject("postCount", postCount); 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.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 selectCommentByEntity(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 | int selectRows(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/DiscussPostMapper.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface DiscussPostMapper { 11 | 12 | List selectDiscussPosts(@Param("userId") Integer userId, int offset, int limit, @Param("orderMode") int orderMode); 13 | 14 | int selectDiscussPostRows(@Param("userId") Integer userId); 15 | 16 | @Insert({ 17 | "insert into discuss_post(user_id, node_id, title, content, type, status, create_time, comment_count, score) ", 18 | "values(#{userId}, #{nodeId}, #{title}, #{content}, #{type}, #{status}, #{createTime}, #{commentCount}, #{score})" 19 | }) 20 | @Options(useGeneratedKeys = true, keyProperty = "id") 21 | int insertDiscussPost(DiscussPost discussPost); 22 | 23 | @Select({ 24 | "select id, user_id, node_id, title, content, type, status, create_time, comment_count, score ", 25 | "from discuss_post ", 26 | "where id = #{id} " 27 | }) 28 | DiscussPost selectDiscussPostById(int id); 29 | 30 | List selectDiscussPostsByNodeId(@Param("nodeId") int nodeId, @Param("offset") int offset, @Param("limit") int limit); 31 | 32 | int selectRowsByNodeId(int nodeId); 33 | 34 | @Update("update discuss_post set comment_count = #{commentCount} where id = #{id}") 35 | int updateCommentCount(int id, int commentCount); 36 | 37 | @Update("update discuss_post set type = #{type} where id = #{id}") 38 | int updateType(int id, int type); 39 | 40 | @Update("update discuss_post set status = #{status} where id = #{id}") 41 | int updateStatus(int id, int status); 42 | 43 | @Update("update discuss_post set score = #{score} where id = #{id}") 44 | int updateScore(int id, double score); 45 | 46 | @Select("select count(1) from discuss_post " + 47 | "where create_time >= #{from} and create_time <= #{to}") 48 | int selectRowsByDateRange(@Param("from") Date from, @Param("to") Date to); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/LoginTicketMapper.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.LoginTicket; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | @Mapper 7 | @Deprecated 8 | public interface LoginTicketMapper { 9 | 10 | @Insert({ 11 | "insert into login_ticket(user_id, ticket, status, expired) ", 12 | "values(#{userId}, #{ticket}, #{status}, #{expired})" 13 | }) 14 | @Options(useGeneratedKeys = true, keyProperty = "id") 15 | int insertLoginTicket(LoginTicket loginTicket); 16 | 17 | @Select({ 18 | "select id, user_id, ticket, status, expired ", 19 | "from login_ticket where ticket=#{ticket}" 20 | }) 21 | LoginTicket selectByTicket(String ticket); 22 | 23 | @Update({ 24 | "update login_ticket set status=#{status} where ticket=#{ticket}" 25 | }) 26 | int updateStatus(String ticket, int status); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.Message; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | import org.apache.ibatis.annotations.Select; 7 | 8 | import java.util.List; 9 | 10 | @Mapper 11 | public interface MessageMapper { 12 | 13 | // 查询当前用户的会话列表,针对每个会话只返回最新一条私信 14 | List selectConversations(int userId, int offset, int limit); 15 | 16 | // 查询当前用户的会话数量 17 | int selectConversationCount(int userId); 18 | 19 | // 查询某个会话包含的私信列表 20 | List selectLetters(String conversationId, int offset, int limit); 21 | 22 | // 查询某个会话包含的对话数量 23 | int selectLetterCount(String conversationId); 24 | 25 | // 查询未读私信数量 26 | int selectLetterUnreadCount(int userId, @Param("conversationId") String conversationId); 27 | 28 | // 新增消息 29 | int insertMessage(Message message); 30 | 31 | // 修改消息状态 32 | int updateStatus(List ids, int status); 33 | 34 | // 查询某个主题下最新的通知 35 | @Select({ 36 | "select id, from_id, to_id, conversation_id, content, status, create_time ", 37 | "from message ", 38 | "where from_id=1 and to_id=#{userId} and conversation_id=#{topic} and status != 2", 39 | "order by create_time desc ", 40 | "limit 1" 41 | }) 42 | Message selectLatestNotice(int userId, String topic); 43 | 44 | // 查询某主题下的通知的数量 45 | @Select({ 46 | "select count(id) from message ", 47 | "where from_id=1 and to_id=#{userId} and conversation_id=#{topic} and status != 2" 48 | }) 49 | int selectNoticeCount(int userId, String topic); 50 | 51 | // 查询未读通知的数量 52 | int selectNoticeUnreadCount(int userId, @Param("topic") String topic); 53 | 54 | // 查询某主题所包含的通知列表 55 | List selectNotices(int userId, String topic, int offset, int limit); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/NodeMapper.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.Node; 4 | import org.apache.ibatis.annotations.Insert; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Options; 7 | import org.apache.ibatis.annotations.Select; 8 | 9 | import java.util.List; 10 | 11 | @Mapper 12 | public interface NodeMapper { 13 | 14 | @Select("select id, `name`, `desc` from node where id = #{id}") 15 | Node selectById(int id); 16 | 17 | @Select("select id, `name`, `desc` from node") 18 | List selectAllNodes(); 19 | 20 | @Insert("insert into node(`name`, `desc`) values(#{name}, #{desc})") 21 | @Options(useGeneratedKeys = true, keyProperty = "id") 22 | int insertNode(Node node); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/UserMapper.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.User; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.Date; 8 | 9 | @Mapper 10 | public interface UserMapper { 11 | 12 | User selectById(int id); 13 | 14 | User selectByName(String username); 15 | 16 | User selectByEmail(String email); 17 | 18 | int insertUser(User user); 19 | 20 | int updateStatus(int id, int status); 21 | 22 | int updateHeader(int id, String headerUrl); 23 | 24 | int updatePassword(int id, String password); 25 | 26 | int selectRows(); 27 | 28 | int selectRowsByDateRange(@Param("from") Date from, @Param("to") Date to); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/dao/elasticsearch/DiscussPostRepository.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao.elasticsearch; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface DiscussPostRepository extends ElasticsearchRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.util.Date; 9 | 10 | @Data 11 | @Accessors(chain = true) 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class Comment { 15 | 16 | private Integer id; 17 | private Integer userId; 18 | private Integer entityType; 19 | private Integer entityId; 20 | private Integer targetId; 21 | private String content; 22 | private Integer status; 23 | private Date createTime; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/DiscussPost.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.elasticsearch.annotations.Document; 9 | import org.springframework.data.elasticsearch.annotations.Field; 10 | import org.springframework.data.elasticsearch.annotations.FieldType; 11 | 12 | import java.util.Date; 13 | 14 | @Data 15 | @Accessors(chain = true) 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Document(indexName = "discusspost", shards = 6, replicas = 2) 19 | public class DiscussPost { 20 | 21 | @Id 22 | private Integer id; 23 | 24 | @Field(type = FieldType.Integer) 25 | private Integer userId; 26 | 27 | @Field(type = FieldType.Integer) 28 | private Integer nodeId; 29 | 30 | @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 31 | private String title; 32 | 33 | @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 34 | private String content; 35 | 36 | @Field(type = FieldType.Integer) 37 | private Integer type; 38 | 39 | @Field(type = FieldType.Integer) 40 | private Integer status; 41 | 42 | @Field(type = FieldType.Date) 43 | private Date createTime; 44 | 45 | @Field(type = FieldType.Integer) 46 | private Integer commentCount; 47 | 48 | @Field(type = FieldType.Double) 49 | private Double score; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/Event.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.*; 4 | import lombok.experimental.Accessors; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @Data 10 | @Accessors(chain = true) 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class Event { 14 | 15 | private String topic; 16 | private int userId; 17 | private int entityType; 18 | private int entityId; 19 | private int entityUserId; 20 | 21 | @Setter(AccessLevel.NONE) 22 | private Map data = new HashMap<>(); 23 | 24 | public Event setData(String key, Object value) { 25 | this.data.put(key, value); 26 | return this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/LoginTicket.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.util.Date; 9 | 10 | @Data 11 | @Accessors(chain = true) 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class LoginTicket { 15 | 16 | private Integer id; 17 | private Integer userId; 18 | private String ticket; 19 | private Integer status; 20 | private Date expired; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/Message.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.util.Date; 9 | 10 | @Data 11 | @Accessors(chain = true) 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class Message { 15 | 16 | private Integer id; 17 | private Integer fromId; 18 | private Integer toId; 19 | private String conversationId; 20 | private String content; 21 | private Integer status; 22 | private Date createTime; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/Node.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.experimental.Accessors; 6 | 7 | @Data 8 | @NoArgsConstructor 9 | @Accessors(chain = true) 10 | public class Node { 11 | private int id; 12 | private String name; 13 | private String desc; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/Page.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | /** 7 | * 封装分页的组件 8 | */ 9 | @Data 10 | @Accessors(chain = true) 11 | public class Page { 12 | // 当前页码 13 | private int current = 1; 14 | // 一页显示的上限 15 | private int limit = 10; 16 | // 数据总数,用于计算总页数 17 | private int rows; 18 | // 查询路径,复用分页链接 19 | private String path; 20 | 21 | public Page setCurrent(int current) { 22 | if (current >= 1) { 23 | this.current = current; 24 | } 25 | return this; 26 | } 27 | 28 | public Page setLimit(int limit) { 29 | if (limit >= 1 && limit <= 100) { 30 | this.limit = limit; 31 | } 32 | return this; 33 | } 34 | 35 | public Page setRows(int rows) { 36 | if (rows >= 0) { 37 | this.rows = rows; 38 | } 39 | return this; 40 | } 41 | 42 | // 获取当前页的起始行 43 | public int getOffset() { 44 | return (current - 1) * limit; 45 | } 46 | 47 | // 获取总页数 48 | public int getTotal() { 49 | if (rows % limit == 0) { 50 | return rows / limit; 51 | } else { 52 | return rows / limit + 1; 53 | } 54 | } 55 | 56 | // 获取页面显示的起始页码 57 | public int getFrom() { 58 | int from = current - 2; 59 | return Math.max(from, 1); // 如果小于 1 则返回 1 60 | } 61 | 62 | // 获取页面显示的终止页码 63 | public int getTo() { 64 | int to = current + 2; 65 | int total = getTotal(); 66 | return Math.min(total, to); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/SearchPage.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.*; 4 | import lombok.experimental.Accessors; 5 | 6 | import java.util.List; 7 | 8 | 9 | /** 10 | * elasticsearch 搜索结果分页实体 11 | * @param 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class SearchPage { 18 | 19 | private int current; // 当前页 20 | private int limit; // 每页的数量 21 | private long total; // 搜索结果总数 22 | private List list; // 搜索到的结果 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/entity/User.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.util.Date; 9 | 10 | @Data 11 | @Accessors(chain = true) 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class User { 15 | 16 | private Integer id; 17 | private String username; 18 | private String password; 19 | private String salt; 20 | private String email; 21 | private Integer type; 22 | private Integer status; 23 | private String activationCode; 24 | private String headerUrl; 25 | private Date createTime; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/event/EventConsumer.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.event; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import me.wangao.community.entity.DiscussPost; 5 | import me.wangao.community.entity.Event; 6 | import me.wangao.community.entity.Message; 7 | import me.wangao.community.service.DiscussPostService; 8 | import me.wangao.community.service.ElasticsearchService; 9 | import me.wangao.community.service.MessageService; 10 | import me.wangao.community.util.CommunityConstant; 11 | import me.wangao.community.util.MailClient; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.apache.kafka.clients.consumer.ConsumerRecord; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.kafka.annotation.KafkaListener; 17 | import org.springframework.kafka.annotation.KafkaListeners; 18 | import org.springframework.stereotype.Component; 19 | 20 | import javax.annotation.Resource; 21 | import java.util.Date; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | @Component 26 | public class EventConsumer implements CommunityConstant { 27 | 28 | private final static Logger logger = LoggerFactory.getLogger(EventConsumer.class); 29 | 30 | @Resource 31 | private MessageService messageService; 32 | 33 | @Resource 34 | private DiscussPostService discussPostService; 35 | 36 | @Resource 37 | private ElasticsearchService elasticsearchService; 38 | 39 | @Resource 40 | private MailClient mailClient; 41 | 42 | @KafkaListener(topics = { TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW }) 43 | public void handleSystemMessage(ConsumerRecord record) { 44 | Event event; 45 | if ((event = dealRecord(record)) == null) return; 46 | 47 | // 发送站内通知 48 | Message message = new Message() 49 | .setFromId(SYSTEM_USER_ID) 50 | .setToId(event.getEntityUserId()) 51 | .setConversationId(event.getTopic()) 52 | .setStatus(0) 53 | .setCreateTime(new Date()); 54 | 55 | Map content = new HashMap<>(); 56 | content.put("userId", event.getUserId()); 57 | content.put("entityType", event.getEntityType()); 58 | content.put("entityId", event.getEntityId()); 59 | if (!event.getData().isEmpty()) { 60 | event.getData().forEach(content::put); 61 | } 62 | 63 | message.setContent(JSON.toJSONString(content)); 64 | messageService.addMessage(message); 65 | } 66 | 67 | // 消费发帖事件,更新elasticsearch 68 | @KafkaListener(topics = {TOPIC_PUBLISH}) 69 | public void handlePublishMessage(ConsumerRecord record) { 70 | Event event; 71 | if ((event = dealRecord(record)) == null) return; 72 | 73 | DiscussPost discussPost = discussPostService.findDiscussPostById(event.getEntityId()); 74 | elasticsearchService.saveDiscussPost(discussPost); 75 | } 76 | 77 | // 消费删帖事件 78 | @KafkaListener(topics = {TOPIC_DELETE}) 79 | public void handleDeleteMessage(ConsumerRecord record) { 80 | Event event; 81 | if ((event = dealRecord(record)) == null) return; 82 | 83 | elasticsearchService.deleteDiscussPost(event.getEntityId()); 84 | } 85 | 86 | // 消费发送邮件事件 87 | @KafkaListener(topics = {TOPIC_EMAIL}) 88 | public void handleSendEmail(ConsumerRecord record) { 89 | Event event; 90 | if ((event = dealRecord(record)) == null) return; 91 | 92 | String to = (String) event.getData().get("to"); 93 | String subject = (String) event.getData().get("subject"); 94 | String content = (String) event.getData().get("content"); 95 | if (StringUtils.isEmpty(to)) { 96 | logger.error("[send email] recipient cannot be empty"); 97 | return; 98 | } 99 | if (StringUtils.isEmpty(subject)) { 100 | logger.error("[send email] subject cannot be empty"); 101 | return; 102 | } 103 | if (StringUtils.isEmpty(content)) { 104 | logger.error("[send email] content cannot be empty"); 105 | return; 106 | } 107 | 108 | mailClient.sendMail(to, subject, content); 109 | } 110 | 111 | /** 112 | * 处理 ConsumerRecord 通用逻辑,发送消息时应是JSON串,将其解析为 Event 并返回。 113 | * @param record 消息记录,格式应该是 Event 的JSON字符串。 114 | * @return 解析得到的 Event 对象,为 null 则说明消息为空或者解析失败。 115 | */ 116 | private static Event dealRecord(ConsumerRecord record) { 117 | if (record == null || record.value() == null) { 118 | logger.error("消息的内容为空"); 119 | return null; 120 | } 121 | Event event = JSON.parseObject(record.value(), Event.class); 122 | if (event == null) { 123 | logger.error("消息格式错误"); 124 | return null; 125 | } 126 | return event; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/event/EventProducer.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.event; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import lombok.extern.slf4j.Slf4j; 6 | import me.wangao.community.entity.Event; 7 | import org.springframework.kafka.core.KafkaTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Resource; 11 | 12 | @Component 13 | @Slf4j 14 | public class EventProducer { 15 | 16 | @Resource 17 | private KafkaTemplate kafkaTemplate; 18 | 19 | // 处理事件 20 | public void fireEvent(Event event) { 21 | if (event == null) { 22 | log.error("event cannot be null"); 23 | return; 24 | } 25 | if (event.getTopic() == null) { 26 | log.error("topic cannot be null"); 27 | return; 28 | } 29 | // 将事件发布到指定的主题 30 | kafkaTemplate.send(event.getTopic(), JSON.toJSONString(event)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/quartz/PostScoreRefreshJob.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.quartz; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import me.wangao.community.service.DiscussPostService; 5 | import me.wangao.community.service.ElasticsearchService; 6 | import me.wangao.community.service.LikeService; 7 | import me.wangao.community.util.CommunityConstant; 8 | import me.wangao.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.data.redis.core.BoundSetOperations; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | 17 | import javax.annotation.Resource; 18 | import java.text.ParseException; 19 | import java.text.SimpleDateFormat; 20 | import java.util.Date; 21 | 22 | public class PostScoreRefreshJob implements Job, CommunityConstant { 23 | 24 | private final static Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class); 25 | 26 | @Resource 27 | private RedisTemplate redisTemplate; 28 | 29 | @Resource 30 | private DiscussPostService discussPostService; 31 | 32 | @Resource 33 | private LikeService likeService; 34 | 35 | @Resource 36 | private ElasticsearchService elasticsearchService; 37 | 38 | private static final Date epoch; 39 | 40 | static { 41 | try { 42 | epoch = new SimpleDateFormat("yyyy-MM-dd").parse("2021-01-01"); 43 | } catch (ParseException e) { 44 | throw new RuntimeException("初始化起始时间失败"); 45 | } 46 | } 47 | 48 | @Override 49 | public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { 50 | String scoreKey = RedisKeyUtil.getPostScoreKey(); 51 | BoundSetOperations operations = redisTemplate.boundSetOps(scoreKey); 52 | 53 | Long size = operations.size(); 54 | if (size == null || size == 0) { 55 | logger.info("任务取消,没有需要刷新的帖子"); 56 | return; 57 | } 58 | 59 | logger.info("任务开始,正在刷新帖子"); 60 | 61 | while (operations.size() > 0) { 62 | Integer postId = (Integer ) operations.pop(); 63 | if (postId != null) { 64 | refresh((int)postId); 65 | } 66 | } 67 | 68 | logger.info("任务结束,帖子刷新完毕"); 69 | } 70 | 71 | private void refresh(int postId) { 72 | DiscussPost post = discussPostService.findDiscussPostById(postId); 73 | if (post == null ) { 74 | logger.error("该帖子不存在,id = " + postId); 75 | return; 76 | } 77 | 78 | // 是否精华 79 | boolean wonderful = post.getStatus() == 1; 80 | // 评论数量 81 | int commentCount = post.getCommentCount(); 82 | // 点赞数量 83 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId); 84 | 85 | // 计算权重 86 | double weight = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2; 87 | // 计算分数 88 | double score = Math.log10(Math.max(weight, 1)) + (post.getCreateTime().getTime() - epoch.getTime()) / (1000. * 3600 * 24); 89 | 90 | // 更新帖子分数 91 | discussPostService.updateScore(postId, score); 92 | 93 | // 同步搜索数据 94 | post.setScore(score); 95 | elasticsearchService.saveDiscussPost(post); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.dao.CommentMapper; 4 | import me.wangao.community.entity.Comment; 5 | import me.wangao.community.util.CommunityConstant; 6 | import me.wangao.community.util.RedisKeyUtil; 7 | import me.wangao.community.util.SensitiveFilter; 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 javax.annotation.Resource; 15 | import java.util.List; 16 | 17 | @Service 18 | public class CommentService implements CommunityConstant { 19 | 20 | @Resource 21 | private CommentMapper commentMapper; 22 | 23 | @Resource 24 | private SensitiveFilter sensitiveFilter; 25 | 26 | @Resource 27 | private DiscussPostService discussPostService; 28 | 29 | @Resource 30 | private CounterService counterService; 31 | 32 | public List findCommentsByEntity(int entityType, int entityId, int offset, int limit) { 33 | return commentMapper.selectCommentByEntity(entityType, entityId, offset, limit); 34 | } 35 | 36 | public int findCommentCount(int entityType, int entityId) { 37 | return commentMapper.selectCountByEntity(entityType, entityId); 38 | } 39 | 40 | @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) 41 | public int addComment(Comment comment) { 42 | if (comment == null) { 43 | throw new IllegalArgumentException("参数不能为空"); 44 | } 45 | 46 | // 添加评论 47 | comment.setContent(HtmlUtils.htmlEscape(comment.getContent())) 48 | .setContent(sensitiveFilter.filter(comment.getContent())); 49 | 50 | int rows = commentMapper.insertComment(comment); 51 | 52 | // 更新帖子评论数量 53 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 54 | int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId()); 55 | discussPostService.updateCommentCount(comment.getEntityId(), count); 56 | } 57 | 58 | // 更新计数器 59 | counterService.incr(RedisKeyUtil.getCommentCounterKey()); 60 | 61 | return rows; 62 | } 63 | 64 | public Comment findCommentById(int id) { 65 | return commentMapper.selectCommentById(id); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/CounterService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.util.RedisKeyUtil; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.annotation.Resource; 8 | 9 | /** 10 | * 计数器服务 11 | */ 12 | @Service 13 | public class CounterService { 14 | 15 | @Resource 16 | private RedisTemplate redisTemplate; 17 | 18 | public void incr(String key) { 19 | redisTemplate.opsForValue().increment(key); 20 | } 21 | 22 | public void decr(String key) { 23 | redisTemplate.opsForValue().decrement(key); 24 | } 25 | 26 | public int count(String key) { 27 | Object counter = redisTemplate.opsForValue().get(key); 28 | if (counter == null) { 29 | return 0; 30 | } 31 | if (counter instanceof Integer) { 32 | return (Integer) counter; 33 | } 34 | return Integer.parseInt((String) counter); 35 | } 36 | 37 | public void set(String key, int count) { 38 | redisTemplate.opsForValue().set(key, count); 39 | } 40 | 41 | public int getUserCount() { 42 | return getCount(RedisKeyUtil.getUserCounterKey()); 43 | } 44 | 45 | public int getPostCount() { 46 | return getCount(RedisKeyUtil.getPostCounterKey()); 47 | } 48 | 49 | public int getCommentCount() { 50 | return getCount(RedisKeyUtil.getCommentCounterKey()); 51 | } 52 | 53 | public int getNodePostCount(int nodeId) { 54 | return getCount(RedisKeyUtil.getNodePostCounterKey(nodeId)); 55 | } 56 | 57 | private int getCount(String key) { 58 | Integer count = (Integer) redisTemplate.opsForValue().get(key); 59 | return count == null ? 0 : count; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/ElasticsearchService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.dao.elasticsearch.DiscussPostRepository; 4 | import me.wangao.community.entity.DiscussPost; 5 | import me.wangao.community.entity.Page; 6 | import me.wangao.community.entity.SearchPage; 7 | import org.elasticsearch.index.query.QueryBuilders; 8 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; 9 | import org.elasticsearch.search.sort.SortBuilders; 10 | import org.elasticsearch.search.sort.SortOrder; 11 | import org.springframework.data.domain.PageRequest; 12 | import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; 13 | import org.springframework.data.elasticsearch.core.SearchHits; 14 | import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; 15 | import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; 16 | import org.springframework.stereotype.Service; 17 | 18 | import javax.annotation.Resource; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | @Service 24 | public class ElasticsearchService { 25 | 26 | @Resource 27 | private DiscussPostRepository discussRepository; 28 | 29 | @Resource 30 | private ElasticsearchRestTemplate elasticTemplate; 31 | 32 | public void saveDiscussPost(DiscussPost discussPost) { 33 | discussRepository.save(discussPost); 34 | } 35 | 36 | public void deleteDiscussPost(int id) { 37 | discussRepository.deleteById(id); 38 | } 39 | 40 | public SearchPage searchDiscussPost(String keyword, int current, int limit) { 41 | NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() 42 | .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content")) 43 | .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) 44 | .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)) 45 | .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) 46 | .withPageable(PageRequest.of(current, limit)) 47 | .withHighlightFields( 48 | new HighlightBuilder.Field("title").preTags("").postTags(""), 49 | new HighlightBuilder.Field("content").preTags("").postTags("") 50 | ) 51 | .build(); 52 | 53 | SearchHits searchHits = elasticTemplate.search(searchQuery, DiscussPost.class); 54 | System.out.println("total hits: " + searchHits.getTotalHits()); 55 | 56 | List postList = new ArrayList<>(); 57 | searchHits.forEach(hit -> { 58 | DiscussPost post = hit.getContent(); 59 | if (!hit.getHighlightField("content").isEmpty()) { 60 | post.setContent(hit.getHighlightField("content").get(0)); 61 | } 62 | if (!hit.getHighlightField("title").isEmpty()) { 63 | post.setTitle(hit.getHighlightField("title").get(0)); 64 | } 65 | postList.add(post); 66 | }); 67 | 68 | return new SearchPage() 69 | .setCurrent(current) 70 | .setLimit(limit) 71 | .setTotal(searchHits.getTotalHits()) 72 | .setList(postList); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/FollowService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.entity.User; 4 | import me.wangao.community.util.CommunityConstant; 5 | import me.wangao.community.util.RedisKeyUtil; 6 | import org.springframework.dao.DataAccessException; 7 | import org.springframework.data.redis.core.RedisOperations; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | import org.springframework.data.redis.core.SessionCallback; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.annotation.Resource; 13 | import java.util.*; 14 | import java.util.stream.Collectors; 15 | 16 | @Service 17 | public class FollowService implements CommunityConstant { 18 | 19 | @Resource 20 | private RedisTemplate redisTemplate; 21 | 22 | @Resource 23 | private UserService userService; 24 | 25 | public void follow(int userId, int entityType, int entityId) { 26 | redisTemplate.execute(new SessionCallback() { 27 | @Override 28 | public Object execute(RedisOperations operations) throws DataAccessException { 29 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 30 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 31 | 32 | operations.multi(); 33 | 34 | operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis()); 35 | operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis()); 36 | 37 | return operations.exec(); 38 | } 39 | }); 40 | } 41 | 42 | public void unfollow(int userId, int entityType, int entityId) { 43 | redisTemplate.execute(new SessionCallback() { 44 | @Override 45 | public Object execute(RedisOperations operations) throws DataAccessException { 46 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 47 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 48 | 49 | operations.multi(); 50 | 51 | operations.opsForZSet().remove(followeeKey, entityId); 52 | operations.opsForZSet().remove(followerKey, userId); 53 | 54 | return operations.exec(); 55 | } 56 | }); 57 | } 58 | 59 | // 查询目标关注的实体数量 60 | public long findFolloweeCount(int userId, int entityType) { 61 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 62 | Long count = redisTemplate.opsForZSet().zCard(followeeKey); 63 | 64 | return count == null ? 0 : count; 65 | } 66 | 67 | // 查询实体的粉丝数量 68 | public long findFollowerCount(int entityType, int entityId) { 69 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 70 | Long count = redisTemplate.opsForZSet().zCard(followerKey); 71 | 72 | return count == null ? 0 : count; 73 | } 74 | 75 | // 查询用户是否关注某实体 76 | public boolean hasFollowed(int userId, int entityType, int entityId) { 77 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 78 | 79 | return redisTemplate.opsForZSet().score(followeeKey, entityId) != null; 80 | } 81 | 82 | // 查询某用户关注的人 83 | public List> findFollowees(int userId, int offset, int limit) { 84 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER); 85 | Set ids = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1); 86 | 87 | if (ids == null) { 88 | return null; 89 | } 90 | 91 | List targetIds = ids.stream().map(e -> (Integer) e).collect(Collectors.toList()); 92 | 93 | List> list = new ArrayList<>(); 94 | targetIds.forEach(targetId -> { 95 | Map map = new HashMap<>(); 96 | User user = userService.findUserById(targetId); 97 | map.put("user", user); 98 | Double score = redisTemplate.opsForZSet().score(followeeKey, targetId); 99 | map.put("followTime", score == null ? new Date(0) : new Date(score.longValue())); 100 | list.add(map); 101 | }); 102 | 103 | return list; 104 | } 105 | 106 | // 查询某用户的粉丝 107 | public List> findFollowers(int userId, int offset, int limit) { 108 | String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId); 109 | 110 | Set ids = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1); 111 | 112 | if (ids == null) { 113 | return null; 114 | } 115 | 116 | List> list = new ArrayList<>(); 117 | ids.stream().map(e -> (Integer) e).collect(Collectors.toList()).forEach(targetId -> { 118 | Map map = new HashMap<>(); 119 | User user = userService.findUserById(targetId); 120 | map.put("user", user); 121 | Double score = redisTemplate.opsForZSet().score(followerKey, targetId); 122 | map.put("followTime", score == null ? new Date(0) : new Date(score.longValue())); 123 | list.add(map); 124 | }); 125 | 126 | return list; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/InitialService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.wangao.community.dao.CommentMapper; 5 | import me.wangao.community.dao.DiscussPostMapper; 6 | import me.wangao.community.dao.NodeMapper; 7 | import me.wangao.community.dao.UserMapper; 8 | import me.wangao.community.entity.Node; 9 | import me.wangao.community.util.RedisKeyUtil; 10 | import org.springframework.boot.CommandLineRunner; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.annotation.Resource; 14 | import java.util.List; 15 | import java.util.concurrent.CountDownLatch; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | @Service 20 | @Slf4j 21 | public class InitialService implements CommandLineRunner { 22 | 23 | @Resource 24 | private CounterService counterService; 25 | 26 | @Resource 27 | private UserMapper userMapper; 28 | 29 | @Resource 30 | private DiscussPostMapper discussPostMapper; 31 | 32 | @Resource 33 | private CommentMapper commentMapper; 34 | 35 | @Resource 36 | private NodeMapper nodeMapper; 37 | 38 | // private static final String userCounterThreadName = "userCounterInit"; 39 | // private static final String postCounterThreadName = "postCounterInit"; 40 | // private static final String commentCounterThreadName = "commentCounterInit"; 41 | // private static final String nodePostCounterThreadName = "nodePostCounterInit"; 42 | 43 | 44 | @Override 45 | public void run(String... args) throws Exception { 46 | // TODO 初始化计数器 47 | counterService.set(RedisKeyUtil.getUserCounterKey(), 0); 48 | counterService.set(RedisKeyUtil.getPostCounterKey(), 0); 49 | counterService.set(RedisKeyUtil.getCommentCounterKey(), 0); 50 | counterService.set(RedisKeyUtil.getNodePostCounterKey(1), 0); 51 | log.info("开始刷新计数器"); 52 | 53 | CountDownLatch counterLatch = new CountDownLatch(4); 54 | ExecutorService executor = Executors.newFixedThreadPool(50); 55 | 56 | executor.execute(() -> { 57 | int userCount = userMapper.selectRows(); 58 | counterService.set(RedisKeyUtil.getUserCounterKey(), userCount); 59 | log.info("已刷新用户计数器"); 60 | counterLatch.countDown(); 61 | }); 62 | 63 | executor.execute(() -> { 64 | int postCount = discussPostMapper.selectDiscussPostRows(null); 65 | counterService.set(RedisKeyUtil.getPostCounterKey(), postCount); 66 | log.info("已刷新帖子计数器"); 67 | counterLatch.countDown(); 68 | }); 69 | 70 | executor.execute(() -> { 71 | int commentRows = commentMapper.selectRows(); 72 | counterService.set(RedisKeyUtil.getCommentCounterKey(), commentRows); 73 | log.info("已刷新评论帖子计数器"); 74 | counterLatch.countDown(); 75 | }); 76 | 77 | executor.execute(() -> { 78 | List nodes = nodeMapper.selectAllNodes(); 79 | CountDownLatch nodePostCounterLatch = new CountDownLatch(nodes.size()); 80 | nodes.forEach(node -> { 81 | int nodePostCount = discussPostMapper.selectRowsByNodeId(node.getId()); 82 | counterService.set(RedisKeyUtil.getNodePostCounterKey(node.getId()), nodePostCount); 83 | log.info("已刷新评论节点计数器(" + node.getName() + ")"); 84 | nodePostCounterLatch.countDown(); 85 | }); 86 | try { 87 | nodePostCounterLatch.await(); 88 | } catch (InterruptedException e) { 89 | log.error(e.getMessage()); 90 | e.printStackTrace(); 91 | } 92 | }); 93 | 94 | counterLatch.await(); 95 | log.info("已刷新所有计数器"); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/LikeService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.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 | import javax.annotation.Resource; 12 | 13 | @Service 14 | public class LikeService { 15 | 16 | @Resource 17 | private RedisTemplate redisTemplate; 18 | 19 | // 点赞 20 | public void like(int userId, int entityType, int entityId, int entityUserId) { 21 | redisTemplate.execute(new SessionCallback() { 22 | @Override 23 | public Object execute(RedisOperations operations) throws DataAccessException { 24 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 25 | String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId); 26 | Boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId); 27 | 28 | operations.multi(); 29 | 30 | if (isMember != null && isMember) { 31 | operations.opsForSet().remove(entityLikeKey, userId); 32 | operations.opsForValue().decrement(userLikeKey); 33 | } else { 34 | operations.opsForSet().add(entityLikeKey, userId); 35 | operations.opsForValue().increment(userLikeKey); 36 | } 37 | 38 | return operations.exec(); 39 | } 40 | }); 41 | } 42 | 43 | // 查询某实体点赞的数量 44 | public long findEntityLikeCount(int entityType, int entityId) { 45 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 46 | Long count = redisTemplate.opsForSet().size(entityLikeKey); 47 | return count == null ? 0 : count; // 避免空指针异常 48 | } 49 | 50 | // 查询某人对某实体的点赞状态,使用int方便以后拓展业务,比如点踩等功能 51 | public int findEntityLikeStatus(int userId, int entityType, int entityId) { 52 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 53 | Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId); 54 | 55 | int ret = 0; 56 | if (isMember != null && isMember) { 57 | ret = 1; 58 | } 59 | 60 | return ret; 61 | } 62 | 63 | // 查询某用户获得的赞 64 | public int findUserLikeCount(int userId) { 65 | String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); 66 | Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey); 67 | 68 | return count == null ? 0 : count; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.dao.MessageMapper; 4 | import me.wangao.community.entity.Message; 5 | import me.wangao.community.util.SensitiveFilter; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.util.HtmlUtils; 9 | 10 | import javax.annotation.Resource; 11 | import java.util.Date; 12 | import java.util.List; 13 | 14 | @Service 15 | public class MessageService { 16 | 17 | @Resource 18 | MessageMapper messageMapper; 19 | 20 | @Resource 21 | SensitiveFilter sensitiveFilter; 22 | 23 | public List findConversations(int userId, int offset, int limit) { 24 | return messageMapper.selectConversations(userId, offset, limit); 25 | } 26 | 27 | public int findConversationCount(int userId) { 28 | return messageMapper.selectConversationCount(userId); 29 | } 30 | 31 | public List findLetters(String conversationId, int offset, int limit) { 32 | return messageMapper.selectLetters(conversationId, offset, limit); 33 | } 34 | 35 | public int findLetterCount(String conversationId) { 36 | return messageMapper.selectLetterCount(conversationId); 37 | } 38 | 39 | public int findLetterUnreadCount(int userId, String conversationId) { 40 | return messageMapper.selectLetterUnreadCount(userId, conversationId); 41 | } 42 | 43 | public int addMessage(Message message) { 44 | message.setContent(HtmlUtils.htmlEscape(message.getContent())) 45 | .setContent(sensitiveFilter.filter(message.getContent())) 46 | .setStatus(0) 47 | .setCreateTime(new Date()); 48 | 49 | if (StringUtils.isBlank(message.getConversationId())) { 50 | if (message.getFromId() < message.getToId()) { 51 | message.setConversationId(message.getFromId() + "_" + message.getToId()); 52 | } else { 53 | message.setConversationId(message.getToId() + "_" + message.getFromId()); 54 | } 55 | } 56 | 57 | return messageMapper.insertMessage(message); 58 | } 59 | 60 | public int readMessage(List ids) { 61 | return messageMapper.updateStatus(ids, 1); 62 | } 63 | 64 | public Message findLatestNotice(int userId, String topic) { 65 | return messageMapper.selectLatestNotice(userId, topic); 66 | } 67 | 68 | public int findNoticeCount(int userId, String topic) { 69 | return messageMapper.selectNoticeCount(userId, topic); 70 | } 71 | 72 | public int findNoticeUnreadCount(int userId, String topic) { 73 | return messageMapper.selectNoticeUnreadCount(userId, topic); 74 | } 75 | 76 | public List findNotices(int userId, String topic, int offset, int limit) { 77 | return messageMapper.selectNotices(userId, topic, offset, limit); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/service/NodeService.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.dao.NodeMapper; 4 | import me.wangao.community.entity.Node; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.annotation.Resource; 8 | import java.util.List; 9 | 10 | @Service 11 | public class NodeService { 12 | 13 | @Resource 14 | private NodeMapper nodeMapper; 15 | 16 | public Node findNodeById(int id) { 17 | return nodeMapper.selectById(id); 18 | } 19 | 20 | public List findAllNodes() { 21 | return nodeMapper.selectAllNodes(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/CommunityConstant.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.util; 2 | 3 | public interface CommunityConstant { 4 | 5 | // 激活成功 6 | int ACTIVATION_SUCCESS = 0; 7 | 8 | // 重复激活 9 | int ACTIVATION_REPEAT = 1; 10 | 11 | // 激活失败 12 | int ACTIVATION_FAILURE = 2; 13 | 14 | // 默认状态的登录凭证超时时间 15 | int DEFAULT_EXPIRED_SECOND = 3600 * 12; 16 | 17 | // 记住状态下的的登录凭证超时时间 18 | int REMEMBER_EXPIRED_SECOND = 3600 * 12 * 100; 19 | 20 | // 实体类型:帖子 21 | int ENTITY_TYPE_POST = 1; 22 | 23 | // 实体类型:评论 24 | int ENTITY_TYPE_COMMENT = 2; 25 | 26 | // 实体类型:用户 27 | int ENTITY_TYPE_USER = 3; 28 | 29 | // Kafka 主题:评论 30 | String TOPIC_COMMENT = "comment"; 31 | 32 | // 主题:点赞 33 | String TOPIC_LIKE = "like"; 34 | 35 | // 主题:关注 36 | String TOPIC_FOLLOW = "follow"; 37 | 38 | // 主题:发帖 39 | String TOPIC_PUBLISH = "publish"; 40 | 41 | // 主题:删帖 42 | String TOPIC_DELETE = "delete"; 43 | 44 | // 主题:邮件 45 | String TOPIC_EMAIL = "sendEmail"; 46 | 47 | // 系统用户的ID 48 | int SYSTEM_USER_ID = 1; 49 | 50 | // 权限:普通用户 51 | String AUTHORITY_USER = "user"; 52 | 53 | // 权限:管理员 54 | String AUTHORITY_ADMIN = "admin"; 55 | 56 | // 权限:版主 57 | String AUTHORITY_MODERATOR = "moderator"; 58 | 59 | // 帖子状态:正常 60 | int POST_STATUS_NORMAL = 0; 61 | 62 | // 帖子状态:精华 63 | int POST_STATUS_HIGHLIGHT = 1; 64 | 65 | // 帖子状态:拉黑 66 | int POST_STATUS_DELETED = 2; 67 | 68 | // 帖子类型:普通 69 | int POST_TYPE_NORMAL = 0; 70 | 71 | // 帖子类型:置顶 72 | int POST_TYPE_TOP = 1; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/CommunityUtil.java: -------------------------------------------------------------------------------- 1 | package me.wangao.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.Calendar; 8 | import java.util.Date; 9 | import java.util.Map; 10 | import java.util.UUID; 11 | 12 | public class CommunityUtil { 13 | 14 | // 生成随机字符串 15 | public static String generateUUID() { 16 | return UUID.randomUUID().toString().replaceAll("-", ""); 17 | } 18 | 19 | // MD5加密 20 | public static String md5(String key) { 21 | if (StringUtils.isBlank(key)) { 22 | return null; 23 | } 24 | return DigestUtils.md5DigestAsHex(key.getBytes()); 25 | } 26 | 27 | public static String getJSONString(int code, String msg, Map map) { 28 | JSONObject json = new JSONObject(); 29 | json.put("code", code); 30 | json.put("msg", msg); 31 | 32 | if (map != null) { 33 | map.forEach(json::put); 34 | } 35 | 36 | return json.toJSONString(); 37 | } 38 | 39 | public static String getJSONString(int code, String msg) { 40 | return getJSONString(code, msg, null); 41 | } 42 | 43 | public static String getJSONString(int code) { 44 | return getJSONString(code, null, null); 45 | } 46 | 47 | // 将日期转换为当日零点 48 | public static Date getDayStart(Calendar calendar) { 49 | calendar = (Calendar) calendar.clone(); 50 | calendar.set(Calendar.HOUR_OF_DAY, 0); 51 | calendar.set(Calendar.MINUTE, 0); 52 | calendar.set(Calendar.SECOND, 0); 53 | return calendar.getTime(); 54 | } 55 | 56 | // 将日期转换为当日23点59分59秒 57 | public static Date getDayEnd(Calendar calendar) { 58 | calendar = (Calendar) calendar.clone(); 59 | calendar.set(Calendar.HOUR_OF_DAY, 23); 60 | calendar.set(Calendar.MINUTE, 59); 61 | calendar.set(Calendar.SECOND, 59); 62 | return calendar.getTime(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package me.wangao.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 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/HostHolder.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.util; 2 | 3 | import me.wangao.community.entity.User; 4 | import org.springframework.stereotype.Component; 5 | 6 | // 持有用户信息,用于代替session对象 7 | @Component 8 | public class HostHolder { 9 | private final ThreadLocal users = new ThreadLocal<>(); 10 | 11 | public void setUser(User user) { 12 | users.set(user); 13 | } 14 | 15 | public User getUser() { 16 | return users.get(); 17 | } 18 | 19 | public void clear() { 20 | users.remove(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/MailClient.java: -------------------------------------------------------------------------------- 1 | package me.wangao.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 | @Component 15 | public class MailClient { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(MailClient.class); 18 | 19 | private JavaMailSender mailSender; 20 | 21 | @Autowired 22 | public MailClient(JavaMailSender mailSender) { 23 | this.mailSender = mailSender; 24 | } 25 | 26 | @Value("${spring.mail.username}") 27 | private String from; 28 | 29 | public void sendMail(String to, String subject, String content) { 30 | try { 31 | MimeMessage message = mailSender.createMimeMessage(); 32 | MimeMessageHelper helper = new MimeMessageHelper(message); 33 | helper.setFrom(from); 34 | helper.setTo(to); 35 | helper.setSubject(subject); 36 | helper.setText(content, true); 37 | mailSender.send(helper.getMimeMessage()); 38 | } catch (MessagingException e) { 39 | e.printStackTrace(); 40 | logger.error("发送邮件失败" + e.getMessage()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.util; 2 | 3 | public class RedisKeyUtil { 4 | 5 | private static final String SPLIT = ":"; 6 | private static final String PREFIX_ENTITY_LIKE = "like:entity"; 7 | private static final String PREFIX_USER_LIKE = "like:user"; 8 | private static final String PREFIX_FOLLOWEE = "followee"; 9 | private static final String PREFIX_FOLLOWER = "follower"; 10 | private static final String PREFIX_CAPTCHA = "captcha"; 11 | private static final String PREFIX_TICKET = "ticket"; 12 | private static final String PREFIX_USER = "user"; 13 | private static final String PREFIX_UV = "uv"; 14 | private static final String PREFIX_DAU = "dau"; 15 | private static final String PREFIX_POST = "post"; 16 | private static final String PREFIX_COUNTER = "counter"; 17 | 18 | private static final String COUNTER_KEY_USER = "user"; 19 | private static final String COUNTER_KEY_POST = "post"; 20 | private static final String COUNTER_KEY_COMMENT = "comment"; 21 | private static final String COUNTER_KEY_NODE_TOPIC = COUNTER_KEY_POST + SPLIT + "node"; 22 | 23 | private static final String COUNTER_ALL = "all"; 24 | private static final String COUNTER_VIEW = "view"; 25 | 26 | // 某个实体的赞 27 | // like:entity:entityType:entityId -> set 28 | public static String getEntityLikeKey(int entityType, int entityId) { 29 | return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId; 30 | } 31 | 32 | // 某用户获得的赞 33 | // like:user:UserId -> int 34 | public static String getUserLikeKey(int userId) { 35 | return PREFIX_USER_LIKE + SPLIT + userId; 36 | } 37 | 38 | // 某个用户关注的实体 39 | // followee:userId:entityType -> zset 40 | public static String getFolloweeKey(int userId, int entityType) { 41 | return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; 42 | } 43 | 44 | // 某个实体拥有的粉丝 45 | // follower:entityType:entityId -> zset 46 | public static String getFollowerKey(int entityType, int entityId) { 47 | return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId; 48 | } 49 | 50 | // 登录验证码 51 | public static String getCaptchaKey(String owner) { 52 | return PREFIX_CAPTCHA + SPLIT + owner; 53 | } 54 | 55 | // 登录凭证 56 | public static String getTicketKey(String ticket) { 57 | return PREFIX_TICKET + SPLIT + ticket; 58 | } 59 | 60 | // 用户 61 | public static String getUserKey(int userId) { 62 | return PREFIX_USER + SPLIT + userId; 63 | } 64 | 65 | // 单日UV 66 | public static String getUVKey(String date) { 67 | return PREFIX_UV + SPLIT + date; 68 | } 69 | 70 | // 区间uv 71 | public static String getUVKey(String startDate, String endDate) { 72 | return PREFIX_UV + SPLIT + startDate + SPLIT + endDate; 73 | } 74 | 75 | // 单日活跃用户 76 | public static String getDAUKey(String date) { 77 | return PREFIX_DAU + SPLIT + date; 78 | } 79 | 80 | // 区间活跃用户 81 | public static String getDAUKey(String startDate, String endDate) { 82 | return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate; 83 | } 84 | 85 | // 统计帖子分数 86 | public static String getPostScoreKey() { 87 | return PREFIX_POST + SPLIT + "score"; 88 | } 89 | 90 | // 全站用户计数器 91 | public static String getUserCounterKey() { 92 | return PREFIX_COUNTER + SPLIT + COUNTER_KEY_USER + SPLIT + COUNTER_ALL; 93 | } 94 | 95 | // 全站话题计数器 96 | public static String getPostCounterKey() { 97 | return PREFIX_COUNTER + SPLIT + COUNTER_KEY_POST + SPLIT + COUNTER_ALL; 98 | } 99 | 100 | // 全站回复计数器 101 | public static String getCommentCounterKey() { 102 | return PREFIX_COUNTER + SPLIT + COUNTER_KEY_COMMENT + SPLIT + COUNTER_ALL; 103 | } 104 | 105 | // 节点话题计数器 106 | public static String getNodePostCounterKey(int nodeId) { 107 | return PREFIX_COUNTER + SPLIT + COUNTER_KEY_NODE_TOPIC + SPLIT + nodeId; 108 | } 109 | 110 | // 帖子浏览量计数器 111 | public static String getPostViewCounterKey() { 112 | return PREFIX_COUNTER + SPLIT + COUNTER_KEY_POST + SPLIT + COUNTER_VIEW; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/me/wangao/community/util/SensitiveFilter.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.util; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | import org.apache.commons.lang3.CharUtils; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.stereotype.Component; 13 | 14 | import javax.annotation.PostConstruct; 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.InputStreamReader; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | @Component 23 | public class SensitiveFilter { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class); 26 | 27 | // 替换符 28 | private static final String REPLACE_SYMBOL = "***"; 29 | 30 | // 根节点 31 | private final TrieNode root = new TrieNode(); 32 | 33 | @PostConstruct 34 | public void init() { 35 | try(InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); 36 | BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { 37 | String keyword; 38 | while ((keyword = reader.readLine()) != null) { 39 | // 将敏感词添加到前缀树 40 | addKeyword(keyword); 41 | } 42 | } catch (IOException e) { 43 | logger.error("加载敏感词文件失败:" + e.getMessage()); 44 | } 45 | } 46 | 47 | // 将一个敏感词添加到前缀树 48 | private void addKeyword(String keyword) { 49 | TrieNode tempNode = root; 50 | for (int i = 0; i < keyword.length(); ++i) { 51 | char c = keyword.charAt(i); 52 | TrieNode subNode = tempNode.getSubNode(c); 53 | 54 | if (subNode == null) { 55 | // 初始化子节点 56 | subNode = new TrieNode(); 57 | tempNode.addSubNode(c, subNode); 58 | } 59 | 60 | tempNode = subNode; 61 | 62 | // 标识结束字符 63 | if (i == keyword.length() - 1) { 64 | subNode.setKeywordEnd(true); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * 过滤敏感词 71 | * @param text 待过滤的文本 72 | * @return 过滤后的文本 73 | */ 74 | public String filter(String text) { 75 | if (StringUtils.isBlank(text)) { 76 | return null; 77 | } 78 | 79 | TrieNode tempNode = root; 80 | int begin = 0; 81 | int position = 0; 82 | 83 | StringBuilder sb = new StringBuilder(); 84 | 85 | while (begin < text.length()) { 86 | if (position >= text.length()) { 87 | // position到头 88 | begin++; 89 | position = begin; 90 | tempNode = root; 91 | } 92 | char c = text.charAt(position); 93 | 94 | // 跳过符号 95 | if(isSymbol(c)) { 96 | if (tempNode == root) { 97 | sb.append(c); 98 | begin++; 99 | } 100 | position++; 101 | continue; 102 | } 103 | 104 | tempNode = tempNode.getSubNode(c); 105 | if (tempNode == null) { 106 | // 以begin开头的字符串不是敏感词 107 | sb.append(text.charAt(begin)); 108 | begin++; 109 | position = begin; 110 | tempNode = root; 111 | } else if (tempNode.isKeywordEnd()){ 112 | // 发现敏感词 113 | sb.append(REPLACE_SYMBOL); 114 | // 进入下一个位置 115 | position++; 116 | begin = position; 117 | tempNode = root; 118 | } else { 119 | // 继续检查下一个字符 120 | position++; 121 | } 122 | } 123 | 124 | return sb.toString(); 125 | } 126 | 127 | // 判断是否为符号 128 | private boolean isSymbol(Character c) { 129 | // 0x2e80 - 0x9fff 是东亚文字范围 130 | return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2e80 || c > 0x9fff); 131 | } 132 | 133 | // 前缀树 134 | @Data 135 | @Accessors(chain = true) 136 | private class TrieNode { 137 | 138 | // 关键词结束标识 139 | private boolean isKeywordEnd = false; 140 | 141 | // 子节点(key是下级字符, value 是下级节点) 142 | @Getter(AccessLevel.NONE) 143 | @Setter(AccessLevel.NONE) 144 | private Map subNodes = new HashMap<>(); 145 | 146 | // 添加子节点 147 | public void addSubNode(Character c, TrieNode node) { 148 | subNodes.put(c, node); 149 | } 150 | 151 | // 获取子节点 152 | public TrieNode getSubNode(Character c) { 153 | return subNodes.get(c); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | thymeleaf: 3 | cache: false 4 | 5 | datasource: 6 | url: jdbc:mysql://localhost:3306/flow?serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useUnicode=true 7 | username: root 8 | password: 123456 9 | driver-class-name: com.mysql.cj.jdbc.Driver 10 | 11 | mail: 12 | host: ${mail.host} 13 | port: ${mail.port:465} 14 | username: ${mail.username} 15 | password: ${mail.password} 16 | protocol: smtps 17 | properties: 18 | mail: 19 | smtp: 20 | ssl: 21 | enable: true 22 | 23 | # RedisProperties 24 | redis: 25 | database: 10 26 | host: localhost 27 | port: 6379 28 | 29 | # kafka 30 | kafka: 31 | bootstrap-servers: localhost:9092 32 | consumer: 33 | group-id: test-consumer-group 34 | enable-auto-commit: true 35 | auto-commit-interval: 3000 36 | quartz: 37 | job-store-type: jdbc 38 | scheduler-name: communityScheduler 39 | properties: 40 | org: 41 | quartz: 42 | scheduler: 43 | instanceId: AUTO 44 | jobStore: 45 | class: org.quartz.impl.jdbcjobstore.JobStoreTX 46 | driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate 47 | isClustered: true 48 | threadPool: 49 | class: org.quartz.simpl.SimpleThreadPool 50 | threadCount: 5 51 | 52 | 53 | # community 54 | community: 55 | path: 56 | domain: http://localhost:8080 57 | upload: D:/work/data/upload 58 | # 七牛 59 | qiniu: 60 | key: 61 | access: ${qiniu.access} 62 | secret: ${qiniu.secret} 63 | bucket: 64 | common: 65 | name: ${qiniu.bucket.name} 66 | url: ${qiniu.bucket.url} 67 | 68 | caffeine: 69 | posts: 70 | max-size: 15 71 | expire-seconds: 180 72 | 73 | mybatis: 74 | mapper-locations: classpath:mapper/*.xml 75 | type-aliases-package: me.wangao.community.entity 76 | configuration: 77 | use-generated-keys: true 78 | map-underscore-to-camel-case: true 79 | 80 | logging: 81 | level: 82 | me: 83 | wangao: 84 | community: debug 85 | server: 86 | servlet: 87 | context-path: / 88 | 89 | management: 90 | endpoints: 91 | web: 92 | exposure: 93 | include: "*" -------------------------------------------------------------------------------- /src/main/resources/mapper/CommentMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | id, user_id, entity_type, entity_id, target_id, content, status, create_time 8 | 9 | 10 | 11 | user_id, entity_type, entity_id, target_id, content, status, create_time 12 | 13 | 14 | 21 | 22 | 27 | 28 | 33 | 34 | 35 | insert into comment() 36 | values (#{userId}, #{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime}) 37 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/main/resources/mapper/DiscussPostMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | id, user_id, node_id, title, content, type, status, create_time, comment_count, score 9 | 10 | 11 | 26 | 27 | 35 | 36 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /src/main/resources/mapper/MessageMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 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 | 26 | 27 | 35 | 36 | 44 | 45 | 50 | 51 | 59 | 60 | 68 | 69 | 76 | 77 | 78 | insert into message() 79 | values (#{fromId}, #{toId}, #{conversationId}, #{content}, #{status}, #{createTime}) 80 | 81 | 82 | 83 | update message set status = #{status} 84 | where id in 85 | 86 | #{id} 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | id, username, password, salt, email, type, status, activation_code, header_url, create_time 9 | 10 | 11 | 12 | username, password, salt, email, type, status, activation_code, header_url, create_time 13 | 14 | 15 | 16 | insert into user () 17 | values (#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime}); 18 | 19 | 20 | 21 | update user set status = #{status} where id = #{id} 22 | 23 | 24 | 25 | update user set header_url = #{headerUrl} where id = #{id} 26 | 27 | 28 | 29 | update user set password = #{password} where id = #{id} 30 | 31 | 32 | 33 | 38 | 39 | 44 | 45 | 50 | 51 | 54 | 55 | 59 | -------------------------------------------------------------------------------- /src/main/resources/sensitive-words.txt: -------------------------------------------------------------------------------- 1 | 赌博 2 | 嫖娼 3 | 吸毒 4 | 开票 -------------------------------------------------------------------------------- /src/main/resources/sql/data.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Dumping data for table `comment` 3 | -- 4 | 5 | # LOCK TABLES `comment` WRITE; 6 | # /*!40000 ALTER TABLE `comment` 7 | # DISABLE KEYS */; 8 | # INSERT INTO `comment` 9 | # VALUES (); 10 | # /*!40000 ALTER TABLE `comment` 11 | # ENABLE KEYS */; 12 | # UNLOCK TABLES; 13 | 14 | -- 15 | -- Dumping data for table `discuss_post` 16 | -- 17 | 18 | LOCK TABLES `discuss_post` WRITE; 19 | /*!40000 ALTER TABLE `discuss_post` 20 | DISABLE KEYS */; 21 | INSERT INTO `discuss_post`(user_id, title, content, type, status, create_time, comment_count, score, node_id) 22 | VALUES (3, '欢迎来到 flow!', '这是 Flow 的第一篇帖子!', 0, 0, now(), 0, 0, 1); 23 | /*!40000 ALTER TABLE `discuss_post` 24 | ENABLE KEYS */; 25 | UNLOCK TABLES; 26 | 27 | -- 28 | -- Dumping data for table `message` 29 | -- 30 | 31 | # LOCK TABLES `message` WRITE; 32 | # /*!40000 ALTER TABLE `message` 33 | # DISABLE KEYS */; 34 | # 35 | # /*!40000 ALTER TABLE `message` 36 | # ENABLE KEYS */; 37 | # UNLOCK TABLES; 38 | 39 | 40 | -- 41 | -- Dumping data for table `user` 42 | -- 43 | 44 | LOCK TABLES `user` WRITE; 45 | /*!40000 ALTER TABLE `user` 46 | DISABLE KEYS */; 47 | INSERT INTO `user`(id, username, password, salt, email, type, status, activation_code, header_url, create_time) 48 | VALUES (1, 'SYSTEM', 'SYSTEM', 'SYSTEM', 'system@sina.com', 0, 1, NULL, 'http://flow-img.waoyun.top/notify.png', 49 | '2021-02-13 02:11:03'), 50 | (2, 'su', '57ee1345597f3bb1d50054c299cca0f7', 'su', 'su@flow.com', 1, 1, NULL, 'http://flow-img.waoyun.top/avatar/0.svg', 51 | '2021-02-13 02:11:03'), 52 | (3, 'admin', 'f6fdffe48c908deb0f4c3bd36c032e72', 'admin', 'admin@flow.com', 2, 1, NULL, 'http://flow-img.waoyun.top/avatar/1.svg', 53 | '2021-02-13 02:11:03'); 54 | /*!40000 ALTER TABLE `user` 55 | ENABLE KEYS */; 56 | UNLOCK TABLES; 57 | 58 | LOCK TABLES `node` WRITE; 59 | /*!40000 ALTER TABLE `node` 60 | DISABLE KEYS */; 61 | insert into node(`name`, `desc`) 62 | values ('热点杂谈', '可发起热点事件、话题进行讨论,严禁讨论政治主题。'), 63 | ('分享创造', '欢迎你在这里发布自己的最新作品!'), 64 | ('奇思妙想', '让你的创意在这里自由流动吧。'), 65 | ('项目相关', '在这里发布关于 Flow 相关的内容。'); 66 | /*!40000 ALTER TABLE `node` 67 | ENABLE KEYS */; 68 | UNLOCK TABLES; -------------------------------------------------------------------------------- /src/main/resources/sql/schema.sql: -------------------------------------------------------------------------------- 1 | create database if not exists flow; 2 | 3 | use flow; 4 | 5 | SET character_set_client = utf8mb4; 6 | 7 | DROP TABLE IF EXISTS `user`; 8 | CREATE TABLE `user` ( 9 | `id` int(11) NOT NULL AUTO_INCREMENT, 10 | `username` varchar(50) NOT NULL, 11 | `password` varchar(50) NOT NULL, 12 | `salt` varchar(50) NOT NULL, 13 | `email` varchar(100) DEFAULT NULL, 14 | `type` int(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;', 15 | `status` int(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;', 16 | `activation_code` varchar(100) DEFAULT NULL, 17 | `header_url` varchar(200) DEFAULT NULL, 18 | `create_time` timestamp NULL DEFAULT NOW(), 19 | PRIMARY KEY (`id`), 20 | KEY `index_username` (`username`(20)), 21 | KEY `index_email` (`email`(20)), 22 | KEY `create_time`(`create_time`) 23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 24 | 25 | DROP TABLE IF EXISTS `message`; 26 | CREATE TABLE `message` ( 27 | `id` int(11) NOT NULL AUTO_INCREMENT, 28 | `from_id` int(11) DEFAULT NULL, 29 | `to_id` int(11) DEFAULT NULL, 30 | `conversation_id` varchar(45) NOT NULL, 31 | `content` text, 32 | `status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;', 33 | `create_time` timestamp NULL DEFAULT NOW(), 34 | PRIMARY KEY (`id`), 35 | KEY `index_from_id` (`from_id`), 36 | KEY `index_to_id` (`to_id`), 37 | KEY `index_conversation_id` (`conversation_id`) 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 39 | 40 | DROP TABLE IF EXISTS `comment`; 41 | CREATE TABLE `comment` ( 42 | `id` int(11) NOT NULL AUTO_INCREMENT, 43 | `user_id` int(11) NOT NULL, 44 | `entity_type` int(11) DEFAULT NULL, 45 | `entity_id` int(11) DEFAULT NULL, 46 | `target_id` int(11) DEFAULT NULL, 47 | `content` text, 48 | `status` int(11) DEFAULT NULL, 49 | `create_time` timestamp NULL DEFAULT NOW(), 50 | PRIMARY KEY (`id`), 51 | KEY `index_user_id` (`user_id`) /*!80000 INVISIBLE */, 52 | KEY `index_entity_id` (`entity_id`) 53 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 54 | 55 | DROP TABLE IF EXISTS `discuss_post`; 56 | create table `discuss_post` ( 57 | `id` int primary key not null auto_increment, 58 | `user_id` varchar(45) not null, 59 | `node_id` int not null default 1, 60 | `title` varchar(255) not null, 61 | `content` text, 62 | `type` int default 0 comment '0-普通; 1-置顶', 63 | `status` int default 0 comment '0-正常; 1-精华; 2-拉黑', 64 | `create_time` timestamp default NOW(), 65 | `comment_count` int default 0, 66 | `score` double default null, 67 | key `index_user_id` (`user_id`), 68 | key `node_id` (`node_id`), 69 | key `type_and_create_time`(`type`, `create_time`), 70 | KEY `create_time`(`create_time`) 71 | )engine = InnoDB default charset = utf8; 72 | 73 | DROP TABLE IF EXISTS `login_ticket`; 74 | CREATE TABLE `login_ticket` ( 75 | `id` int(11) NOT NULL AUTO_INCREMENT, 76 | `user_id` int(11) NOT NULL, 77 | `ticket` varchar(45) NOT NULL, 78 | `status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;', 79 | `expired` timestamp NOT NULL, 80 | PRIMARY KEY (`id`), 81 | KEY `index_ticket` (`ticket`(20)) 82 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 83 | 84 | DROP TABLE IF EXISTS `node`; 85 | CREATE TABLE `node` ( 86 | `id` int NOT NULL PRIMARY KEY AUTO_INCREMENT, 87 | `name` varchar(32) not null unique comment '节点名', 88 | `desc` varchar(255) default null comment '节点描述', 89 | key idx_name(`name`) 90 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '节点'; -------------------------------------------------------------------------------- /src/main/resources/static/css/data.css: -------------------------------------------------------------------------------- 1 | .data-container > div > div { 2 | color: var(--color-gray); 3 | } 4 | 5 | .data-container > div > .data { 6 | font-size: 2em; 7 | font-weight: 500; 8 | color: #000; 9 | } 10 | 11 | .chart-box { 12 | width: 100%; 13 | height: 300px; 14 | } 15 | 16 | @media screen and (min-width: 576px) { 17 | .data-container > div { 18 | margin: 0 1rem; 19 | } 20 | .data-container > div:first-child { 21 | margin-left: 0; 22 | } 23 | .data-container > div { 24 | margin-right: 0; 25 | } 26 | } 27 | 28 | @media screen and (max-width: 576px) { 29 | .data-container > div { 30 | margin: 0.5rem 0; 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /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 | } 22 | 23 | .op-btn { 24 | border:none; 25 | } 26 | 27 | .op-btn[disabled] { 28 | color: var(--color-fade); 29 | } -------------------------------------------------------------------------------- /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/static/css/login.css: -------------------------------------------------------------------------------- 1 | .main .container { 2 | width: 720px; 3 | } 4 | -------------------------------------------------------------------------------- /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/resources/static/img/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wao3/flow/0c2a15cd35d03ce1367c5a364bb7ef792bc09004/src/main/resources/static/img/captcha.png -------------------------------------------------------------------------------- /src/main/resources/static/img/comments.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wao3/flow/0c2a15cd35d03ce1367c5a364bb7ef792bc09004/src/main/resources/static/img/flow.png -------------------------------------------------------------------------------- /src/main/resources/static/img/like.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/report.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/trend.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/js/data.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(() => { 4 | const charts = { 5 | dau: echarts.init(document.getElementById('dau-chart'),'macarons'), 6 | uv: echarts.init(document.getElementById('uv-chart'),'macarons'), 7 | }; 8 | 9 | initChart('uv'); 10 | initChart('dau'); 11 | 12 | $(`#uv-submit`).click(count('uv')); 13 | $(`#dau-submit`).click(count('dau')); 14 | 15 | 16 | initData('uv'); 17 | setTimeout(() => initData('dau'), 100); 18 | 19 | function count(type) { 20 | type = type.toString().toLowerCase(); 21 | if (type !== 'uv' && type !== 'dau') { 22 | throw '无法计算' + type; 23 | } 24 | const url = CONTEXT_PATH + "/data/" + type; 25 | return function () { 26 | const start = $(`#${type}-start`).val(); 27 | const end = $(`#${type}-end`).val(); 28 | charts[type].showLoading({ 29 | text: 'loading', 30 | color: '#4cbbff', 31 | textColor: '#4cbbff', 32 | maskColor: 'rgba(255, 255, 255, 0.1)', 33 | }); 34 | $.post(url, { start, end }, data => { 35 | data = JSON.parse(data); 36 | if (data.code === 0) { 37 | $(`#${type}-total`).text(data.count); 38 | fillChart(type, data.list); 39 | charts[type].hideLoading(); 40 | } else { 41 | alert(data.msg); 42 | } 43 | }) 44 | return false; 45 | } 46 | } 47 | 48 | function initChart(type) { 49 | type = type.toString().toLowerCase(); 50 | if (type !== 'uv' && type !== 'dau') { 51 | throw '无法计算' + type; 52 | } 53 | let typeName = type === 'uv' ? '访客' : '活跃用户'; 54 | charts[type].hideLoading(); 55 | charts[type].setOption({ 56 | title: { 57 | text: typeName, 58 | }, 59 | tooltip: { 60 | trigger: 'axis', 61 | }, 62 | toolbox: { 63 | show: true, 64 | feature: { 65 | dataView: { 66 | readOnly: false 67 | }, //数据视图 68 | magicType: { 69 | type: ['line', 'bar'] 70 | }, //切换为折线图,切换为柱状图 71 | } 72 | }, 73 | legend: { 74 | data:[typeName] 75 | }, 76 | xAxis: { 77 | data: [] 78 | }, 79 | yAxis: {}, 80 | series: [{ 81 | name: typeName, 82 | type: 'bar', 83 | data: [], 84 | smooth: true, 85 | }], 86 | grid: { 87 | top: '15%', 88 | left: '3%', 89 | right: '4%', 90 | bottom: '3%', 91 | containLabel: true 92 | }, 93 | }); 94 | } 95 | 96 | function fillChart(type, list) { 97 | type = type.toString().toLowerCase(); 98 | if (type !== 'uv' && type !== 'dau') { 99 | throw '无法计算' + type; 100 | } 101 | let typeName = type === 'uv' ? '访客' : '活跃用户'; 102 | let axis = []; 103 | let datas = []; 104 | 105 | list.forEach(i => { 106 | axis.push(i.date); 107 | datas.push(i.data); 108 | }) 109 | 110 | charts[type].setOption({ 111 | xAxis: { 112 | data: axis 113 | }, 114 | series: [{ 115 | name: typeName, 116 | data: datas 117 | }] 118 | }) 119 | } 120 | 121 | function formatDate(date) { 122 | let d = new Date(date), 123 | month = '' + (d.getMonth() + 1), 124 | day = '' + d.getDate(), 125 | year = d.getFullYear(); 126 | 127 | if (month.length < 2) month = '0' + month; 128 | if (day.length < 2) day = '0' + day; 129 | 130 | return [year, month, day].join('-'); 131 | } 132 | 133 | function initData(type) { 134 | let today = new Date(); 135 | let sevenDayAgo = new Date(today.valueOf() - 1000 * 60 * 60 * 24 * 7); // 7天前 136 | 137 | $(`#${type}-start`).val(formatDate(sevenDayAgo)); 138 | $(`#${type}-end`).val(formatDate(today)); 139 | 140 | $(`#${type}-submit`).click(); 141 | } 142 | }); -------------------------------------------------------------------------------- /src/main/resources/static/js/discuss.js: -------------------------------------------------------------------------------- 1 | function like(btn, entityType, entityId, entityUserId, postId) { 2 | $.post(CONTEXT_PATH + "/like", { entityType, entityId, entityUserId, postId }, function (data) { 3 | data = JSON.parse(data); 4 | if (data.code === 0) { 5 | $(btn).children("i").text(data.likeCount); 6 | $(btn).children("b").text(data.likeStatus === 1 ? "已赞" : "点赞"); 7 | } else { 8 | alert(data.msg) 9 | } 10 | }) 11 | } 12 | 13 | $(function (){ 14 | 15 | $("#topBtn").click(postCtl("top")); 16 | $("#wonderfulBtn").click(postCtl("wonderful")); 17 | $("#deleteBtn").click(postCtl("delete")); 18 | $("#cancelTopBtn").click(postCtl("cancelTop")); 19 | $("#cancelWonderfulBtn").click(postCtl("cancelWonderful")); 20 | 21 | // 对帖子进行操作 22 | function postCtl(operation) { 23 | let url = CONTEXT_PATH + "/discuss/" + operation; 24 | let postId = $("#postId").data("id"); 25 | return function () { 26 | $.post(url, {id: postId}, (data) => { 27 | data = JSON.parse(data); 28 | if (data.code === 0) { 29 | $("#" + operation + "Btn").attr("disabled", "disabled"); 30 | if (operation === "delete") { 31 | location.href = CONTEXT_PATH + "/index"; 32 | } 33 | } else { 34 | alert(data.msg); 35 | } 36 | }) 37 | }; 38 | } 39 | }); -------------------------------------------------------------------------------- /src/main/resources/static/js/global.js: -------------------------------------------------------------------------------- 1 | const CONTEXT_PATH = ''; 2 | 3 | // 发送ajax请求之前,将csrf令牌设置到请求消息头 4 | let tmp = $("#csrf"); 5 | let token = tmp.data("token"); 6 | let header = tmp.data("header"); 7 | 8 | $(document).ajaxSend(function (e, xhr, options) { 9 | xhr.setRequestHeader(header, token); 10 | }); 11 | 12 | window.alert = function(message) { 13 | if(!$(".alert-box").length) { 14 | $("body").append( 15 | '' 33 | ); 34 | } 35 | 36 | var h = $(".alert-box").height(); 37 | var y = h / 2 - 100; 38 | if(h > 600) y -= 100; 39 | $(".alert-box .modal-dialog").css("margin", (y < 0 ? 0 : y) + "px auto"); 40 | 41 | $(".alert-box .modal-body p").text(message); 42 | $(".alert-box").modal("show"); 43 | } 44 | 45 | function go(url) { 46 | window.location.href = CONTEXT_PATH + url; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/resources/static/js/index.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#publishBtn").click(publish); 3 | }); 4 | 5 | function publish() { 6 | $("#publishModal").modal("hide"); 7 | 8 | // 获取标题内容 9 | let title = $("#recipient-name").val(); 10 | let content = $("#message-text").val(); 11 | let nodeId = $("#select-node").val(); 12 | 13 | // 异步发送 14 | $.post( 15 | CONTEXT_PATH + "/discuss/add", 16 | { title, content, nodeId }, 17 | function (data) { 18 | data = JSON.parse(data); 19 | // 在提示框中显示返回消息 20 | let hintModal = $("#hintModal"); 21 | console.log(data); 22 | $("#hintBody").text(data.msg); 23 | hintModal.modal("show"); 24 | setTimeout(function(){ 25 | $("#hintModal").modal("hide"); 26 | if (data.code === 0) { 27 | window.location.reload(); 28 | } 29 | }, 2000); 30 | } 31 | ) 32 | } -------------------------------------------------------------------------------- /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 | let toName = $("#recipient-name").val(); 10 | let content = $("#message-text").val(); 11 | $.post(CONTEXT_PATH + "/letter/send", {toName, content}, function (data) { 12 | data = JSON.parse(data); 13 | $("#hintBody").text(data.msg); 14 | 15 | $("#hintModal").modal("show"); 16 | setTimeout(function(){ 17 | $("#hintModal").modal("hide"); 18 | location.reload(); 19 | }, 2000); 20 | }) 21 | 22 | 23 | } 24 | 25 | function delete_msg() { 26 | // TODO 删除数据 27 | $(this).parents(".media").remove(); 28 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/profile.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $(".follow-btn").click(follow); 3 | }); 4 | 5 | function follow() { 6 | let btn = this; 7 | if($(btn).hasClass("btn-dark")) { 8 | // 关注TA 9 | $.post(CONTEXT_PATH + "/follow", { entityType: 3, entityId: $(btn).next().val() }, data => { 10 | data = JSON.parse(data); 11 | if (data.code === 0) { 12 | location.reload(); 13 | } else { 14 | alert(data.msg); 15 | } 16 | }) 17 | } else { 18 | // 取消关注 19 | $.post(CONTEXT_PATH + "/unfollow", { entityType: 3, entityId: $(btn).next().val() }, data => { 20 | data = JSON.parse(data); 21 | if (data.code === 0) { 22 | location.reload(); 23 | } else { 24 | alert(data.msg); 25 | } 26 | }) 27 | } 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/static/js/setting.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | let uploadForm = $("#uploadForm"); 3 | uploadForm.submit(upload); 4 | function upload() { 5 | $.ajax({ 6 | url: "http://upload-z2.qiniup.com", 7 | method: "post", 8 | processData: false, 9 | contentType: false, 10 | data: new FormData(uploadForm[0]), 11 | success: (data) => { 12 | if (data != null && data.code === 0) { 13 | // 更新头像访问路径 14 | let fileName = $("input[name='key']").val(); 15 | updateHeaderUrl(fileName); 16 | } else { 17 | alert("上传失败") 18 | } 19 | } 20 | }) 21 | return false; 22 | } 23 | function updateHeaderUrl(fileName) { 24 | $.post(CONTEXT_PATH + "/user/header/url", { fileName }, (data) => { 25 | data = JSON.parse(data); 26 | if (data.code === 0) { 27 | window.location.reload(); 28 | } else { 29 | alert(data.msg); 30 | } 31 | }) 32 | } 33 | }); -------------------------------------------------------------------------------- /src/main/resources/templates/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-404 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-500 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/activation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flow-激活账号 7 | 8 | 9 |
10 |

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

13 |

14 | 您正在注册Flow, 这是一封激活邮件, 请点击 15 | http://www.nowcoder.com/activation/abcdefg123456.html, 16 | 激活您的牛客账号! 17 |

18 |
19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/forget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flow-忘记密码 7 | 8 | 9 |
10 |

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

13 |

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

17 |
18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/admin/data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-数据统计 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 |
22 |
23 |
24 |
今日注册
25 |
123
26 |
27 |
28 |
今日发帖
29 |
123
30 |
31 |
32 |
今日访客
33 |
123
34 |
35 |
36 |
今日活跃用户
37 |
123
38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
网站 UV
47 |
48 |
49 | 50 | 51 | 52 |
53 |
该范围累计访客:
54 |
55 |
56 |
57 | 58 | 59 |
60 |
61 |
活跃用户
62 |
63 |
64 | 65 | 66 | 67 |
68 |
该范围累计活跃用户:
69 |
70 |
71 |
72 |
73 |
74 | 75 | 76 |
77 | 78 |
79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/followee.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-关注 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 36 | 37 | 38 |
    39 |
  • 40 | 41 | 用户头像 42 | 43 |
    44 |
    45 | 落基山脉下的闲人 46 | 47 | 关注于 2019-04-28 14:13:25 48 | 49 |
    50 |
    51 | 55 | 56 |
    57 |
    58 |
  • 59 |
60 | 61 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/follower.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-关注 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 36 | 37 | 38 |
    39 |
  • 40 | 41 | 用户头像 42 | 43 |
    44 |
    45 | 落基山脉下的闲人 46 | 47 | 关注于 2019-04-28 14:13:25 48 | 49 |
    50 |
    51 | 55 | 56 |
    57 |
    58 |
  • 59 |
60 | 61 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/forget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-忘记密码 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 | 该邮箱已被注册! 27 |
28 |
29 |
30 |
31 | 32 |
33 | 34 |
35 | 验证码不正确! 36 |
37 |
38 |
39 | 获取验证码 40 |
41 |
42 |
43 | 44 |
45 | 46 |
47 | 密码长度不能小于8位! 48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/letter-detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-私信详情 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
来自 落基山脉下的闲人 的私信
24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
    33 |
  • 34 | 35 | 用户头像 36 | 37 | 46 |
  • 47 |
48 | 49 | 62 |
63 |
64 | 65 | 93 | 94 | 106 | 107 | 108 |
109 |
110 | 111 | 112 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/letter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-私信列表 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | 23 | 41 | 42 |
43 | 44 | 45 | 70 | 71 | 72 |
73 |
74 | 75 | 103 | 104 | 116 | 117 |
118 |
119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-登录 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
登录
22 |
23 |
24 | 25 |
26 | 29 |
30 | 该账号不存在! 31 |
32 |
33 |
34 |
35 | 36 |
37 | 40 |
41 | 密码长度不能小于8位! 42 |
43 |
44 |
45 |
46 | 47 |
48 | 50 |
51 | 验证码不正确! 52 |
53 |
54 |
55 | 56 | 刷新验证码 57 |
58 |
59 |
60 |
61 |
62 | 64 | 65 | 忘记密码? 66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/my-post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-个人主页 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 32 | 返回个人主页> 33 |
34 | 35 |
36 |
发布的帖子(93)
37 |
    38 |
  • 39 | 42 |
    43 | 这里是帖子内容 44 |
    45 |
    46 | 赞 11 发布于 2019-04-15 10:10:10 47 |
    48 |
  • 49 |
50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/my-reply.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-个人主页 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 32 | 返回个人主页> 33 |
34 | 35 |
36 |
回复的帖子(379)
37 | 50 | 51 | 64 |
65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/notice-detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-通知详情 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
系统通知
24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 |
    32 |
  • 33 | 系统图标 34 | 59 |
  • 60 |
61 | 62 | 63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 | 71 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/notice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-通知 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | 23 | 41 |
42 | 43 | 44 | 105 |
106 |
107 | 108 | 109 |
110 |
111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/operate-result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-操作结果 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |

您的账号已经激活成功,可以正常使用了!

22 |
23 |

24 | 系统会在 8 秒后自动跳转, 25 | 您也可以点此 链接, 手动跳转! 26 |

27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-个人主页 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 | 33 |
34 | 35 |
36 | 用户头像 37 |
38 |
39 | nowcoder 40 | 44 | 45 |
46 |
47 | 注册于 2015-06-12 15:20:12 48 |
49 |
50 | 关注了 5 51 | 关注者 123 52 | 获得了 87 个赞 53 |
54 |
55 |
56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-注册 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |

注 册

22 |
23 |
24 | 25 |
26 | 29 |
30 | 该账号已存在! 31 |
32 |
33 |
34 |
35 | 36 |
37 | 40 |
41 | 密码长度不能小于8位! 42 |
43 |
44 |
45 |
46 | 47 |
48 | 51 |
52 | 两次输入的密码不一致! 53 |
54 |
55 |
56 |
57 | 58 |
59 | 62 |
63 | 该邮箱已注册! 64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flow-搜索结果 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 26 | 27 |
    28 |
  • 29 | 用户头像 30 |
    31 |
    32 | 备战春招,面试刷题跟他复习,一个月全搞定! 33 |
    34 |
    35 | 金三银四的金三已经到了,你还沉浸在过年的喜悦中吗? 如果是,那我要让你清醒一下了:目前大部分公司已经开启了内推,正式网申也将在3月份陆续开始,金三银四,春招的求职黄金时期已经来啦!!! 再不准备,作为19应届生的你可能就找不到工作了。。。作为20届实习生的你可能就找不到实习了。。。 现阶段时间紧,任务重,能做到短时间内快速提升的也就只有算法了, 那么算法要怎么复习?重点在哪里?常见笔试面试算法题型和解题思路以及最优代码是怎样的? 跟左程云老师学算法,不仅能解决以上所有问题,还能在短时间内得到最大程度的提升!!! 36 |
    37 |
    38 | 寒江雪 39 | 发布于 2019-04-15 15:32:18 40 |
      41 |
    • 11
    • 42 |
    • |
    • 43 |
    • 回复 11
    • 44 |
    45 |
    46 |
    47 |
  • 48 |
49 | 50 | 51 |
52 |
53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/setting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow-账号设置 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
上传头像
24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 | 文件错误 34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 | 49 |
50 |
修改密码
51 |
52 |
53 | 54 |
55 | 56 |
57 | 密码长度不能小于8位! 58 |
59 |
60 |
61 |
62 | 63 |
64 | 65 |
66 | 密码长度不能小于8位! 67 |
68 |
69 |
70 |
71 | 72 |
73 | 74 |
75 | 两次输入的密码不一致! 76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 |
92 |
93 | 94 | 95 | 96 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/CommunityApplicationTests.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class CommunityApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/dao/DiscussPostMapperTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import java.util.List; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | @SpringBootTest 14 | class DiscussPostMapperTest { 15 | 16 | @Resource 17 | DiscussPostMapper discussPostMapper; 18 | 19 | @Test 20 | void selectDiscussPosts() { 21 | List discussPosts1 = discussPostMapper.selectDiscussPosts(null, 0, 10, 0); 22 | discussPosts1.forEach(System.out::println); 23 | 24 | System.out.println("==============="); 25 | List discussPosts2 = discussPostMapper.selectDiscussPosts(149, 0, 10, 0); 26 | discussPosts2.forEach(System.out::println); 27 | } 28 | 29 | @Test 30 | void selectDiscussPostRows() { 31 | int allRows = discussPostMapper.selectDiscussPostRows(null); 32 | System.out.println("all rows: " + allRows); 33 | 34 | int rows149 = discussPostMapper.selectDiscussPostRows(149); 35 | System.out.println("id149 user's rows: " + rows149); 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/dao/LoginTicketMapperTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.LoginTicket; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import java.util.Date; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | @SpringBootTest 14 | class LoginTicketMapperTest { 15 | 16 | @Resource 17 | LoginTicketMapper loginTicketMapper; 18 | 19 | @Test 20 | void insertLoginTicket() { 21 | LoginTicket loginTicket = new LoginTicket(); 22 | loginTicket.setUserId(101) 23 | .setTicket("abc") 24 | .setStatus(0) 25 | .setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10)); 26 | 27 | int i = loginTicketMapper.insertLoginTicket(loginTicket); 28 | System.out.println(loginTicket); 29 | assertNotNull(loginTicket.getId()); 30 | } 31 | 32 | @Test 33 | void selectAndUpdate() { 34 | LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc"); 35 | assertNotNull(loginTicket); 36 | System.out.println(loginTicket); 37 | 38 | int i = loginTicketMapper.updateStatus("abc", 1); 39 | assertEquals(i, 1); 40 | LoginTicket loginTicket2 = loginTicketMapper.selectByTicket("abc"); 41 | System.out.println(loginTicket2); 42 | assertEquals(loginTicket2.getStatus(), 1); 43 | } 44 | } -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/dao/MessageMapperTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.Message; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import java.util.List; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | @SpringBootTest 14 | class MessageMapperTest { 15 | 16 | @Resource 17 | private MessageMapper messageMapper; 18 | 19 | @Test 20 | void selectConversations() { 21 | List messages = messageMapper.selectConversations(111, 0, 20); 22 | messages.forEach(System.out::println); 23 | } 24 | 25 | @Test 26 | void selectConversationCount() { 27 | int count = messageMapper.selectConversationCount(111); 28 | System.out.println("conversation count: " + count); 29 | } 30 | 31 | @Test 32 | void selectLetters() { 33 | List messages = messageMapper.selectLetters("111_112", 0, 10); 34 | messages.forEach(System.out::println); 35 | } 36 | 37 | @Test 38 | void selectLetterCount() { 39 | int count = messageMapper.selectLetterCount("111_112"); 40 | System.out.println("111_112 Letter count: " + count); 41 | } 42 | 43 | @Test 44 | void selectLetterUnreadCount() { 45 | int count = messageMapper.selectLetterUnreadCount(131, "111_131"); 46 | System.out.println("131's 111_131 letter unread count: " + count); 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/dao/UserMapperTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.dao; 2 | 3 | import me.wangao.community.entity.User; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | import javax.annotation.Resource; 9 | 10 | import java.util.Date; 11 | 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | @SpringBootTest 15 | class UserMapperTest { 16 | 17 | @Resource 18 | UserMapper userMapper; 19 | 20 | @Test 21 | void select() { 22 | User user1 = userMapper.selectById(101); 23 | System.out.println(user1); 24 | 25 | User user2 = userMapper.selectByName("liubei"); 26 | System.out.println(user2); 27 | 28 | User user3 = userMapper.selectByEmail("nowcoder101@sina.com"); 29 | System.out.println(user3); 30 | } 31 | 32 | @Test 33 | void insert() { 34 | User user = new User(); 35 | user.setUsername("test") 36 | .setPassword("123456") 37 | .setSalt("abc") 38 | .setEmail("test@qq.com") 39 | .setHeaderUrl("https://cdn.v2ex.com/avatar/8a8c/cca8/482014_large.png") 40 | .setCreateTime(new Date()); 41 | 42 | int rows = userMapper.insertUser(user); 43 | System.out.println(rows); 44 | System.out.println(user.getId()); 45 | } 46 | 47 | @Test 48 | void update() { 49 | int rows1 = userMapper.updateStatus(150, 0); 50 | System.out.println(rows1); 51 | 52 | int rows2 = userMapper.updateHeader(150, "https://cdn.v2ex.com/avatar/8a8c/cca8/482013_large.png"); 53 | System.out.println(rows2); 54 | 55 | int rows3 = userMapper.updatePassword(150,"654321"); 56 | System.out.println(rows2); 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/other/MyTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.other; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | 8 | import javax.annotation.Resource; 9 | 10 | @SpringBootTest 11 | public class MyTest { 12 | 13 | @Resource 14 | private RedisTemplate redisTemplate; 15 | 16 | @Test 17 | void testSet() { 18 | redisTemplate.opsForSet().add("test", "a"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/service/DiscussPostServiceTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.service; 2 | 3 | import me.wangao.community.entity.DiscussPost; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import java.util.Date; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | @SpringBootTest 14 | class DiscussPostServiceTest { 15 | 16 | @Resource 17 | private DiscussPostService discussPostService; 18 | 19 | @Test 20 | public void initDataForTest() { 21 | for (int i = 0; i < 300000; ++i) { 22 | DiscussPost post = new DiscussPost() 23 | .setUserId(111) 24 | .setTitle("互联网求职暖春计划") 25 | .setContent("互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划互联网求职暖春计划") 26 | .setCreateTime(new Date()) 27 | .setScore(Math.random() * 2000); 28 | 29 | discussPostService.addDiscussPost(post); 30 | } 31 | } 32 | 33 | @Test 34 | void findDiscussPosts() { 35 | 36 | System.out.println(discussPostService.findDiscussPosts(null, 0, 10, 1)); 37 | System.out.println(discussPostService.findDiscussPosts(null, 0, 10, 1)); 38 | System.out.println(discussPostService.findDiscussPosts(null, 0, 10, 1)); 39 | System.out.println(discussPostService.findDiscussPosts(null, 0, 10, 0)); 40 | } 41 | 42 | @Test 43 | void findDiscussPostRows() { 44 | } 45 | } -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/util/MailClientTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | @SpringBootTest 10 | class MailClientTest { 11 | 12 | @Autowired 13 | private MailClient mailClient; 14 | 15 | @Test 16 | void sendMail() { 17 | mailClient.sendMail("1012717693@qq.com", "test", "test to unsubscribe"); 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/me/wangao/community/util/SensitiveFilterTest.java: -------------------------------------------------------------------------------- 1 | package me.wangao.community.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | @SpringBootTest 11 | class SensitiveFilterTest { 12 | 13 | @Resource 14 | SensitiveFilter sensitiveFilter; 15 | 16 | @Test 17 | void filter() { 18 | String text = "这里不能赌博,不能吸毒,不能嫖娼,不能开票"; 19 | String filterText = sensitiveFilter.filter(text); 20 | System.out.println(filterText); 21 | 22 | String text2 = "这里不能❤赌❤博❤,不能❤吸❤毒❤,不能❤嫖❤娼❤,不能❤开❤票❤"; 23 | String filterText2 = sensitiveFilter.filter(text2); 24 | System.out.println(filterText2); 25 | } 26 | } --------------------------------------------------------------------------------