├── src ├── main │ ├── resources │ │ ├── static │ │ │ ├── css │ │ │ │ ├── login.css │ │ │ │ ├── letter.css │ │ │ │ ├── discuss-detail.css │ │ │ │ └── global.css │ │ │ ├── img │ │ │ │ ├── 404.png │ │ │ │ ├── error.png │ │ │ │ └── captcha.png │ │ │ ├── js │ │ │ │ ├── register.js │ │ │ │ ├── letter.js │ │ │ │ ├── global.js │ │ │ │ ├── index.js │ │ │ │ ├── profile.js │ │ │ │ ├── setting.js │ │ │ │ └── discuss.js │ │ │ └── html │ │ │ │ ├── student.html │ │ │ │ └── ajax-demo.html │ │ ├── README.md │ │ ├── application.properties │ │ ├── templates │ │ │ ├── demo │ │ │ │ └── view.html │ │ │ ├── mail │ │ │ │ ├── demo.html │ │ │ │ ├── forget.html │ │ │ │ └── activation.html │ │ │ └── site │ │ │ │ └── operate-result.html │ │ ├── mapper │ │ │ ├── user-mapper.xml │ │ │ ├── discusspost-mapper.xml │ │ │ └── message-mapper.xml │ │ └── logback-spring.xml │ └── java │ │ └── tech │ │ └── turl │ │ └── community │ │ ├── dao │ │ ├── AlphaDao.java │ │ ├── AlphaDaoHibernateImpl.java │ │ ├── AlphaDaoMyBatisImpl.java │ │ ├── elasticsearch │ │ │ └── DiscussPostRepository.java │ │ ├── UserMapper.java │ │ ├── LoginTicketMapper.java │ │ ├── DiscussPostMapper.java │ │ ├── CommentMapper.java │ │ └── MessageMapper.java │ │ ├── config │ │ ├── ThreadPoolConfig.java │ │ ├── AlphaConfig.java │ │ ├── WkConfig.java │ │ ├── RedisConfig.java │ │ ├── KaptchaConfig.java │ │ ├── WebMvcConfig.java │ │ ├── QuartzConfig.java │ │ └── SecurityConfig.java │ │ ├── annotation │ │ └── LoginRequired.java │ │ ├── quartz │ │ ├── AlphaJob.java │ │ └── PostScoreRefreshJob.java │ │ ├── util │ │ ├── HostHolder.java │ │ ├── CookieUtil.java │ │ ├── CommunityConstant.java │ │ ├── MailClient.java │ │ ├── CommunityUtil.java │ │ ├── RedisCellRateLimiter.java │ │ ├── RedisKeyUtil.java │ │ └── SensitiveFilter.java │ │ ├── CommunityApplication.java │ │ ├── event │ │ └── EventProducer.java │ │ ├── entity │ │ ├── Demo.java │ │ ├── LoginTicket.java │ │ ├── Page.java │ │ ├── Event.java │ │ ├── Message.java │ │ ├── Comment.java │ │ ├── User.java │ │ └── DiscussPost.java │ │ ├── actuator │ │ └── DatabaseEndpoint.java │ │ ├── controller │ │ ├── interceptor │ │ │ ├── DataInterceptor.java │ │ │ ├── MessageInterceptor.java │ │ │ ├── LoginRequiredInterceptor.java │ │ │ ├── AlphaInterceptor.java │ │ │ └── LoginTicketInterceptor.java │ │ ├── advice │ │ │ └── ExceptionAdvice.java │ │ ├── DataContoller.java │ │ ├── SearchController.java │ │ ├── HomeController.java │ │ ├── LikeController.java │ │ ├── ShareController.java │ │ ├── CommentController.java │ │ └── FollowController.java │ │ ├── aspect │ │ ├── AlphaAspect.java │ │ └── ServiceLogAspect.java │ │ └── service │ │ ├── CommentService.java │ │ ├── LikeService.java │ │ ├── MessageService.java │ │ ├── DataService.java │ │ ├── ElasticsearchService.java │ │ └── AlphaService.java └── test │ └── java │ └── tech │ └── turl │ └── community │ ├── UUIDTests.java │ ├── WkTests.java │ ├── TimeTest.java │ ├── LoggerTest.java │ ├── RedisCellTests.java │ ├── QuartzTests.java │ ├── TransactionTests.java │ ├── wkhtmltopdf │ ├── HtmlToPdfInterceptor.java │ └── HtmlToPdf.java │ ├── TestSensitiveFilter.java │ ├── MailTests.java │ ├── TestLoginTicketMapper.java │ ├── TestMessageMapper.java │ ├── KafkaTests.java │ ├── CaffeineTests.java │ ├── BlockingQueueTests.java │ ├── TestMapper.java │ ├── CommunityApplicationTests.java │ └── ThreadPoolTests.java ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── .gitignore ├── LICENSE ├── sql └── init_schema.sql ├── README.md └── pom.xml /src/main/resources/static/css/login.css: -------------------------------------------------------------------------------- 1 | .main .container { 2 | width: 720px; 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/README.md: -------------------------------------------------------------------------------- 1 | sensitive-words.txt为本项目收集的敏感词库,仅限用于合法的、积极向上的敏感词过滤使用,严禁用于从事违反法律法规、危害国家、危害人民、不道德的活动!!! -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengguohuang/community/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/static/img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengguohuang/community/HEAD/src/main/resources/static/img/404.png -------------------------------------------------------------------------------- /src/main/resources/static/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengguohuang/community/HEAD/src/main/resources/static/img/error.png -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengguohuang/community/HEAD/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/main/resources/static/img/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengguohuang/community/HEAD/src/main/resources/static/img/captcha.png -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/AlphaDao.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | public interface AlphaDao { 4 | String select(); 5 | } 6 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/main/resources/static/css/letter.css: -------------------------------------------------------------------------------- 1 | .main .nav .badge { 2 | position: absolute; 3 | top: -3px; 4 | left: 68px; 5 | } 6 | 7 | .main .media .badge { 8 | position: absolute; 9 | top: 12px; 10 | left: -3px; 11 | } 12 | 13 | .toast { 14 | max-width: 100%; 15 | width: 80%; 16 | } -------------------------------------------------------------------------------- /src/main/resources/templates/demo/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Teacher 6 | 7 | 8 |

9 |

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

欢迎,

9 | 10 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/AlphaDaoHibernateImpl.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.springframework.stereotype.Repository; 4 | 5 | @Repository("alphaDaoHibernate") 6 | public class AlphaDaoHibernateImpl implements AlphaDao { 7 | @Override 8 | public String select() { 9 | return "Hibernate"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/static/css/discuss-detail.css: -------------------------------------------------------------------------------- 1 | .content { 2 | font-size: 16px; 3 | line-height: 2em; 4 | } 5 | 6 | .replyform textarea { 7 | width: 100%; 8 | height: 200px; 9 | } 10 | 11 | .floor { 12 | background: #dcdadc; 13 | padding: 4px 12px; 14 | border-radius: 3px; 15 | font-size: 14px; 16 | } 17 | 18 | .input-size { 19 | width: 100%; 20 | height: 35px; 21 | } -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/AlphaDaoMyBatisImpl.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.springframework.context.annotation.Primary; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | @Primary 8 | public class AlphaDaoMyBatisImpl implements AlphaDao { 9 | @Override 10 | public String select() { 11 | return "MyBatis"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/UUIDTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * @author zhengguohuang 9 | * @date 2021/04/22 10 | */ 11 | public class UUIDTests { 12 | @Test 13 | public void testUUID() { 14 | for (int i = 0; i < 100; i++) { 15 | System.out.println(UUID.randomUUID()); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/static/js/register.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("form").submit(check_data); 3 | $("input").focus(clear_error); 4 | }); 5 | 6 | function check_data() { 7 | var pwd1 = $("#password").val(); 8 | var pwd2 = $("#confirm-password").val(); 9 | if(pwd1 != pwd2) { 10 | $("#confirm-password").addClass("is-invalid"); 11 | return false; 12 | } 13 | return true; 14 | } 15 | 16 | function clear_error() { 17 | $(this).removeClass("is-invalid"); 18 | } -------------------------------------------------------------------------------- /src/main/resources/templates/mail/forget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 牛客网-忘记密码 7 | 8 | 9 |
10 |

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

13 |

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

17 |
18 | 19 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/ThreadPoolConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.EnableAsync; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | /** 8 | * @author zhengguohuang 9 | * @date 2021/03/29 10 | */ 11 | @Configuration 12 | @EnableScheduling 13 | @EnableAsync 14 | public class ThreadPoolConfig {} 15 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/annotation/LoginRequired.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author zhengguohuang 10 | * @date 2021/03/16 11 | */ 12 | @Target(ElementType.METHOD) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface LoginRequired { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/AlphaConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import java.text.SimpleDateFormat; 7 | 8 | @Configuration 9 | public class AlphaConfig { 10 | // 方法名为bean的名字,方法返回对象为bean的内容 11 | @Bean 12 | public SimpleDateFormat simpleDateFormat(){ 13 | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/elasticsearch/DiscussPostRepository.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao.elasticsearch; 2 | 3 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 4 | import org.springframework.stereotype.Repository; 5 | import tech.turl.community.entity.DiscussPost; 6 | 7 | /** 8 | * @author zhengguohuang 9 | * @date 2021/03/25 10 | */ 11 | @Repository 12 | public interface DiscussPostRepository extends ElasticsearchRepository { 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/WkTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * @author zhengguohuang 7 | * @date 2021/03/29 8 | */ 9 | public class WkTests { 10 | public static void main(String[] args) throws IOException { 11 | String cmd = 12 | "e:/wkhtmltopdf/bin/wkhtmltoimage --quality 75 https://baidu.com d:/work/data/wk-images/1.png"; 13 | Runtime.getRuntime().exec(cmd); 14 | System.out.println("ok"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/quartz/AlphaJob.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.quartz; 2 | 3 | import org.quartz.Job; 4 | import org.quartz.JobExecutionContext; 5 | import org.quartz.JobExecutionException; 6 | 7 | /** 8 | * @author zhengguohuang 9 | * @date 2021/03/29 10 | */ 11 | public class AlphaJob implements Job { 12 | @Override 13 | public void execute(JobExecutionContext context) throws JobExecutionException { 14 | System.out.println(Thread.currentThread().getName() + ": execute a quartz job."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/activation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 牛客网-激活账号 7 | 8 | 9 |
10 |

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

13 |

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

18 |
19 | 20 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/HostHolder.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | import org.springframework.stereotype.Component; 4 | import tech.turl.community.entity.User; 5 | 6 | /** 7 | * 持有用户信息,用于代替session对象 8 | * 9 | * @author zhengguohuang 10 | * @date 2021/3/15 11 | */ 12 | @Component 13 | public class HostHolder { 14 | private ThreadLocal users = new ThreadLocal<>(); 15 | 16 | public void setUser(User user) { 17 | users.set(user); 18 | } 19 | 20 | public User getUser() { 21 | return users.get(); 22 | } 23 | 24 | public void clear() { 25 | users.remove(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/TimeTest.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Calendar; 5 | import java.util.Date; 6 | 7 | /** 8 | * @author zhengguohuang 9 | * @date 2021/03/25 10 | */ 11 | public class TimeTest { 12 | public static void main(String[] args) { 13 | Date date = new Date();//获取当前时间     14 | Calendar calendar = Calendar.getInstance(); 15 | calendar.setTime(date); 16 | calendar.add(Calendar.MINUTE, -1); 17 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm"); 18 | System.out.println(simpleDateFormat.format(calendar.getTime())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/LoggerTest.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ContextConfiguration; 8 | 9 | @SpringBootTest 10 | @ContextConfiguration(classes = CommunityApplication.class) 11 | public class LoggerTest { 12 | private static final Logger LOGGER = LoggerFactory.getLogger(LoggerTest.class); 13 | 14 | @Test 15 | public void testLogger() { 16 | LOGGER.debug("debug log"); 17 | LOGGER.info("info log"); 18 | LOGGER.warn("warn log"); 19 | LOGGER.error("error log"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/UserMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.apache.ibatis.annotations.Param; 5 | import tech.turl.community.entity.User; 6 | 7 | @Mapper 8 | public interface UserMapper { 9 | User selectById(@Param("id") int id); 10 | 11 | User selectByName(@Param("username") String username); 12 | 13 | User selectByEmail(@Param("email") String email); 14 | 15 | int insertUser(User user); 16 | 17 | int updateStatus(@Param("id") int id, @Param("status") int status); 18 | 19 | int updateHeader(@Param("id") int id, @Param("headerUrl") String headerUrl); 20 | 21 | int updatePassword(@Param("id") int id, @Param("password") String password); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/CommunityApplication.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | import javax.annotation.PostConstruct; 7 | 8 | /** 9 | * @author zhengguohuang 10 | * @date 2021/03/26 11 | */ 12 | @SpringBootApplication 13 | public class CommunityApplication { 14 | @PostConstruct 15 | public void init() { 16 | // 解决netty启动冲突问题 17 | // see Netty4Utils 18 | System.setProperty("es.set.netty.runtime.available.processors", "false"); 19 | } 20 | 21 | public static void main(String[] args) { 22 | // 启动Tomcat,创建容器 23 | SpringApplication.run(CommunityApplication.class, args); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/event/EventProducer.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.kafka.core.KafkaTemplate; 6 | import org.springframework.stereotype.Component; 7 | import tech.turl.community.entity.Event; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/03/23 12 | */ 13 | @Component 14 | public class EventProducer { 15 | 16 | @Autowired 17 | private KafkaTemplate kafkaTemplate; 18 | 19 | /** 20 | * 处理事件 21 | * 22 | * @param event 23 | */ 24 | public void fireEvent(Event event) { 25 | // 将事件发布到指定的主题 26 | kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/RedisCellTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.util.RedisCellRateLimiter; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/04/27 12 | */ 13 | @SpringBootTest 14 | @ContextConfiguration(classes = CommunityApplication.class) 15 | public class RedisCellTests { 16 | @Autowired private RedisCellRateLimiter redisCellRateLimiter; 17 | 18 | @Test 19 | public void testRateLimit() { 20 | System.out.println(redisCellRateLimiter.tryAcquire("rate:limit:post:1", 3, 3, 3600, 1)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/static/html/ajax-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AJAX 6 | 7 | 8 |

9 | 10 |

11 | 12 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | import javax.servlet.http.Cookie; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | /** 7 | * 通过request得到Cookie的值 8 | * 9 | * @author zhengguohuang 10 | * @date 2021/3/15 11 | */ 12 | public class CookieUtil { 13 | public static String getValue(HttpServletRequest request, String name) { 14 | if (request == null || name == null) { 15 | throw new IllegalArgumentException("参数为空!"); 16 | } 17 | Cookie[] cookies = request.getCookies(); 18 | if (cookies != null) { 19 | for (Cookie cookie : cookies) { 20 | if (cookie.getName().equals(name)) { 21 | return cookie.getValue(); 22 | } 23 | } 24 | } 25 | return null; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/QuartzTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.quartz.JobKey; 5 | import org.quartz.Scheduler; 6 | import org.quartz.SchedulerException; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ContextConfiguration; 10 | 11 | /** 12 | * @author zhengguohuang 13 | * @date 2021/03/29 14 | */ 15 | @SpringBootTest 16 | @ContextConfiguration(classes = CommunityApplication.class) 17 | public class QuartzTests { 18 | @Autowired private Scheduler scheduler; 19 | 20 | @Test 21 | public void testDeleteJob() throws SchedulerException { 22 | boolean result = scheduler.deleteJob(new JobKey("alphaJob", "alphaJobGroup")); 23 | System.out.println(result); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/WkConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import javax.annotation.PostConstruct; 9 | import java.io.File; 10 | 11 | /** 12 | * @author zhengguohuang 13 | * @date 2021/03/29 14 | */ 15 | @Configuration 16 | public class WkConfig { 17 | private static final Logger LOGGER = LoggerFactory.getLogger(WkConfig.class); 18 | 19 | @Value("${wk.image.storage}") 20 | private String wkImageStorage; 21 | 22 | @PostConstruct 23 | public void init() { 24 | // 创建WK图片目录 25 | File file = new File(wkImageStorage); 26 | if (!file.exists()) { 27 | file.mkdir(); 28 | LOGGER.info("创建WK图片目录: " + wkImageStorage); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/TransactionTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.service.AlphaService; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/03/17 12 | */ 13 | @SpringBootTest 14 | @ContextConfiguration(classes = CommunityApplication.class) 15 | public class TransactionTests { 16 | @Autowired 17 | private AlphaService alphaService; 18 | 19 | @Test 20 | public void testSave1() { 21 | Object object = alphaService.save1(); 22 | System.out.println(object); 23 | } 24 | 25 | @Test 26 | public void testSave2() { 27 | Object object = alphaService.save2(); 28 | System.out.println(object); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/Demo.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | /** 4 | * @author zhengguohuang 5 | * @date 2021/03/16 6 | */ 7 | public class Demo { 8 | private int id; 9 | private String name; 10 | private int age; 11 | 12 | public int getId() { 13 | return id; 14 | } 15 | 16 | public void setId(int id) { 17 | this.id = id; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | 28 | public int getAge() { 29 | return age; 30 | } 31 | 32 | public void setAge(int age) { 33 | this.age = age; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "Demo{" + 39 | "id=" + id + 40 | ", name='" + name + '\'' + 41 | ", age=" + age + 42 | '}'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/wkhtmltopdf/HtmlToPdfInterceptor.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.wkhtmltopdf; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | 8 | /** wkhtmltopdf.exe execution information interception */ 9 | public class HtmlToPdfInterceptor extends Thread { 10 | private InputStream is; 11 | 12 | public HtmlToPdfInterceptor(InputStream is) { 13 | this.is = is; 14 | } 15 | 16 | public void run() { 17 | try { 18 | InputStreamReader isr = new InputStreamReader(is, "utf-8"); 19 | BufferedReader br = new BufferedReader(isr); 20 | String line = null; 21 | while ((line = br.readLine()) != null) { 22 | System.out.println(line.toString()); // output content 23 | } 24 | } catch (IOException e) { 25 | e.printStackTrace(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/static/js/letter.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#sendBtn").click(send_letter); 3 | $(".close").click(delete_msg); 4 | }); 5 | 6 | function send_letter() { 7 | $("#sendModal").modal("hide"); 8 | 9 | var toName = $("#recipient-name").val(); 10 | var content = $("#message-text").val(); 11 | $.post( 12 | CONTEXT_PATH + "/letter/send", 13 | {"toName": toName, "content": content}, 14 | function (data) { 15 | data = $.parseJSON(data); 16 | if (data.code == 0) { 17 | $("#hintBody").text("发送成功!"); 18 | } else { 19 | $("#hintBody").text(data.msg); 20 | 21 | } 22 | $("#hintModal").modal("show"); 23 | setTimeout(function () { 24 | $("#hintModal").modal("hide"); 25 | location.reload(); 26 | }, 2000); 27 | 28 | } 29 | ); 30 | 31 | 32 | } 33 | 34 | function delete_msg() { 35 | // TODO 删除数据 36 | $(this).parents(".media").remove(); 37 | } -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/TestSensitiveFilter.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.util.SensitiveFilter; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/03/16 12 | */ 13 | @SpringBootTest 14 | @ContextConfiguration(classes = CommunityApplication.class) 15 | public class TestSensitiveFilter { 16 | 17 | @Autowired 18 | private SensitiveFilter sensitiveFilter; 19 | 20 | @Test 21 | public void testSensitiveFilter() { 22 | String text = "这里可以赌博,段义和可以嫖娼,可以吸毒,可以开票,哈哈哈!"; 23 | text = sensitiveFilter.filter(text); 24 | System.out.println(text); 25 | 26 | 27 | text = "这里可以☺☺赌☺博☺,可以¶嫖☺娼♮,可以吸♂毒,可以㉿开☺☺票㉿,哈哈哈!"; 28 | text = sensitiveFilter.filter(text); 29 | System.out.println(text); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 hzg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/static/js/global.js: -------------------------------------------------------------------------------- 1 | var CONTEXT_PATH = ""; 2 | window.alert = function(message) { 3 | if(!$(".alert-box").length) { 4 | $("body").append( 5 | '' 23 | ); 24 | } 25 | 26 | var h = $(".alert-box").height(); 27 | var y = h / 2 - 100; 28 | if(h > 600) y -= 100; 29 | $(".alert-box .modal-dialog").css("margin", (y < 0 ? 0 : y) + "px auto"); 30 | 31 | $(".alert-box .modal-body p").text(message); 32 | $(".alert-box").modal("show"); 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/MailTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import org.thymeleaf.TemplateEngine; 8 | import org.thymeleaf.context.Context; 9 | import tech.turl.community.util.MailClient; 10 | @SpringBootTest 11 | @ContextConfiguration(classes = CommunityApplication.class) 12 | public class MailTests { 13 | @Autowired 14 | private MailClient mailClient; 15 | @Autowired 16 | private TemplateEngine templateEngine; 17 | @Test 18 | public void testTextMail(){ 19 | mailClient.sendMail("838667533@qq.com", "Test", "welcome"); 20 | } 21 | @Test 22 | public void testHtmlMail(){ 23 | Context context = new Context(); 24 | context.setVariable("username", "sandt"); 25 | String content = templateEngine.process("/mail/demo", context); 26 | System.out.println(content); 27 | mailClient.sendMail("838667533@qq.com", "HTML", content); 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.RedisSerializer; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/03/20 12 | */ 13 | @Configuration 14 | public class RedisConfig { 15 | @Bean 16 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) { 17 | RedisTemplate template = new RedisTemplate<>(); 18 | template.setConnectionFactory(factory); 19 | // 设置key的序列化方式 20 | template.setKeySerializer(RedisSerializer.string()); 21 | // 设置value的序列化方式 22 | template.setValueSerializer(RedisSerializer.json()); 23 | // 设置hash的key的序列化方式 24 | template.setHashKeySerializer(RedisSerializer.string()); 25 | // 设置hash的value的序列化方式 26 | template.setHashValueSerializer(RedisSerializer.json()); 27 | 28 | template.afterPropertiesSet(); 29 | return template; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/static/js/index.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("#publishBtn").click(publish); 3 | }); 4 | 5 | function publish() { 6 | $("#publishModal").modal("hide"); 7 | 8 | // 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中 9 | // var token = $("meta[name='_csrf']").attr("content"); 10 | // var header = $("meta[name='_csrf_header']").attr("content"); 11 | // $(document).ajaxSend(function (e, xhr, options) { 12 | // xhr.setRequestHeader(header, token); 13 | // }); 14 | 15 | // 获取标题和内容 16 | var title = $("#recipient-name").val(); 17 | var content = $("#message-text").val(); 18 | 19 | // 发送异步请求(POST) 20 | $.post( 21 | CONTEXT_PATH + "/discuss/add", 22 | {"title": title, "content": content}, 23 | function (data) { 24 | data = $.parseJSON(data); 25 | // 在提示框中显示返回消息 26 | $("#hintBody").text(data.msg); 27 | // 显示提示框 28 | $("#hintModal").modal("show"); 29 | // 2秒后,自动隐藏提示框 30 | setTimeout(function () { 31 | $("#hintModal").modal("hide"); 32 | // 刷新页面 33 | if (data.code == 0) { 34 | window.location.reload(); 35 | } 36 | }, 2000); 37 | } 38 | ); 39 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/profile.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $(".follow-btn").click(follow); 3 | }); 4 | 5 | function follow() { 6 | var btn = this; 7 | if($(btn).hasClass("btn-info")) { 8 | // 关注TA 9 | $.post( 10 | CONTEXT_PATH + "/follow", 11 | {"entityType": 3, "entityId": $(btn).prev().val()}, 12 | function (data) { 13 | data = $.parseJSON(data); 14 | if (data.code == 0) { 15 | window.location.reload(); 16 | } else { 17 | alert(data.msg); 18 | } 19 | } 20 | ); 21 | 22 | 23 | // $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary"); 24 | } else { 25 | // 取消关注 26 | $.post( 27 | CONTEXT_PATH + "/unfollow", 28 | {"entityType": 3, "entityId": $(btn).prev().val()}, 29 | function (data) { 30 | data = $.parseJSON(data); 31 | if (data.code == 0) { 32 | window.location.reload(); 33 | } else { 34 | alert(data.msg); 35 | } 36 | } 37 | ); 38 | // $(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info"); 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/actuator/DatabaseEndpoint.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.actuator; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 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 | import tech.turl.community.util.CommunityUtil; 10 | 11 | import javax.sql.DataSource; 12 | import java.sql.Connection; 13 | import java.sql.SQLException; 14 | 15 | /** 16 | * @author zhengguohuang 17 | * @date 2021/03/31 18 | */ 19 | @Component 20 | @Endpoint(id = "database") 21 | public class DatabaseEndpoint { 22 | private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseEndpoint.class); 23 | @Autowired private DataSource dataSource; 24 | 25 | @ReadOperation 26 | public String checkConnection() { 27 | try (Connection conn = dataSource.getConnection()) { 28 | return CommunityUtil.getJSONString(0, "获取连接成功!"); 29 | } catch (SQLException e) { 30 | LOGGER.error("获取连接失败!" + e.getMessage()); 31 | return CommunityUtil.getJSONString(1, "获取连接失败!"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/interceptor/DataInterceptor.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller.interceptor; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.servlet.HandlerInterceptor; 6 | import tech.turl.community.entity.User; 7 | import tech.turl.community.service.DataService; 8 | import tech.turl.community.util.HostHolder; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | 13 | /** 14 | * @author zhengguohuang 15 | * @date 2021/03/29 16 | */ 17 | @Component 18 | public class DataInterceptor implements HandlerInterceptor { 19 | 20 | @Autowired 21 | private DataService dataService; 22 | 23 | @Autowired 24 | private HostHolder hostHolder; 25 | 26 | @Override 27 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 28 | // 统计UV 29 | String ip = request.getRemoteHost(); 30 | dataService.recordUV(ip); 31 | 32 | // 统计DAU 33 | User user = hostHolder.getUser(); 34 | if (user != null) { 35 | dataService.recordDAU(user.getId()); 36 | } 37 | 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/KaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.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 | @Bean 14 | public Producer kaptchaProducer(){ 15 | Properties properties = new Properties(); 16 | properties.setProperty("kaptcha.image.width", "100"); 17 | properties.setProperty("kaptcha.image.height", "40"); 18 | properties.setProperty("kaptcha.textproducer.font.size", "32"); 19 | properties.setProperty("kaptcha.textproducer.font.color", "0,0,0"); 20 | properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 21 | properties.setProperty("kaptcha.textproducer.char.length", "4"); 22 | properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); 23 | 24 | 25 | DefaultKaptcha kaptcha = new DefaultKaptcha(); 26 | Config config = new Config(properties); 27 | kaptcha.setConfig(config); 28 | return kaptcha; 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/LoginTicketMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.apache.ibatis.annotations.*; 4 | import tech.turl.community.entity.LoginTicket; 5 | 6 | /** 7 | * @author zhengguohuang 8 | * @date 2021/3/15 9 | */ 10 | @Mapper 11 | @Deprecated 12 | public interface LoginTicketMapper { 13 | /** 14 | * 插入login_ticket表 15 | * 16 | * @param loginTicket 17 | * @return 18 | */ 19 | @Insert({ 20 | "insert into login_ticket(user_id, ticket, status, expired) ", 21 | "values(#{userId}, #{ticket}, #{status}, #{expired})" 22 | }) 23 | @Options(useGeneratedKeys = true, keyProperty = "id") 24 | int insertLoginTicket(LoginTicket loginTicket); 25 | 26 | /** 27 | * 通过Ticket查找记录 28 | * 29 | * @param ticket 30 | * @return 31 | */ 32 | @Select({ 33 | "select id, user_id, ticket, status, expired ", 34 | "from login_ticket where ticket=#{ticket}" 35 | }) 36 | LoginTicket selectByTicket(String ticket); 37 | 38 | /** 39 | * 更新login_ticket的status字段 40 | * 41 | * @param ticket 42 | * @param status 43 | * @return 44 | */ 45 | @Update({ 46 | "update login_ticket set status = #{status} where ticket=#{ticket} " 47 | }) 48 | int updateStatus(@Param("ticket") String ticket, @Param("status") int status); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/aspect/AlphaAspect.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.aspect; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.*; 5 | 6 | /** 7 | * @author zhengguohuang 8 | * @date 2021/03/20 9 | */ 10 | //@Component 11 | //@Aspect 12 | public class AlphaAspect { 13 | @Pointcut("execution(* tech.turl.community.service.*.*(..))") 14 | public void pointcut() { 15 | 16 | } 17 | 18 | @Before("pointcut()") 19 | public void before() { 20 | System.out.println("before"); 21 | } 22 | 23 | @After("pointcut()") 24 | public void after() { 25 | System.out.println("after"); 26 | } 27 | 28 | @AfterReturning("pointcut()") 29 | public void afterRetuning() { 30 | System.out.println("afterRetuning"); 31 | } 32 | 33 | @AfterThrowing("pointcut()") 34 | public void afterThrowing() { 35 | System.out.println("afterThrowing"); 36 | } 37 | 38 | @Around("pointcut()") 39 | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { 40 | System.out.println("around before"); 41 | Object object = joinPoint.proceed(); 42 | System.out.println("around after"); 43 | return object; 44 | } 45 | /** 46 | * around before 47 | * before 48 | * ----- 49 | * around after 50 | * after 51 | * afterRetuning 52 | */ 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/CommunityConstant.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | /** 4 | * @author zhengguohuang 5 | * @date 2021/3/15 6 | */ 7 | public interface CommunityConstant { 8 | 9 | /** 激活成功 */ 10 | int ACTIVATION_SUCCESS = 0; 11 | 12 | /** 重复激活 */ 13 | int ACTIVATION_REPEAT = 1; 14 | 15 | /** 激活失败 */ 16 | int ACTIVATION_FAILED = 2; 17 | 18 | /** 默认状态的登录凭证的超时时间 12小时 */ 19 | int DEFAULT_EXPIRED_SECONDS = 3600 * 12; 20 | 21 | /** 记住状态下的登录凭证超时时间 100天 */ 22 | int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100; 23 | 24 | /** 实体类型:帖子 */ 25 | int ENTITY_TYPE_POST = 1; 26 | 27 | /** 实体类型:评论 */ 28 | int ENTITY_TYPE_COMMENT = 2; 29 | 30 | /** 实体类型:评论 */ 31 | int ENTITY_TYPE_USER = 3; 32 | 33 | /** 主题:评论 */ 34 | String TOPIC_COMMENT = "comment"; 35 | 36 | /** 主题:点赞 */ 37 | String TOPIC_LIKE = "like"; 38 | 39 | /** 主题:关注 */ 40 | String TOPIC_FOLLOW = "follow"; 41 | 42 | /** 主题:发帖 */ 43 | String TOPIC_PUBLISH = "publish"; 44 | 45 | /** 主题:分享 */ 46 | String TOPIC_SHARE = "share"; 47 | /** 删除 */ 48 | String TOPIC_DELETE = "delete"; 49 | 50 | /** 系统用户id */ 51 | int SYSTEM_USER_ID = 1; 52 | 53 | /** 权限:普通用户 */ 54 | String AUTHORITY_USER = "user"; 55 | 56 | /** 权限:管理员 */ 57 | String AUTHORITY_ADMIN = "admin"; 58 | 59 | /** 权限:版主 */ 60 | String AUTHORITY_MODERATOR = "moderator"; 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/TestLoginTicketMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.dao.LoginTicketMapper; 8 | import tech.turl.community.entity.LoginTicket; 9 | 10 | import java.util.Date; 11 | 12 | @SpringBootTest 13 | @ContextConfiguration(classes = CommunityApplication.class) 14 | public class TestLoginTicketMapper { 15 | 16 | @Autowired 17 | private LoginTicketMapper loginTicketMapper; 18 | 19 | @Test 20 | public void testInsert() { 21 | LoginTicket loginTicket = new LoginTicket(); 22 | loginTicket.setUserId(1); 23 | loginTicket.setStatus(0); 24 | loginTicket.setTicket("dddd"); 25 | loginTicket.setExpired(new Date()); 26 | int ret = loginTicketMapper.insertLoginTicket(loginTicket); 27 | System.out.println(ret); 28 | } 29 | 30 | @Test 31 | public void testSelectByTicket() { 32 | LoginTicket loginTicket = loginTicketMapper.selectByTicket("dddd"); 33 | System.out.println(loginTicket); 34 | } 35 | 36 | @Test 37 | public void testUpdateStatus() { 38 | int ret = loginTicketMapper.updateStatus("dddd", 1); 39 | System.out.println(ret); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/TestMessageMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.dao.MessageMapper; 8 | import tech.turl.community.entity.Message; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/03/19 15 | */ 16 | @SpringBootTest 17 | @ContextConfiguration(classes = CommunityApplication.class) 18 | public class TestMessageMapper { 19 | @Autowired 20 | private MessageMapper messageMapper; 21 | 22 | @Test 23 | public void test() { 24 | List list = messageMapper.selectConversations(111, 0, 20); 25 | for (Message message : list) { 26 | System.out.println(message); 27 | } 28 | int count = messageMapper.selectConversationCount(111); 29 | System.out.println(count); 30 | 31 | list = messageMapper.selectLetters("111_112", 0, 10); 32 | for (Message message : list) { 33 | System.out.println(message); 34 | } 35 | count = messageMapper.selectLetterCount("111_112"); 36 | System.out.println(count); 37 | 38 | count = messageMapper.selectLetterUnreadCount(131, "111_131"); 39 | System.out.println(count); 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/interceptor/MessageInterceptor.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller.interceptor; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.servlet.HandlerInterceptor; 6 | import org.springframework.web.servlet.ModelAndView; 7 | import tech.turl.community.entity.User; 8 | import tech.turl.community.service.MessageService; 9 | import tech.turl.community.util.HostHolder; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | /** 15 | * @author zhengguohuang 16 | * @date 2021/03/24 17 | */ 18 | @Component 19 | public class MessageInterceptor implements HandlerInterceptor { 20 | @Autowired 21 | private HostHolder hostHolder; 22 | 23 | @Autowired 24 | private MessageService messageService; 25 | 26 | @Override 27 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 28 | User user = hostHolder.getUser(); 29 | if (user != null && modelAndView != null) { 30 | int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null); 31 | int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null); 32 | modelAndView.addObject("allUnreadCount", letterUnreadCount + noticeUnreadCount); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/KafkaTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerRecord; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.kafka.annotation.KafkaListener; 8 | import org.springframework.kafka.core.KafkaTemplate; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.test.context.ContextConfiguration; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/03/23 15 | */ 16 | @SpringBootTest 17 | @ContextConfiguration(classes = CommunityApplication.class) 18 | public class KafkaTests { 19 | @Autowired 20 | private KafkaProducer kafkaProducer; 21 | 22 | @Test 23 | public void testKafka() throws InterruptedException { 24 | kafkaProducer.sendMessage("test", "你好"); 25 | kafkaProducer.sendMessage("test", "在吗"); 26 | 27 | Thread.sleep(1000 * 10); 28 | } 29 | } 30 | 31 | @Component 32 | class KafkaProducer { 33 | @Autowired 34 | private KafkaTemplate kafkaTemplate; 35 | 36 | public void sendMessage(String topic, String content) { 37 | kafkaTemplate.send(topic, content); 38 | } 39 | } 40 | 41 | @Component 42 | class KafkaConsumer { 43 | @KafkaListener(topics = {"test"}) 44 | public void handleMessage(ConsumerRecord record) { 45 | System.out.println("我消费了:" + record.value()); 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/MailClient.java: -------------------------------------------------------------------------------- 1 | package tech.turl.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 | private static final Logger LOGGER = LoggerFactory.getLogger(MailClient.class); 17 | 18 | @Autowired 19 | private JavaMailSender mailSender; 20 | 21 | @Value("${spring.mail.username}") 22 | private String from; 23 | 24 | public void sendMail(String to, String subject, String content){ 25 | try { 26 | MimeMessage message= mailSender.createMimeMessage(); 27 | MimeMessageHelper helper = new MimeMessageHelper(message); 28 | helper.setFrom(from); 29 | helper.setTo(to); 30 | helper.setSubject(subject); 31 | helper.setText(content,true); 32 | mailSender.send(helper.getMimeMessage()); 33 | } catch (MessagingException e) { 34 | e.printStackTrace(); 35 | LOGGER.error("发送邮件失败:"+e.getMessage()); 36 | } 37 | } 38 | public static void main(String[] args) { 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/CaffeineTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.entity.DiscussPost; 8 | import tech.turl.community.service.DiscussPostService; 9 | 10 | import java.util.Date; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/03/30 15 | */ 16 | @SpringBootTest 17 | @ContextConfiguration(classes = CommunityApplication.class) 18 | public class CaffeineTests { 19 | @Autowired private DiscussPostService postService; 20 | 21 | @Test 22 | public void initDataForTest() { 23 | for (int i = 0; i < 100000; i++) { 24 | DiscussPost post = new DiscussPost(); 25 | post.setUserId(111); 26 | post.setTitle("互联网秋招暖春计划"); 27 | post.setContent("xxxxxxxxxxxxxxxxxxxxx互联网XXX"); 28 | post.setCreateTime(new Date()); 29 | post.setScore(Math.random() * 2000); 30 | postService.addDiscussPost(post); 31 | } 32 | } 33 | 34 | @Test 35 | public void testCache() { 36 | System.out.println(postService.findDiscussPosts(0, 0, 10, 1)); 37 | System.out.println(postService.findDiscussPosts(0, 0, 10, 1)); 38 | System.out.println(postService.findDiscussPosts(0, 0, 10, 1)); 39 | System.out.println(postService.findDiscussPosts(0, 0, 10, 0)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/LoginTicket.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author zhengguohuang 7 | * @date 2021/3/15 8 | */ 9 | public class LoginTicket { 10 | private int id; 11 | private int userId; 12 | private String ticket; 13 | private int status; 14 | private Date expired; 15 | 16 | public int getId() { 17 | return id; 18 | } 19 | 20 | public void setId(int id) { 21 | this.id = id; 22 | } 23 | 24 | public int getUserId() { 25 | return userId; 26 | } 27 | 28 | public void setUserId(int userId) { 29 | this.userId = userId; 30 | } 31 | 32 | public String getTicket() { 33 | return ticket; 34 | } 35 | 36 | public void setTicket(String ticket) { 37 | this.ticket = ticket; 38 | } 39 | 40 | public int getStatus() { 41 | return status; 42 | } 43 | 44 | public void setStatus(int status) { 45 | this.status = status; 46 | } 47 | 48 | public Date getExpired() { 49 | return expired; 50 | } 51 | 52 | public void setExpired(Date expired) { 53 | this.expired = expired; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "LoginTicket{" + 59 | "id=" + id + 60 | ", userId=" + userId + 61 | ", ticket='" + ticket + '\'' + 62 | ", status=" + status + 63 | ", expired=" + expired + 64 | '}'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/interceptor/LoginRequiredInterceptor.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller.interceptor; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.method.HandlerMethod; 5 | import org.springframework.web.servlet.HandlerInterceptor; 6 | import tech.turl.community.annotation.LoginRequired; 7 | import tech.turl.community.util.HostHolder; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.lang.reflect.Method; 12 | 13 | /** 14 | * @author zhengguohuang 15 | * @date 2021/03/16 16 | */ 17 | // @Component 18 | @Deprecated 19 | public class LoginRequiredInterceptor implements HandlerInterceptor { 20 | @Autowired private HostHolder hostHolder; 21 | 22 | @Override 23 | public boolean preHandle( 24 | HttpServletRequest request, HttpServletResponse response, Object handler) 25 | throws Exception { 26 | if (handler instanceof HandlerMethod) { 27 | HandlerMethod handlerMethod = (HandlerMethod) handler; 28 | Method method = handlerMethod.getMethod(); 29 | LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); 30 | if (loginRequired != null && hostHolder.getUser() == null) { 31 | response.sendRedirect(request.getContextPath() + "/login"); 32 | System.out.println("----------------------拦截---------------------"); 33 | return false; 34 | } 35 | } 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/advice/ExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller.advice; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import tech.turl.community.util.CommunityUtil; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.io.PrintWriter; 14 | 15 | 16 | /** 17 | * @author zhengguohuang 18 | * @date 2021/03/20 19 | */ 20 | @ControllerAdvice(annotations = Controller.class) 21 | public class ExceptionAdvice { 22 | private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionAdvice.class); 23 | 24 | @ExceptionHandler 25 | public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException { 26 | LOGGER.error("服务器发生异常: ", e.getMessage()); 27 | for (StackTraceElement element : e.getStackTrace()) { 28 | LOGGER.error(element.toString()); 29 | } 30 | String xRequestedWith = request.getHeader("x-requested-with"); 31 | String type = "XMLHttpRequest"; 32 | if (type.equals(xRequestedWith)) { 33 | response.setContentType("application/plain;charset=utf-8"); 34 | PrintWriter writer = response.getWriter(); 35 | writer.write(CommunityUtil.getJSONString(1, "服务器异常!")); 36 | } else { 37 | response.sendRedirect(request.getContextPath() + "/error"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/static/js/setting.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $("#uploadForm").submit(upload); 3 | $("#forgetForm").submit(forget); 4 | }); 5 | 6 | function upload() { 7 | $.ajax({ 8 | url: 9 | "http://upload.qiniup.com", 10 | method: 11 | "post", 12 | processData: 13 | false, 14 | contentType: 15 | false, 16 | data: 17 | new FormData($("#uploadForm")[0]), 18 | success: function (data) { 19 | if (data && data.code == 0) { 20 | // 更新头像路径 21 | $.post( 22 | CONTEXT_PATH + "/user/header/url", 23 | {"fileName": $("input[name='key']").val()}, 24 | function (data) { 25 | data = $.parseJSON(data); 26 | if (data.code == 0) { 27 | window.location.reload(); 28 | } else { 29 | alert(data.msg); 30 | } 31 | } 32 | ); 33 | } else { 34 | alert("上传失败!"); 35 | } 36 | } 37 | 38 | }) 39 | ; 40 | return false; 41 | } 42 | 43 | function forget() { 44 | $.post( 45 | CONTEXT_PATH + "/user/forgetPassword", 46 | {"oldPassword": $("input[name='oldPassword']").val(), "newPassword": $("input[name='newPassword']").val()}, 47 | function (data) { 48 | data = $.parseJSON(data); 49 | if (data.code == 0) { 50 | // window.location.href = "/logout"; 51 | } else { 52 | alert(data.msg); 53 | } 54 | } 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/mapper/user-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | id, username, password, salt, email, type, status, activation_code, header_url, create_time 8 | 9 | 10 | username, password, salt, email, type, status, activation_code, header_url, create_time 11 | 12 | 17 | 22 | 27 | 28 | INSERT INTO user () 29 | VALUES (#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, 30 | #{activationCode}, #{headerUrl}, #{createTime}) 31 | 32 | 33 | UPDATE user SET status = #{status} WHERE id = #{id} 34 | 35 | 36 | UPDATE user SET header_url = #{headerUrl} WHERE id = #{id} 37 | 38 | 39 | UPDATE user SET password = #{password} WHERE id = #{id} 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/aspect/ServiceLogAspect.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.aspect; 2 | 3 | import org.aspectj.lang.JoinPoint; 4 | import org.aspectj.lang.annotation.Aspect; 5 | import org.aspectj.lang.annotation.Before; 6 | import org.aspectj.lang.annotation.Pointcut; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.context.request.RequestContextHolder; 11 | import org.springframework.web.context.request.ServletRequestAttributes; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import java.text.SimpleDateFormat; 15 | import java.util.Date; 16 | 17 | /** 18 | * @author zhengguohuang 19 | * @date 2021/03/20 20 | */ 21 | @Component 22 | @Aspect 23 | public class ServiceLogAspect { 24 | 25 | private static final Logger LOGGER = LoggerFactory.getLogger(ServiceLogAspect.class); 26 | 27 | @Pointcut("execution(* tech.turl.community.service.*.*(..))") 28 | public void pointcut() {} 29 | 30 | @Before("pointcut()") 31 | public void before(JoinPoint joinPoint) { 32 | // 用户[1.2.3.4],在[xxxx],访问了[tech.turl.community.service.xxx()]. 33 | ServletRequestAttributes attributes = 34 | (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 35 | if (attributes == null) { 36 | return; 37 | } 38 | HttpServletRequest request = attributes.getRequest(); 39 | String ip = request.getRemoteHost(); 40 | String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); 41 | String target = 42 | joinPoint.getSignature().getDeclaringTypeName() 43 | + "." 44 | + joinPoint.getSignature().getName(); 45 | LOGGER.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/BlockingQueueTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.ArrayBlockingQueue; 5 | import java.util.concurrent.BlockingQueue; 6 | 7 | /** 8 | * @author zhengguohuang 9 | * @date 2021/03/23 10 | */ 11 | public class BlockingQueueTests { 12 | 13 | public static void main(String[] args) { 14 | BlockingQueue queue = new ArrayBlockingQueue(10); 15 | new Thread(new Producer(queue)).start(); 16 | new Thread(new Consumer(queue)).start(); 17 | new Thread(new Consumer(queue)).start(); 18 | new Thread(new Consumer(queue)).start(); 19 | 20 | } 21 | } 22 | 23 | class Producer implements Runnable { 24 | private BlockingQueue queue; 25 | 26 | public Producer(BlockingQueue queue) { 27 | this.queue = queue; 28 | } 29 | 30 | @Override 31 | public void run() { 32 | try { 33 | for (int i = 0; i < 100; i++) { 34 | Thread.sleep(20); 35 | queue.put(i); 36 | System.out.println(Thread.currentThread().getName() + "生产:" + queue.size()); 37 | } 38 | } catch (Exception e) { 39 | e.printStackTrace(); 40 | } 41 | 42 | } 43 | } 44 | 45 | class Consumer implements Runnable { 46 | private BlockingQueue queue; 47 | 48 | public Consumer(BlockingQueue queue) { 49 | this.queue = queue; 50 | } 51 | 52 | @Override 53 | public void run() { 54 | try { 55 | while (true) { 56 | Thread.sleep(new Random().nextInt(1000)); 57 | queue.take(); 58 | System.out.println(Thread.currentThread().getName() + "消费:" + queue.size()); 59 | } 60 | } catch (Exception e) { 61 | e.printStackTrace(); 62 | } 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/Page.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | public class Page { 4 | // 当前页码 5 | private int current = 1; 6 | // 显示上限 7 | private int limit = 10; 8 | // 数据总数(用于计算总页数) 9 | private int rows; 10 | // 查询路径(用于复用分页链接) 11 | private String path; 12 | 13 | public int getCurrent() { 14 | return current; 15 | } 16 | 17 | public void setCurrent(int current) { 18 | if(current >= 1){ 19 | this.current = current; 20 | } 21 | } 22 | 23 | public int getLimit() { 24 | return limit; 25 | } 26 | 27 | public void setLimit(int limit) { 28 | if(limit >= 1 && limit <= 100){ 29 | this.limit = limit; 30 | } 31 | } 32 | 33 | public int getRows() { 34 | return rows; 35 | } 36 | 37 | public void setRows(int rows) { 38 | if(rows >= 0){ 39 | this.rows = rows; 40 | } 41 | } 42 | 43 | public String getPath() { 44 | return path; 45 | } 46 | 47 | public void setPath(String path) { 48 | this.path = path; 49 | } 50 | 51 | /** 52 | * 获取当前页的起始行 53 | * @return 54 | */ 55 | public int getOffset(){ 56 | // current * limit - limit 57 | return (current - 1) * limit; 58 | } 59 | 60 | /** 61 | * 获取总页数 62 | * @return 63 | */ 64 | public int getTotal(){ 65 | return rows % limit == 0 ? rows / limit : rows / limit + 1; 66 | } 67 | 68 | /** 69 | * 获取起始页码 70 | * @return 71 | */ 72 | public int getFrom(){ 73 | int from = current - 2; 74 | return from < 1 ? 1 : from; 75 | } 76 | 77 | /** 78 | * 获取结束页码 79 | * @return 80 | */ 81 | public int getTo(){ 82 | int to = current + 2; 83 | int total = getTotal(); 84 | return to > total ? total : to; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/Event.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * 用于封装消息的事件对象 8 | * 9 | * @author zhengguohuang 10 | * @date 2021/03/23 11 | */ 12 | public class Event { 13 | private String topic; 14 | /** 15 | * 触发事件的用户 16 | */ 17 | private int userId; 18 | /** 19 | * 目标实体 20 | */ 21 | private int entityType; 22 | private int entityId; 23 | /** 24 | * 目标实体的拥有者,如果目标实体是用户则entityId=entityUserId 25 | */ 26 | private int entityUserId; 27 | /** 28 | * 封装其他数据,用于扩展 29 | */ 30 | private Map data = new HashMap<>(16); 31 | 32 | public String getTopic() { 33 | return topic; 34 | } 35 | 36 | public Event setTopic(String topic) { 37 | this.topic = topic; 38 | return this; 39 | } 40 | 41 | public int getUserId() { 42 | return userId; 43 | } 44 | 45 | public Event setUserId(int userId) { 46 | this.userId = userId; 47 | return this; 48 | } 49 | 50 | public int getEntityType() { 51 | return entityType; 52 | } 53 | 54 | public Event setEntityType(int entityType) { 55 | this.entityType = entityType; 56 | return this; 57 | } 58 | 59 | public int getEntityId() { 60 | return entityId; 61 | } 62 | 63 | public Event setEntityId(int entityId) { 64 | this.entityId = entityId; 65 | return this; 66 | } 67 | 68 | public int getEntityUserId() { 69 | return entityUserId; 70 | } 71 | 72 | public Event setEntityUserId(int entityUserId) { 73 | this.entityUserId = entityUserId; 74 | return this; 75 | } 76 | 77 | public Map getData() { 78 | return data; 79 | } 80 | 81 | public Event setData(String key, Object value) { 82 | this.data.put(key, value); 83 | return this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/CommunityUtil.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.util.DigestUtils; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.UUID; 10 | 11 | /** 12 | * @author zhengguohuang 13 | * @date 2021/03/17 14 | */ 15 | public class CommunityUtil { 16 | /** 17 | * 生成随机字符串 18 | * @return 19 | */ 20 | public static String generateUUID(){ 21 | return UUID.randomUUID().toString().replaceAll("-",""); 22 | } 23 | 24 | /** 25 | * MD5加密 26 | * hello -> abc123def456 27 | * hello + 3e4af -> aof39fjf3ffi 28 | * @param key 29 | */ 30 | public static String md5(String key){ 31 | if(StringUtils.isBlank(key)){ 32 | return null; 33 | } 34 | return DigestUtils.md5DigestAsHex(key.getBytes()); 35 | } 36 | 37 | public static void main(String[] args) { 38 | System.out.println(generateUUID()); 39 | System.out.println(md5("123")); 40 | 41 | Map map = new HashMap<>(16); 42 | map.put("name", "zhangsan"); 43 | map.put("age", 25); 44 | System.out.println(getJSONString(0, "ok", map)); 45 | } 46 | 47 | public static String getJSONString(int code, String msg, Map map) { 48 | JSONObject json = new JSONObject(); 49 | json.put("code", code); 50 | json.put("msg", msg); 51 | if (map != null) { 52 | for (String key : map.keySet()) { 53 | json.put(key, map.get(key)); 54 | } 55 | } 56 | return json.toJSONString(); 57 | } 58 | 59 | public static String getJSONString(int code, String msg) { 60 | return getJSONString(code, msg, null); 61 | } 62 | 63 | public static String getJSONString(int code) { 64 | return getJSONString(code, null, null); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/interceptor/AlphaInterceptor.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller.interceptor; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.servlet.HandlerInterceptor; 7 | import org.springframework.web.servlet.ModelAndView; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/3/15 15 | */ 16 | @Component 17 | public class AlphaInterceptor implements HandlerInterceptor { 18 | private static final Logger LOGGER = LoggerFactory.getLogger(AlphaInterceptor.class); 19 | 20 | /** 21 | * 在Controller之前执行 22 | * 23 | * @param request 24 | * @param response 25 | * @param handler 26 | * @return 27 | * @throws Exception 28 | */ 29 | @Override 30 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 31 | LOGGER.debug("preHandle: " + handler.toString()); 32 | return true; 33 | } 34 | 35 | /** 36 | * 在Controller之后执行 37 | * 38 | * @param request 39 | * @param response 40 | * @param handler 41 | * @param modelAndView 42 | * @throws Exception 43 | */ 44 | @Override 45 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 46 | LOGGER.debug("postHandle: " + handler.toString()); 47 | } 48 | 49 | /** 50 | * 在模板引擎之后执行 51 | * 52 | * @param request 53 | * @param response 54 | * @param handler 55 | * @param ex 56 | * @throws Exception 57 | */ 58 | @Override 59 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 60 | LOGGER.debug("afterCompletion: " + handler.toString()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/resources/static/js/discuss.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $("#topBtn").click(setTop); 3 | $("#wonderfulBtn").click(setWonderful); 4 | $("#deleteBtn").click(setDelete); 5 | }); 6 | 7 | function like(btn, entityType, entityId, entityUserId, postId) { 8 | $.post( 9 | CONTEXT_PATH + "/like", 10 | {"entityType": entityType, "entityId": entityId, "entityUserId": entityUserId, "postId": postId}, 11 | function (data) { 12 | data = $.parseJSON(data); 13 | if (data.code == 0) { 14 | $(btn).children("i").text(data.likeCount); 15 | $(btn).children("b").text(data.likeStatus == 1 ? '已赞' : "赞"); 16 | } else { 17 | alert(data.msg); 18 | } 19 | } 20 | ); 21 | } 22 | 23 | // 置顶 24 | function setTop() { 25 | $.post( 26 | CONTEXT_PATH + "/discuss/top", 27 | {"id": $("#postId").val()}, 28 | function (data) { 29 | data = $.parseJSON(data); 30 | if (data.code == 0) { 31 | $("#topBtn").attr("disabled", "disabled"); 32 | } else { 33 | alert(data.msg); 34 | } 35 | } 36 | ); 37 | } 38 | 39 | 40 | // 加精 41 | function setWonderful() { 42 | $.post( 43 | CONTEXT_PATH + "/discuss/wonderful", 44 | {"id": $("#postId").val()}, 45 | function (data) { 46 | data = $.parseJSON(data); 47 | if (data.code == 0) { 48 | $("#wonderfulBtn").attr("disabled", "disabled"); 49 | } else { 50 | alert(data.msg); 51 | } 52 | } 53 | ); 54 | } 55 | 56 | // 删除 57 | function setDelete() { 58 | $.post( 59 | CONTEXT_PATH + "/discuss/delete", 60 | {"id": $("#postId").val()}, 61 | function (data) { 62 | data = $.parseJSON(data); 63 | if (data.code == 0) { 64 | location.href = CONTEXT_PATH + "/"; 65 | } else { 66 | alert(data.msg); 67 | } 68 | } 69 | ); 70 | } -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/Message.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author zhengguohuang 7 | * @date 2021/03/19 8 | */ 9 | public class Message { 10 | private int id; 11 | private int fromId; 12 | private int toId; 13 | private String conversationId; 14 | private String content; 15 | private int status; 16 | private Date createTime; 17 | 18 | public int getId() { 19 | return id; 20 | } 21 | 22 | public void setId(int id) { 23 | this.id = id; 24 | } 25 | 26 | public int getFromId() { 27 | return fromId; 28 | } 29 | 30 | public void setFromId(int fromId) { 31 | this.fromId = fromId; 32 | } 33 | 34 | public int getToId() { 35 | return toId; 36 | } 37 | 38 | public void setToId(int toId) { 39 | this.toId = toId; 40 | } 41 | 42 | public String getConversationId() { 43 | return conversationId; 44 | } 45 | 46 | public void setConversationId(String conversationId) { 47 | this.conversationId = conversationId; 48 | } 49 | 50 | public String getContent() { 51 | return content; 52 | } 53 | 54 | public void setContent(String content) { 55 | this.content = content; 56 | } 57 | 58 | public int getStatus() { 59 | return status; 60 | } 61 | 62 | public void setStatus(int status) { 63 | this.status = status; 64 | } 65 | 66 | public Date getCreateTime() { 67 | return createTime; 68 | } 69 | 70 | public void setCreateTime(Date createTime) { 71 | this.createTime = createTime; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "Message{" + 77 | "id=" + id + 78 | ", fromId=" + fromId + 79 | ", toId=" + toId + 80 | ", conversationId='" + conversationId + '\'' + 81 | ", content='" + content + '\'' + 82 | ", status=" + status + 83 | ", createTime=" + createTime + 84 | '}'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | import tech.turl.community.controller.interceptor.AlphaInterceptor; 8 | import tech.turl.community.controller.interceptor.DataInterceptor; 9 | import tech.turl.community.controller.interceptor.LoginTicketInterceptor; 10 | import tech.turl.community.controller.interceptor.MessageInterceptor; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/3/15 15 | */ 16 | @Configuration 17 | public class WebMvcConfig implements WebMvcConfigurer { 18 | @Autowired private AlphaInterceptor alphaInterceptor; 19 | 20 | @Autowired private LoginTicketInterceptor loginTicketInterceptor; 21 | 22 | // @Autowired 23 | // private LoginRequiredInterceptor loginRequiredInterceptor; 24 | 25 | @Autowired private MessageInterceptor messageInterceptor; 26 | 27 | @Autowired private DataInterceptor dataInterceptor; 28 | 29 | @Override 30 | public void addInterceptors(InterceptorRegistry registry) { 31 | registry 32 | .addInterceptor(alphaInterceptor) 33 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg") 34 | .addPathPatterns("/register", "/login"); 35 | 36 | registry 37 | .addInterceptor(loginTicketInterceptor) 38 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 39 | 40 | // registry.addInterceptor(loginRequiredInterceptor) 41 | // .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", 42 | // "/*/*.jpeg"); 43 | 44 | registry 45 | .addInterceptor(messageInterceptor) 46 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 47 | 48 | registry 49 | .addInterceptor(dataInterceptor) 50 | .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/DiscussPostMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.apache.ibatis.annotations.Param; 5 | import tech.turl.community.entity.DiscussPost; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/03/17 12 | */ 13 | @Mapper 14 | public interface DiscussPostMapper { 15 | /** 16 | * 分页查询帖子 17 | * 18 | * @param userId 19 | * @param offset 20 | * @param limit 21 | * @param orderMode 如果传入1按照热度来排 22 | * @return 23 | */ 24 | List selectDiscussPosts( 25 | @Param("userId") int userId, 26 | @Param("offset") int offset, 27 | @Param("limit") int limit, 28 | @Param("orderMode") int orderMode); 29 | 30 | /** 31 | * 查询用户的帖子 @Param注解用于给定参数取别名 如果只有一个参数,并且在if里面使用,则必须加别名 32 | * 33 | * @param userId 34 | * @return 35 | */ 36 | int selectDiscussPostRows(@Param("userId") int userId); 37 | 38 | /** 39 | * 插入一条帖子 40 | * 41 | * @param discussPost 42 | * @return 43 | */ 44 | int insertDiscussPost(DiscussPost discussPost); 45 | 46 | /** 47 | * 通过id查找帖子 48 | * 49 | * @param id 50 | * @return 51 | */ 52 | DiscussPost selectDiscussPostById(int id); 53 | 54 | /** 55 | * 更新帖子评论数量 56 | * 57 | * @param id 58 | * @param commentCount 59 | * @return 60 | */ 61 | int updateCommentCount(@Param("id") int id, @Param("commentCount") int commentCount); 62 | 63 | /** 64 | * 更新帖子类型 0:普通 1:置顶 65 | * 66 | * @param id 67 | * @param type 68 | * @return 69 | */ 70 | int updateType(@Param("id") int id, @Param("type") int type); 71 | 72 | /** 73 | * 更新帖子状态 0:正常 1:精华 2:拉黑 74 | * 75 | * @param id 76 | * @param status 77 | * @return 78 | */ 79 | int updateStatus(@Param("id") int id, @Param("status") int status); 80 | 81 | /** 82 | * 更新帖子分数 83 | * 84 | * @param id 85 | * @param score 86 | */ 87 | void updateScore(@Param("id") int id, @Param("score") double score); 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/RedisCellRateLimiter.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.data.redis.core.script.DefaultRedisScript; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * @author zhengguohuang 13 | * @date 2021/04/27 14 | */ 15 | @Component 16 | public class RedisCellRateLimiter implements CommunityConstant { 17 | @Autowired private RedisTemplate redisTemplate; 18 | private static final String LUA_SCRIPT = 19 | "local key = KEYS[1]\n" 20 | + "local init_burst = tonumber(ARGV[1])\n" 21 | + "local max_burst = tonumber(ARGV[2])\n" 22 | + "local period = tonumber(ARGV[3])\n" 23 | + "local quota = ARGV[4]\n" 24 | + "return redis.call('CL.THROTTLE',key,init_burst,max_burst,period,quota)"; 25 | 26 | /** 27 | * 尝试申请资源 28 | * 29 | * @param key Redis key 30 | * @param initBurst 初始的容量 31 | * @param maxCapacity 漏桶最大容量 32 | * @param period 时间间隔 33 | * @param quote 目标资源数 34 | * @return 是否通过限流 35 | */ 36 | public boolean tryAcquire(String key, int initBurst, int maxCapacity, int period, int quote) { 37 | List keys = new ArrayList<>(); 38 | keys.add(key); 39 | 40 | DefaultRedisScript> redisScript = new DefaultRedisScript(LUA_SCRIPT, List.class); 41 | Object result = 42 | redisTemplate.execute(redisScript, keys, initBurst, maxCapacity, period, quote); 43 | List res = (List) result; 44 | /** res.get(0) 通过拒绝 res.get(1) 漏斗容量 res.get(2) 剩余空间 res.get(3) 重试时间 res.get(4) 漏斗空间 */ 45 | return res.get(0) == 0; 46 | } 47 | 48 | /** 49 | * 尝试申请资源 50 | * 51 | * @param 52 | * @return 是否通过限流 53 | */ 54 | public boolean tryAcquire(String key, int initBurst, int maxCapacity, int period) { 55 | return this.tryAcquire(key, initBurst, maxCapacity, period, 1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/DataContoller.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.format.annotation.DateTimeFormat; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import tech.turl.community.service.DataService; 11 | 12 | import java.util.Date; 13 | 14 | /** 15 | * @author zhengguohuang 16 | * @date 2021/03/29 17 | */ 18 | @RequestMapping("/data") 19 | @Controller 20 | public class DataContoller { 21 | @Autowired private DataService dataService; 22 | 23 | /** 24 | * 统计页面 25 | * 26 | * @return 27 | */ 28 | @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}) 29 | public String getDataPage() { 30 | return "site/admin/data"; 31 | } 32 | 33 | /** 34 | * 统计UV 35 | * 36 | * @param start 37 | * @param end 38 | * @param model 39 | * @return 40 | */ 41 | @PostMapping("/uv") 42 | public String getUV( 43 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date start, 44 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, 45 | Model model) { 46 | long uv = dataService.calculateUV(start, end); 47 | model.addAttribute("uvResult", uv); 48 | model.addAttribute("uvStartDate", start); 49 | model.addAttribute("uvEndDate", end); 50 | return "forward:/data"; 51 | } 52 | 53 | /** 54 | * 统计网站活跃用户 55 | * 56 | * @param start 57 | * @param end 58 | * @param model 59 | * @return 60 | */ 61 | @PostMapping("/dau") 62 | public String getDAU( 63 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date start, 64 | @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, 65 | Model model) { 66 | long dau = dataService.calculateDAU(start, end); 67 | model.addAttribute("dauResult", dau); 68 | model.addAttribute("dauStartDate", start); 69 | model.addAttribute("dauEndDate", end); 70 | return "forward:/data"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * @author zhengguohuang 7 | * @date 2021/03/17 8 | */ 9 | public class Comment { 10 | private int id; 11 | private int userId; 12 | private int entityType; 13 | private int entityId; 14 | private int targetId; 15 | private String content; 16 | private int status; 17 | private Date createTime; 18 | 19 | public int getId() { 20 | return id; 21 | } 22 | 23 | public void setId(int id) { 24 | this.id = id; 25 | } 26 | 27 | public int getUserId() { 28 | return userId; 29 | } 30 | 31 | public void setUserId(int userId) { 32 | this.userId = userId; 33 | } 34 | 35 | public int getEntityType() { 36 | return entityType; 37 | } 38 | 39 | public void setEntityType(int entityType) { 40 | this.entityType = entityType; 41 | } 42 | 43 | public int getEntityId() { 44 | return entityId; 45 | } 46 | 47 | public void setEntityId(int entityId) { 48 | this.entityId = entityId; 49 | } 50 | 51 | public int getTargetId() { 52 | return targetId; 53 | } 54 | 55 | public void setTargetId(int targetId) { 56 | this.targetId = targetId; 57 | } 58 | 59 | public String getContent() { 60 | return content; 61 | } 62 | 63 | public void setContent(String content) { 64 | this.content = content; 65 | } 66 | 67 | public int getStatus() { 68 | return status; 69 | } 70 | 71 | public void setStatus(int status) { 72 | this.status = status; 73 | } 74 | 75 | public Date getCreateTime() { 76 | return createTime; 77 | } 78 | 79 | public void setCreateTime(Date createTime) { 80 | this.createTime = createTime; 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return "Comment{" + 86 | "id=" + id + 87 | ", userId=" + userId + 88 | ", entityType=" + entityType + 89 | ", entityId=" + entityId + 90 | ", targetId=" + targetId + 91 | ", content='" + content + '\'' + 92 | ", status=" + status + 93 | ", createTime=" + createTime + 94 | '}'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/resources/static/css/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | background: #eee; 7 | font-family: arial, STHeiti, 'Microsoft YaHei', \5b8b\4f53; 8 | font-size: 14px; 9 | height: 100%; 10 | } 11 | 12 | .nk-container { 13 | position: relative; 14 | height: auto; 15 | min-height: 100%; 16 | } 17 | 18 | .container { 19 | width: 960px; 20 | padding: 0; 21 | } 22 | 23 | header .navbar-brand { 24 | background: url('http://static.nowcoder.com/images/res/logo/logo-v3.png') no-repeat; 25 | background-size: 147px 42px; 26 | width: 147px; 27 | height: 42px; 28 | margin: 5px 15px 5px 0; 29 | } 30 | 31 | header .navbar { 32 | padding: 5px 0; 33 | font-size: 16px; 34 | } 35 | 36 | header .badge { 37 | position: absolute; 38 | top: -3px; 39 | left: 33px; 40 | } 41 | 42 | footer { 43 | padding: 20px 0; 44 | font-size: 12px; 45 | position: absolute; 46 | bottom: 0; 47 | width: 100%; 48 | } 49 | 50 | footer .qrcode { 51 | text-align: center; 52 | } 53 | 54 | footer .detail-info{ 55 | border-left: 1px solid #888; 56 | } 57 | 58 | footer .company-info li { 59 | padding-left: 16px; 60 | margin: 4px 0; 61 | } 62 | 63 | .main { 64 | padding: 20px 0; 65 | padding-bottom: 200px; 66 | } 67 | 68 | .main .container { 69 | background: #fff; 70 | padding: 20px; 71 | } 72 | 73 | i { 74 | font-style: normal; 75 | } 76 | 77 | u { 78 | text-decoration: none; 79 | } 80 | 81 | b { 82 | font-weight: normal; 83 | } 84 | 85 | a { 86 | color: #000; 87 | } 88 | 89 | a:hover { 90 | text-decoration: none; 91 | } 92 | 93 | .font-size-12 { 94 | font-size: 12px; 95 | } 96 | .font-size-14 { 97 | font-size: 14px; 98 | } 99 | .font-size-16 { 100 | font-size: 16px; 101 | } 102 | .font-size-18 { 103 | font-size: 18px; 104 | } 105 | .font-size-20 { 106 | font-size: 20px; 107 | } 108 | .font-size-22 { 109 | font-size: 20px; 110 | } 111 | .font-size-24 { 112 | font-size: 20px; 113 | } 114 | 115 | .hidden { 116 | display: none; 117 | } 118 | 119 | .rt-0 { 120 | right: 0; 121 | top: 0; 122 | } 123 | 124 | .square { 125 | display: inline-block; 126 | width: 7px; 127 | height: 7px; 128 | background: #ff6547; 129 | margin-bottom: 2px; 130 | margin-right: 3px; 131 | } 132 | 133 | .bg-gray { 134 | background: #eff0f2; 135 | } 136 | 137 | .user-header { 138 | width: 50px; 139 | height: 50px; 140 | } 141 | 142 | em { 143 | font-style: normal; 144 | color: red; 145 | } 146 | -------------------------------------------------------------------------------- /src/main/resources/mapper/discusspost-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | id,user_id, title, content, type, status, create_time, comment_count, score 9 | 10 | 11 | user_id, title, content, type, status, create_time, comment_count, score 12 | 13 | 14 | 15 | 30 | 38 | 39 | 40 | INSERT INTO discuss_post (user_id, title, content, type, status, create_time, comment_count, score) 41 | VALUES (#{userId}, #{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score}) 42 | 43 | 44 | 49 | 50 | 51 | UPDATE discuss_post SET comment_count = #{commentCount} WHERE id = #{id} 52 | 53 | 54 | 55 | UPDATE discuss_post SET type = #{type} WHERE id = #{id} 56 | 57 | 58 | UPDATE discuss_post SET status = #{status} WHERE id = #{id} 59 | 60 | 61 | 62 | UPDATE discuss_post SET score = #{score} WHERE id = #{id} 63 | 64 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/TestMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 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 | import org.springframework.test.context.ContextConfiguration; 7 | import tech.turl.community.dao.DiscussPostMapper; 8 | import tech.turl.community.dao.UserMapper; 9 | import tech.turl.community.entity.DiscussPost; 10 | import tech.turl.community.entity.User; 11 | 12 | import java.util.Date; 13 | import java.util.List; 14 | 15 | @SpringBootTest 16 | @ContextConfiguration(classes = CommunityApplication.class) 17 | public class TestMapper { 18 | @Autowired private UserMapper userMapper; 19 | 20 | @Test 21 | public void testSelectUser() { 22 | User user = userMapper.selectById(101); 23 | System.out.println(user); 24 | 25 | user = userMapper.selectByName("liubei"); 26 | System.out.println(user); 27 | 28 | user = userMapper.selectByEmail("nowcoder101@sina.com"); 29 | System.out.println(user); 30 | } 31 | 32 | @Test 33 | public void testInsertUser() { 34 | User user = new User(); 35 | user.setUsername("test"); 36 | user.setPassword("123456"); 37 | user.setSalt("abc"); 38 | user.setEmail("test@qq.com"); 39 | user.setHeaderUrl("http://www.abc.com/1.png"); 40 | user.setCreateTime(new Date()); 41 | int rows = userMapper.insertUser(user); 42 | System.out.println(rows); 43 | System.out.println(user.getId()); 44 | } 45 | 46 | @Test 47 | public void updateUser() { 48 | int rows = userMapper.updateStatus(150, 1); 49 | System.out.println(rows); 50 | 51 | rows = userMapper.updateHeader(150, "png"); 52 | System.out.println(rows); 53 | 54 | rows = userMapper.updatePassword(150, "hello"); 55 | System.out.println(rows); 56 | } 57 | 58 | @Test 59 | public void test() { 60 | System.out.println(Integer.parseInt("-001111", 10)); 61 | } 62 | 63 | @Autowired private DiscussPostMapper discussPostMapper; 64 | 65 | @Test 66 | public void testSelectPosts() { 67 | List list = discussPostMapper.selectDiscussPosts(0, 0, 10, 0); 68 | for (DiscussPost post : list) { 69 | System.out.println(post); 70 | } 71 | int count = discussPostMapper.selectDiscussPostRows(0); 72 | System.out.println(count); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.apache.ibatis.annotations.Insert; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | import org.apache.ibatis.annotations.Select; 7 | import tech.turl.community.entity.Comment; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * @author zhengguohuang 13 | * @date 2021/03/17 14 | */ 15 | @Mapper 16 | public interface CommentMapper { 17 | /** 18 | * 分页查询评论 19 | * 20 | * @param 21 | * @return 22 | */ 23 | @Select({ 24 | "select id, user_id, entity_type, entity_id, target_id, content, status, create_time", 25 | "from comment", 26 | "where status=0 and entity_type = #{entityType} and entity_id = #{entityId}", 27 | "order by create_time asc", 28 | "limit #{offset}, #{limit}" 29 | }) 30 | /** 31 | * 查询评论 32 | * @param entityType 实体类型 33 | * @param entityId 实体Id 34 | * @param limit 35 | * @param offset 36 | * @return 一个分页 37 | */ 38 | List selectCommentsByEntity(@Param("entityType") int entityType, @Param("entityId") int entityId, 39 | @Param("offset") int offset, @Param("limit") int limit); 40 | 41 | /** 42 | * 查询评论总数 43 | * 44 | * @param entityType 45 | * @param entityId 46 | * @return 47 | */ 48 | @Select({ 49 | "select count(id)", 50 | "from comment", 51 | "where status=0 and entity_type = #{entityType} and entity_id = #{entityId}" 52 | }) 53 | int selectCountByEntity(@Param("entityType") int entityType, @Param("entityId") int entityId); 54 | 55 | 56 | /** 57 | * 增加评论 58 | * 59 | * @param comment 评论对象 60 | * @return 影响的行数 61 | */ 62 | @Insert({ 63 | "insert into comment (user_id, entity_type, entity_id, target_id, content, status, create_time)", 64 | "values (#{userId}, #{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime})" 65 | }) 66 | int insertComment(Comment comment); 67 | 68 | /** 69 | * 通过id查找评论 70 | * 71 | * @param id 评论id 72 | * @return 评论 73 | */ 74 | @Select({ 75 | "select id, user_id, entity_type, entity_id, target_id, content, status, create_time", 76 | "from comment", 77 | "where id = #{id}", 78 | }) 79 | Comment selectCommentsById(@Param("id") int id); 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.ui.Model; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import tech.turl.community.entity.DiscussPost; 8 | import tech.turl.community.entity.Page; 9 | import tech.turl.community.service.ElasticsearchService; 10 | import tech.turl.community.service.LikeService; 11 | import tech.turl.community.service.UserService; 12 | import tech.turl.community.util.CommunityConstant; 13 | 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author zhengguohuang 21 | * @date 2021/03/26 22 | */ 23 | @Controller 24 | public class SearchController implements CommunityConstant { 25 | 26 | @Autowired private ElasticsearchService elasticsearchService; 27 | 28 | @Autowired private UserService userService; 29 | 30 | @Autowired private LikeService likeService; 31 | 32 | /** 33 | * 搜索帖子 34 | * 35 | * @param keyword 36 | * @param page 37 | * @param model 38 | * @return 39 | */ 40 | @GetMapping("/search") 41 | public String search(String keyword, Page page, Model model) { 42 | // 搜索帖子 43 | org.springframework.data.domain.Page searchResult = 44 | elasticsearchService.searchDiscussPost( 45 | keyword, page.getCurrent() - 1, page.getLimit()); 46 | // 聚合数据 47 | List> discussPosts = new ArrayList<>(); 48 | if (searchResult != null) { 49 | for (DiscussPost post : searchResult) { 50 | Map map = new HashMap<>(16); 51 | // 帖子 52 | map.put("post", post); 53 | // 作者 54 | map.put("user", userService.findUserById(post.getUserId())); 55 | // 点赞数量 56 | map.put( 57 | "likeCount", 58 | likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId())); 59 | discussPosts.add(map); 60 | } 61 | } 62 | model.addAttribute("discussPosts", discussPosts); 63 | model.addAttribute("keyword", keyword); 64 | page.setPath("/search?keyword=" + keyword); 65 | page.setRows(searchResult == null ? 0 : (int) searchResult.getTotalElements()); 66 | return "site/search"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Isolation; 6 | import org.springframework.transaction.annotation.Propagation; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import org.springframework.web.util.HtmlUtils; 9 | import tech.turl.community.dao.CommentMapper; 10 | import tech.turl.community.entity.Comment; 11 | import tech.turl.community.util.CommunityConstant; 12 | import tech.turl.community.util.SensitiveFilter; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * @author zhengguohuang 18 | * @date 2021/03/17 19 | */ 20 | @Service 21 | public class CommentService implements CommunityConstant { 22 | @Autowired private CommentMapper commentMapper; 23 | 24 | @Autowired private SensitiveFilter sensitiveFilter; 25 | 26 | @Autowired private DiscussPostService discussPostService; 27 | 28 | public List findCommentsByEntity(int entityType, int entityId, int offset, int limit) { 29 | return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit); 30 | } 31 | 32 | public int findCommentCount(int entityType, int entityId) { 33 | return commentMapper.selectCountByEntity(entityType, entityId); 34 | } 35 | 36 | /** 37 | * 增加评论 38 | * 39 | * @param comment 40 | * @return 41 | */ 42 | @Transactional( 43 | isolation = Isolation.READ_COMMITTED, 44 | propagation = Propagation.REQUIRED, 45 | rollbackFor = Exception.class) 46 | public int addComment(Comment comment) { 47 | if (comment == null) { 48 | throw new IllegalArgumentException("参数不能为空!"); 49 | } 50 | // 添加评论 51 | comment.setContent(HtmlUtils.htmlEscape(comment.getContent())); 52 | comment.setContent(sensitiveFilter.filter(comment.getContent())); 53 | int rows = commentMapper.insertComment(comment); 54 | 55 | // 更新帖子数量 56 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 57 | int count = 58 | commentMapper.selectCountByEntity( 59 | comment.getEntityType(), comment.getEntityId()); 60 | discussPostService.updateCommentCount(comment.getEntityId(), count); 61 | } 62 | 63 | return rows; 64 | } 65 | 66 | public Comment findCommentsById(int entityId) { 67 | return commentMapper.selectCommentsById(entityId); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/CommunityApplicationTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.BeansException; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import tech.turl.community.dao.AlphaDao; 12 | import tech.turl.community.service.AlphaService; 13 | 14 | import java.text.SimpleDateFormat; 15 | import java.util.Date; 16 | 17 | @SpringBootTest 18 | @ContextConfiguration(classes = CommunityApplication.class) 19 | class CommunityApplicationTests implements ApplicationContextAware { 20 | private ApplicationContext applicationContext; 21 | @Test 22 | void contextLoads() { 23 | } 24 | 25 | @Override 26 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 27 | this.applicationContext = applicationContext; 28 | //System.out.println(applicationContext); 29 | } 30 | 31 | @Test 32 | public void testApplicationContex(){ 33 | System.out.println(applicationContext); 34 | AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class); 35 | System.out.println(alphaDao.select()); 36 | 37 | alphaDao=applicationContext.getBean("alphaDaoHibernate", AlphaDao.class); 38 | System.out.println(alphaDao.select()); 39 | } 40 | @Test 41 | public void testBeanManagement(){ 42 | AlphaService alphaService = applicationContext.getBean(AlphaService.class); 43 | System.out.println(alphaService); 44 | alphaService = applicationContext.getBean(AlphaService.class); 45 | System.out.println(alphaService); 46 | /** 47 | * 结果为 48 | * tech.turl.community.service.AlphaService@44c13103 49 | * tech.turl.community.service.AlphaService@44c13103 50 | */ 51 | } 52 | 53 | @Test 54 | public void testBeanConfig(){ 55 | SimpleDateFormat simpleDateFormat = applicationContext.getBean(SimpleDateFormat.class); 56 | System.out.println(simpleDateFormat.format(new Date())); 57 | } 58 | 59 | 60 | @Autowired 61 | private AlphaService alphaService; 62 | @Autowired 63 | private SimpleDateFormat simpleDateFormat; 64 | @Autowired 65 | @Qualifier("alphaDaoHibernate") 66 | private AlphaDao alphaDao; 67 | @Test 68 | public void testDI(){ 69 | System.out.println(alphaService); 70 | System.out.println(simpleDateFormat); 71 | System.out.println(alphaDao.select()); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.ui.Model; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import tech.turl.community.entity.DiscussPost; 9 | import tech.turl.community.entity.Page; 10 | import tech.turl.community.entity.User; 11 | import tech.turl.community.service.DiscussPostService; 12 | import tech.turl.community.service.LikeService; 13 | import tech.turl.community.service.UserService; 14 | import tech.turl.community.util.CommunityConstant; 15 | 16 | import java.util.ArrayList; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author zhengguohuang 23 | * @date 2021/3/15 24 | */ 25 | @Controller 26 | public class HomeController implements CommunityConstant { 27 | @Autowired private DiscussPostService discussPostService; 28 | @Autowired private UserService userService; 29 | 30 | @Autowired private LikeService likeService; 31 | 32 | @GetMapping("/") 33 | public String getIndexPage( 34 | Model model, 35 | Page page, 36 | @RequestParam(name = "orderMode", defaultValue = "0") int orderMode) { 37 | // 方法调用栈,SpringMVC会自动实例化Model和Page,并将Page注入Model 38 | // 所以,在thymeleaf中可以直接访问Page对象中的数据 39 | page.setRows(discussPostService.findDiscussPostRows(0)); 40 | page.setPath("/?orderMode=" + orderMode); 41 | List list = 42 | discussPostService.findDiscussPosts( 43 | 0, page.getOffset(), page.getLimit(), orderMode); 44 | List> discussPosts = new ArrayList<>(); 45 | if (list != null) { 46 | for (DiscussPost post : list) { 47 | Map map = new HashMap<>(16); 48 | map.put("post", post); 49 | User user = userService.findUserById(post.getUserId()); 50 | map.put("user", user); 51 | 52 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()); 53 | map.put("likeCount", likeCount); 54 | 55 | discussPosts.add(map); 56 | } 57 | } 58 | model.addAttribute("discussPosts", discussPosts); 59 | model.addAttribute("orderMode", orderMode); 60 | return "index"; 61 | } 62 | 63 | @GetMapping("/error") 64 | public String getErrorPage() { 65 | return "error/500"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/LikeController.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.ResponseBody; 8 | import tech.turl.community.entity.Event; 9 | import tech.turl.community.entity.User; 10 | import tech.turl.community.event.EventProducer; 11 | import tech.turl.community.service.LikeService; 12 | import tech.turl.community.util.CommunityConstant; 13 | import tech.turl.community.util.CommunityUtil; 14 | import tech.turl.community.util.HostHolder; 15 | import tech.turl.community.util.RedisKeyUtil; 16 | 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | /** 21 | * @author zhengguohuang 22 | * @date 2021/03/21 23 | */ 24 | @Controller 25 | public class LikeController implements CommunityConstant { 26 | @Autowired private HostHolder hostHolder; 27 | 28 | @Autowired private LikeService likeService; 29 | 30 | @Autowired private EventProducer eventProducer; 31 | @Autowired private RedisTemplate redisTemplate; 32 | 33 | /** 34 | * 点赞 35 | * 36 | * @param entityType 37 | * @param entityId 38 | * @return 39 | */ 40 | @PostMapping("/like") 41 | @ResponseBody 42 | public String like(int entityType, int entityId, int entityUserId, int postId) { 43 | User user = hostHolder.getUser(); 44 | // 点赞 45 | likeService.like(user.getId(), entityType, entityId, entityUserId); 46 | // 数量 47 | long likeCount = likeService.findEntityLikeCount(entityType, entityId); 48 | // 状态 49 | int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId); 50 | // 返回的结果 51 | Map map = new HashMap<>(16); 52 | map.put("likeCount", likeCount); 53 | map.put("likeStatus", likeStatus); 54 | // 触发事件 55 | if (likeStatus == 1) { 56 | Event event = 57 | new Event() 58 | .setTopic(TOPIC_LIKE) 59 | .setUserId(user.getId()) 60 | .setEntityType(entityType) 61 | .setEntityId(entityId) 62 | .setEntityUserId(entityUserId) 63 | .setData("postId", postId); 64 | eventProducer.fireEvent(event); 65 | } 66 | if (entityType == ENTITY_TYPE_POST) { 67 | // 计算帖子的分数 68 | String redisKey = RedisKeyUtil.getPostScoreKey(); 69 | redisTemplate.opsForSet().add(redisKey, postId); 70 | } 71 | return CommunityUtil.getJSONString(0, null, map); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/User.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class User { 6 | private int id; 7 | private String username; 8 | private String password; 9 | private String salt; 10 | private String email; 11 | private int type; 12 | private int status; 13 | private String activationCode; 14 | private String headerUrl; 15 | private Date createTime; 16 | 17 | public int getId() { 18 | return id; 19 | } 20 | 21 | public void setId(int id) { 22 | this.id = id; 23 | } 24 | 25 | public String getUsername() { 26 | return username; 27 | } 28 | 29 | public void setUsername(String username) { 30 | this.username = username; 31 | } 32 | 33 | public String getPassword() { 34 | return password; 35 | } 36 | 37 | public void setPassword(String password) { 38 | this.password = password; 39 | } 40 | 41 | public String getSalt() { 42 | return salt; 43 | } 44 | 45 | public void setSalt(String salt) { 46 | this.salt = salt; 47 | } 48 | 49 | public String getEmail() { 50 | return email; 51 | } 52 | 53 | public void setEmail(String email) { 54 | this.email = email; 55 | } 56 | 57 | public int getType() { 58 | return type; 59 | } 60 | 61 | public void setType(int type) { 62 | this.type = type; 63 | } 64 | 65 | public int getStatus() { 66 | return status; 67 | } 68 | 69 | public void setStatus(int status) { 70 | this.status = status; 71 | } 72 | 73 | public String getActivationCode() { 74 | return activationCode; 75 | } 76 | 77 | public void setActivationCode(String activationCode) { 78 | this.activationCode = activationCode; 79 | } 80 | 81 | public String getHeaderUrl() { 82 | return headerUrl; 83 | } 84 | 85 | public void setHeaderUrl(String headerUrl) { 86 | this.headerUrl = headerUrl; 87 | } 88 | 89 | public Date getCreateTime() { 90 | return createTime; 91 | } 92 | 93 | public void setCreateTime(Date createTime) { 94 | this.createTime = createTime; 95 | } 96 | 97 | @Override 98 | public String toString() { 99 | return "User{" + 100 | "id=" + id + 101 | ", username='" + username + '\'' + 102 | ", password='" + password + '\'' + 103 | ", salt='" + salt + '\'' + 104 | ", email='" + email + '\'' + 105 | ", type=" + type + 106 | ", status=" + status + 107 | ", activationCode='" + activationCode + '\'' + 108 | ", headerUrl='" + headerUrl + '\'' + 109 | ", createTime=" + createTime + 110 | '}'; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/interceptor/LoginTicketInterceptor.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller.interceptor; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.security.core.context.SecurityContextImpl; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | import org.springframework.web.servlet.ModelAndView; 11 | import tech.turl.community.entity.LoginTicket; 12 | import tech.turl.community.entity.User; 13 | import tech.turl.community.service.UserService; 14 | import tech.turl.community.util.CookieUtil; 15 | import tech.turl.community.util.HostHolder; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.util.Date; 20 | 21 | /** 22 | * @author zhengguohuang 23 | * @date 2021/3/15 24 | */ 25 | @Component 26 | public class LoginTicketInterceptor implements HandlerInterceptor { 27 | @Autowired private UserService userService; 28 | 29 | @Autowired private HostHolder hostHolder; 30 | 31 | @Override 32 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 33 | throws Exception { 34 | // 从cookie中获取凭证 35 | String ticket = CookieUtil.getValue(request, "ticket"); 36 | 37 | if (ticket != null) { 38 | // 查询凭证 39 | LoginTicket loginTicket = userService.findLoginTicket(ticket); 40 | // 检查凭证是否有效 41 | if (loginTicket != null 42 | && loginTicket.getStatus() == 0 43 | && loginTicket.getExpired().after(new Date())) { 44 | // 根据凭证查询用户 45 | User user = userService.findUserById(loginTicket.getUserId()); 46 | // 在本次请求中持有用户 47 | hostHolder.setUser(user); 48 | // 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权. 49 | Authentication authentication = 50 | new UsernamePasswordAuthenticationToken( 51 | user, user.getPassword(), userService.getAuthorities(user.getId())); 52 | SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | 59 | @Override 60 | public void postHandle( 61 | HttpServletRequest request, 62 | HttpServletResponse response, 63 | Object handler, 64 | ModelAndView modelAndView) 65 | throws Exception { 66 | User user = hostHolder.getUser(); 67 | if (user != null && modelAndView != null) { 68 | modelAndView.addObject("loginUser", user); 69 | } 70 | } 71 | 72 | @Override 73 | public void afterCompletion( 74 | HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 75 | throws Exception { 76 | hostHolder.clear(); 77 | // SecurityContextHolder.clearContext(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/service/LikeService.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.dao.DataAccessException; 5 | import org.springframework.data.redis.core.RedisOperations; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.core.SessionCallback; 8 | import org.springframework.stereotype.Service; 9 | import tech.turl.community.util.RedisKeyUtil; 10 | 11 | /** 12 | * @author zhengguohuang 13 | * @date 2021/03/21 14 | */ 15 | @Service 16 | public class LikeService { 17 | @Autowired 18 | private RedisTemplate redisTemplate; 19 | 20 | /** 21 | * 点赞 22 | * 23 | * @param userId 24 | * @param entityType 25 | * @param entityId 26 | */ 27 | public void like(int userId, int entityType, int entityId, int entityUserId) { 28 | 29 | redisTemplate.execute(new SessionCallback() { 30 | @Override 31 | public Object execute(RedisOperations operations) throws DataAccessException { 32 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 33 | String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId); 34 | 35 | boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId); 36 | 37 | operations.multi(); 38 | if (isMember) { 39 | redisTemplate.opsForSet().remove(entityLikeKey, userId); 40 | operations.opsForValue().decrement(userLikeKey); 41 | 42 | } else { 43 | redisTemplate.opsForSet().add(entityLikeKey, userId); 44 | operations.opsForValue().increment(userLikeKey); 45 | } 46 | return operations.exec(); 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * 获取点赞数量 53 | * 54 | * @param entityType 55 | * @param entityId 56 | * @return 57 | */ 58 | public long findEntityLikeCount(int entityType, int entityId) { 59 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 60 | return redisTemplate.opsForSet().size(entityLikeKey); 61 | } 62 | 63 | /** 64 | * 查询某人对某实体的点赞状态 65 | * 1表示已点赞 66 | * 67 | * @param userId 68 | * @param entityType 69 | * @param entityId 70 | * @return 71 | */ 72 | public int findEntityLikeStatus(int userId, int entityType, int entityId) { 73 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 74 | return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0; 75 | } 76 | 77 | 78 | /** 79 | * 查询某个用户获得的赞 80 | * 81 | * @param userId 82 | * @return 83 | */ 84 | public int findUserLikeCount(int userId) { 85 | String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); 86 | Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey); 87 | return count == null ? 0 : count; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/dao/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.dao; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.apache.ibatis.annotations.Param; 5 | import tech.turl.community.entity.Message; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author zhengguohuang 11 | * @date 2021/03/19 12 | */ 13 | @Mapper 14 | public interface MessageMapper { 15 | /** 16 | * 查询当前用户的会话列表,针对每个会话只返回一条最新的私信 17 | * 18 | * @param userId 19 | * @param offset 20 | * @param limit 21 | * @return 22 | */ 23 | List selectConversations(@Param("userId") int userId, @Param("offset") int offset, @Param("limit") int limit); 24 | 25 | /** 26 | * 查询当前用户的会话数量 27 | * 28 | * @param userId 29 | * @return 30 | */ 31 | int selectConversationCount(@Param("userId") int userId); 32 | 33 | /** 34 | * 查询某个会话所包含的私信列表 35 | * 36 | * @param conversationId 37 | * @param offset 38 | * @param limit 39 | * @return 40 | */ 41 | List selectLetters(@Param("conversationId") String conversationId, @Param("offset") int offset, @Param("limit") int limit); 42 | 43 | /** 44 | * 查询某个会话包含的私信数量 45 | * 46 | * @param conversationId 47 | * @return 48 | */ 49 | int selectLetterCount(@Param("conversationId") String conversationId); 50 | 51 | /** 52 | * 查询未读私信数量 53 | * 54 | * @param userId 55 | * @param conversationId 56 | * @return 57 | */ 58 | int selectLetterUnreadCount(@Param("userId") int userId, @Param("conversationId") String conversationId); 59 | 60 | /** 61 | * 新增消息 62 | * 63 | * @param message 64 | * @return 65 | */ 66 | int insertMessage(Message message); 67 | 68 | /** 69 | * 修改消息状态 70 | * 71 | * @param ids 72 | * @param status 73 | * @return 74 | */ 75 | int updateStatus(@Param("ids") List ids, @Param("status") int status); 76 | 77 | /** 78 | * 查询某个主题下最新的通知 79 | * 80 | * @param userId 81 | * @param topic 82 | * @return 83 | */ 84 | Message selectLatesNotice(@Param("userId") int userId, @Param("topic") String topic); 85 | 86 | /** 87 | * 查询某个主题下最新的通知 88 | * 89 | * @param userId 90 | * @param topic 91 | * @return 92 | */ 93 | int selectNoticeCount(@Param("userId") int userId, @Param("topic") String topic); 94 | 95 | /** 96 | * 查询未读的通知的数量 97 | * 98 | * @param userId 99 | * @param topic 100 | * @return 101 | */ 102 | int selectNoticeUnreadCount(@Param("userId") int userId, @Param("topic") String topic); 103 | 104 | /** 105 | * 查询某个主题所包含的通知列表 106 | * 107 | * @param userId 108 | * @param topic 109 | * @param offset 110 | * @param limit 111 | * @return 112 | */ 113 | List selectNotices(@Param("userId") int userId, @Param("topic") String topic, @Param("offset") int offset, @Param("limit") int limit); 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.web.util.HtmlUtils; 6 | import tech.turl.community.dao.MessageMapper; 7 | import tech.turl.community.entity.Message; 8 | import tech.turl.community.util.SensitiveFilter; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/03/19 15 | */ 16 | @Service 17 | public class MessageService { 18 | @Autowired 19 | private MessageMapper messageMapper; 20 | 21 | @Autowired 22 | private SensitiveFilter sensitiveFilter; 23 | 24 | public List findConversations(int userId, int offset, int limit) { 25 | return messageMapper.selectConversations(userId, offset, limit); 26 | } 27 | 28 | public int findConversationCount(int userId) { 29 | return messageMapper.selectConversationCount(userId); 30 | } 31 | 32 | public List findLetters(String conversationId, int offset, int limit) { 33 | return messageMapper.selectLetters(conversationId, offset, limit); 34 | } 35 | 36 | public int findLetterCount(String conversationId) { 37 | return messageMapper.selectLetterCount(conversationId); 38 | } 39 | 40 | public int findLetterUnreadCount(int userId, String conversationId) { 41 | return messageMapper.selectLetterUnreadCount(userId, conversationId); 42 | } 43 | 44 | /** 45 | * 添加消息 46 | * 47 | * @param message 48 | * @return 49 | */ 50 | public int addMessage(Message message) { 51 | message.setContent(HtmlUtils.htmlEscape(message.getContent())); 52 | message.setContent(sensitiveFilter.filter(message.getContent())); 53 | return messageMapper.insertMessage(message); 54 | } 55 | 56 | /** 57 | * 读取消息 58 | * 59 | * @param ids 60 | * @return 61 | */ 62 | public int readMessage(List ids) { 63 | return messageMapper.updateStatus(ids, 1); 64 | } 65 | 66 | 67 | /** 68 | * 查找最近的通知 69 | * 70 | * @param userId 71 | * @param topic 72 | * @return 73 | */ 74 | public Message findLatestNotice(int userId, String topic) { 75 | return messageMapper.selectLatesNotice(userId, topic); 76 | } 77 | 78 | /** 79 | * 查询消息数量 80 | * 81 | * @param userId 82 | * @param topic 83 | * @return 84 | */ 85 | public int findNoticeCount(int userId, String topic) { 86 | return messageMapper.selectNoticeCount(userId, topic); 87 | } 88 | 89 | /** 90 | * 查询未读消息数量 91 | * 92 | * @param userId 93 | * @param topic 94 | * @return 95 | */ 96 | public int findNoticeUnreadCount(int userId, String topic) { 97 | return messageMapper.selectNoticeUnreadCount(userId, topic); 98 | } 99 | 100 | public List findNotices(int userId, String topic, int offset, int limit) { 101 | return messageMapper.selectNotices(userId, topic, offset, limit); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/wkhtmltopdf/HtmlToPdf.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.wkhtmltopdf; 2 | 3 | import java.io.File; 4 | 5 | public class HtmlToPdf { 6 | // wkhtmltopdf path in the system 7 | private static final String toPdfTool = "e:/wkhtmltopdf/bin/wkhtmltoimage.exe"; 8 | 9 | /** 10 | * html to pdf 11 | * 12 | * @param srcPath html path, which can be a path on the hard disk or a network path 13 | * @param destPath pdf save path 14 | * @return returns true if the conversion is successful 15 | */ 16 | public static boolean convert(String srcPath, String destPath) { 17 | File file = new File(destPath); 18 | File parent = file.getParentFile(); 19 | // If the pdf save path does not exist, create the path 20 | if (!parent.exists()) { 21 | parent.mkdirs(); 22 | } 23 | StringBuilder cmd = new StringBuilder(); 24 | if (System.getProperty("os.name").indexOf("Windows") == -1) { 25 | // Non-windows system 26 | // toPdfTool = FileUtil.convertSystemFilePath("/home/ubuntu/wkhtmltox/bin/wkhtmltopdf"); 27 | } 28 | cmd.append(toPdfTool); 29 | cmd.append(" "); 30 | /* 31 | cmd.append(" --header-line");//The line below the header 32 | //cmd.append(" --header-center Here is the header, here is the header, here is the header, here is the header ");//The middle content of the header 33 | cmd.append(" --margin-top 3cm ");//Set the top margin of the page (default 10mm) 34 | cmd.append(" --header-html file:///"+WebUtil.getServletContext().getRealPath("")+FileUtil.convertSystemFilePath("\\style\\pdf\\head.html"));/ / (Add an HTML header, followed by the URL) 35 | cmd.append(" --header-spacing 5 ");// (Set the distance between header and content, default 0) 36 | //cmd.append(" --footer-center (footer content set in the center position)");//footer content set in the center position 37 | cmd.append(" --footer-html file:///"+WebUtil.getServletContext().getRealPath("")+FileUtil.convertSystemFilePath("\\style\\pdf\\foter.html"));/ / (Add an HTML footer followed by the URL) 38 | cmd.append(" --footer-line");//* display a line on the footer content) 39 | cmd.append(" --footer-spacing 5 ");// (Set the distance between the footer and the content) 40 | */ 41 | cmd.append(srcPath); 42 | cmd.append(" "); 43 | cmd.append(destPath); 44 | 45 | boolean result = true; 46 | try { 47 | Process proc = Runtime.getRuntime().exec(cmd.toString()); 48 | HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream()); 49 | HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream()); 50 | error.start(); 51 | output.start(); 52 | proc.waitFor(); 53 | } catch (Exception e) { 54 | result = false; 55 | e.printStackTrace(); 56 | } 57 | 58 | return result; 59 | } 60 | 61 | public static void main(String[] args) { 62 | long start = System.currentTimeMillis(); 63 | HtmlToPdf.convert("http://www.baidu.com", "d:\\test1.png"); 64 | System.out.println(System.currentTimeMillis() - start); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/quartz/PostScoreRefreshJob.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.quartz; 2 | 3 | import org.quartz.Job; 4 | import org.quartz.JobExecutionContext; 5 | import org.quartz.JobExecutionException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.redis.core.BoundSetOperations; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | import tech.turl.community.entity.DiscussPost; 12 | import tech.turl.community.service.DiscussPostService; 13 | import tech.turl.community.service.ElasticsearchService; 14 | import tech.turl.community.service.LikeService; 15 | import tech.turl.community.util.CommunityConstant; 16 | import tech.turl.community.util.RedisKeyUtil; 17 | 18 | import java.text.ParseException; 19 | import java.text.SimpleDateFormat; 20 | import java.util.Date; 21 | 22 | /** 23 | * @author zhengguohuang 24 | * @date 2021/03/29 25 | */ 26 | public class PostScoreRefreshJob implements Job, CommunityConstant { 27 | 28 | private static final Logger LOGGER = LoggerFactory.getLogger(PostScoreRefreshJob.class); 29 | 30 | @Autowired private RedisTemplate redisTemplate; 31 | 32 | @Autowired private DiscussPostService discussPostService; 33 | 34 | @Autowired private LikeService likeService; 35 | 36 | @Autowired private ElasticsearchService elasticsearchService; 37 | 38 | private static final Date EPOCH; 39 | 40 | static { 41 | try { 42 | EPOCH = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00"); 43 | } catch (ParseException e) { 44 | throw new RuntimeException("初始化牛客纪元失败!", e); 45 | } 46 | } 47 | 48 | @Override 49 | public void execute(JobExecutionContext context) throws JobExecutionException { 50 | 51 | String redisKey = RedisKeyUtil.getPostScoreKey(); 52 | BoundSetOperations operations = redisTemplate.boundSetOps(redisKey); 53 | if (operations.size() == 0) { 54 | LOGGER.info("任务取消,没有需要刷新的帖子!"); 55 | return; 56 | } 57 | LOGGER.info("[任务开始] 正在刷新帖子分数:" + operations.size()); 58 | while (operations.size() > 0) { 59 | this.refresh((Integer) operations.pop()); 60 | } 61 | LOGGER.info("[任务结束] 帖子分数刷新完毕!"); 62 | } 63 | 64 | private void refresh(int postId) { 65 | DiscussPost post = discussPostService.findDiscussPostById(postId); 66 | 67 | if (post == null) { 68 | LOGGER.error("该帖子不存在:id = " + postId); 69 | return; 70 | } 71 | 72 | // 是否精华 73 | boolean wonderful = post.getStatus() == 1; 74 | // 评论数量 75 | int commentCount = post.getCommentCount(); 76 | // 点赞数量 77 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId); 78 | 79 | // 计算权重 80 | double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2; 81 | // 分数 = 帖子权重 + 距离天数 82 | double score = 83 | Math.log10(Math.max(w, 1)) 84 | + (post.getCreateTime().getTime() - EPOCH.getTime()) / (1000 * 3600 * 24); 85 | // 更新帖子分数 86 | discussPostService.updateScore(postId, score); 87 | // 同步搜索数据 88 | post.setScore(score); 89 | elasticsearchService.saveDiscussPost(post); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/entity/DiscussPost.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.entity; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.elasticsearch.annotations.Document; 5 | import org.springframework.data.elasticsearch.annotations.Field; 6 | import org.springframework.data.elasticsearch.annotations.FieldType; 7 | 8 | import java.util.Date; 9 | 10 | /** 11 | * @author zhengguohuang 12 | * @date 2021/03/17 13 | */ 14 | @Document(indexName = "discusspost", shards = 6, replicas = 3) 15 | public class DiscussPost { 16 | @Id 17 | private int id; 18 | @Field(type = FieldType.Integer) 19 | private int userId; 20 | @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 21 | private String title; 22 | @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 23 | private String content; 24 | @Field(type = FieldType.Integer) 25 | private int type; 26 | @Field(type = FieldType.Integer) 27 | private int status; 28 | @Field(type = FieldType.Date) 29 | private Date createTime; 30 | @Field(type = FieldType.Integer) 31 | private int commentCount; 32 | @Field(type = FieldType.Double) 33 | private double score; 34 | 35 | public int getCommentCount() { 36 | return commentCount; 37 | } 38 | 39 | public void setCommentCount(int commentCount) { 40 | this.commentCount = commentCount; 41 | } 42 | 43 | public int getId() { 44 | return id; 45 | } 46 | 47 | public void setId(int id) { 48 | this.id = id; 49 | } 50 | 51 | public int getUserId() { 52 | return userId; 53 | } 54 | 55 | public void setUserId(int userId) { 56 | this.userId = userId; 57 | } 58 | 59 | public String getTitle() { 60 | return title; 61 | } 62 | 63 | public void setTitle(String title) { 64 | this.title = title; 65 | } 66 | 67 | public String getContent() { 68 | return content; 69 | } 70 | 71 | public void setContent(String content) { 72 | this.content = content; 73 | } 74 | 75 | public int getType() { 76 | return type; 77 | } 78 | 79 | public void setType(int type) { 80 | this.type = type; 81 | } 82 | 83 | public int getStatus() { 84 | return status; 85 | } 86 | 87 | public void setStatus(int status) { 88 | this.status = status; 89 | } 90 | 91 | public Date getCreateTime() { 92 | return createTime; 93 | } 94 | 95 | public void setCreateTime(Date createTime) { 96 | this.createTime = createTime; 97 | } 98 | 99 | public double getScore() { 100 | return score; 101 | } 102 | 103 | public void setScore(double score) { 104 | this.score = score; 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "DiscussPost{" + 110 | "id=" + id + 111 | ", userId=" + userId + 112 | ", title='" + title + '\'' + 113 | ", content='" + content + '\'' + 114 | ", type=" + type + 115 | ", status=" + status + 116 | ", createTime=" + createTime + 117 | ", score=" + score + 118 | '}'; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/ShareController.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | import tech.turl.community.entity.Event; 13 | import tech.turl.community.event.EventProducer; 14 | import tech.turl.community.util.CommunityConstant; 15 | import tech.turl.community.util.CommunityUtil; 16 | 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.File; 19 | import java.io.FileInputStream; 20 | import java.io.IOException; 21 | import java.io.OutputStream; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | /** 26 | * @author zhengguohuang 27 | * @date 2021/03/29 28 | */ 29 | @Controller 30 | public class ShareController implements CommunityConstant { 31 | private static final Logger LOGGER = LoggerFactory.getLogger(ShareController.class); 32 | 33 | @Autowired private EventProducer eventProducer; 34 | 35 | @Value("${community.path.domain}") 36 | private String domain; 37 | 38 | @Value("${server.servlet.context-path}") 39 | private String contextPath; 40 | 41 | @Value("${wk.image.storage}") 42 | private String wkImageStorage; 43 | 44 | @Value("${qiniu.bucket.share.url}") 45 | private String shareBucketUrl; 46 | 47 | @GetMapping("/share") 48 | @ResponseBody 49 | public String share(String htmlUrl) { 50 | // 文件名 51 | String fileName = CommunityUtil.generateUUID(); 52 | 53 | // 异步生成长图 54 | Event event = 55 | new Event() 56 | .setTopic(TOPIC_SHARE) 57 | .setData("htmlUrl", htmlUrl) 58 | .setData("fileName", fileName) 59 | .setData("suffix", ".png"); 60 | eventProducer.fireEvent(event); 61 | 62 | // 返回访问路径 63 | Map map = new HashMap<>(16); 64 | // map.put("shareUrl", domain + contextPath + "/share/image/" + fileName); 65 | map.put("shareUrl", shareBucketUrl + "/" + fileName); 66 | return CommunityUtil.getJSONString(0, null, map); 67 | } 68 | 69 | /** 70 | * 废弃(改为从七牛云获取图片) 获取长图 71 | * 72 | * @param fileName 73 | * @param response 74 | */ 75 | @GetMapping("/share/image/{fileName}") 76 | public void getShareImage( 77 | @PathVariable("fileName") String fileName, HttpServletResponse response) { 78 | if (StringUtils.isBlank(fileName)) { 79 | throw new IllegalArgumentException("文件名不能为空"); 80 | } 81 | response.setContentType("image/png"); 82 | File file = new File(wkImageStorage + "/" + fileName + ".png"); 83 | try { 84 | OutputStream os = response.getOutputStream(); 85 | FileInputStream fis = new FileInputStream(file); 86 | byte[] buffer = new byte[1024]; 87 | int b = 0; 88 | while ((b = fis.read(buffer)) != -1) { 89 | os.write(buffer, 0, b); 90 | } 91 | } catch (IOException e) { 92 | LOGGER.error("获取长图失败: " + e.getMessage()); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import tech.turl.community.entity.Comment; 10 | import tech.turl.community.entity.DiscussPost; 11 | import tech.turl.community.entity.Event; 12 | import tech.turl.community.entity.User; 13 | import tech.turl.community.event.EventProducer; 14 | import tech.turl.community.service.CommentService; 15 | import tech.turl.community.service.DiscussPostService; 16 | import tech.turl.community.util.CommunityConstant; 17 | import tech.turl.community.util.HostHolder; 18 | import tech.turl.community.util.RedisKeyUtil; 19 | 20 | import java.util.Date; 21 | 22 | /** 23 | * @author zhengguohuang 24 | * @date 2021/03/19 25 | */ 26 | @Controller 27 | @RequestMapping("/comment") 28 | public class CommentController implements CommunityConstant { 29 | @Autowired private CommentService commentService; 30 | 31 | @Autowired private HostHolder hostHolder; 32 | 33 | @Autowired private EventProducer eventProducer; 34 | 35 | @Autowired private DiscussPostService discussPostService; 36 | 37 | @Autowired private RedisTemplate redisTemplate; 38 | 39 | /** 40 | * 添加评论 41 | * 42 | * @param discussPostId 43 | * @param comment 44 | * @return 45 | */ 46 | @PostMapping("/add/{discussPostId}") 47 | public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) { 48 | User user = hostHolder.getUser(); 49 | comment.setUserId(user.getId()); 50 | comment.setStatus(0); 51 | comment.setCreateTime(new Date()); 52 | commentService.addComment(comment); 53 | 54 | // 触发事件 55 | Event event = 56 | new Event() 57 | .setTopic(TOPIC_COMMENT) 58 | .setUserId(comment.getUserId()) 59 | .setEntityType(comment.getEntityType()) 60 | .setEntityId(comment.getEntityId()) 61 | .setData("postId", discussPostId); 62 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 63 | DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId()); 64 | event.setEntityUserId(target.getUserId()); 65 | } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) { 66 | Comment target = commentService.findCommentsById(comment.getEntityId()); 67 | event.setEntityUserId(target.getUserId()); 68 | } 69 | eventProducer.fireEvent(event); 70 | 71 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 72 | // 触发发帖事件 73 | event = 74 | new Event() 75 | .setTopic(TOPIC_PUBLISH) 76 | .setUserId(comment.getUserId()) 77 | .setEntityType(ENTITY_TYPE_POST) 78 | .setEntityId(discussPostId); 79 | eventProducer.fireEvent(event); 80 | // 计算帖子的分数 81 | String redisKey = RedisKeyUtil.getPostScoreKey(); 82 | redisTemplate.opsForSet().add(redisKey, discussPostId); 83 | } 84 | 85 | return "redirect:/discuss/detail/" + discussPostId; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/QuartzConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.quartz.JobDataMap; 4 | import org.quartz.JobDetail; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.scheduling.quartz.JobDetailFactoryBean; 8 | import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; 9 | import tech.turl.community.quartz.AlphaJob; 10 | import tech.turl.community.quartz.PostScoreRefreshJob; 11 | 12 | /** 13 | * @author zhengguohuang 14 | * @date 2021/03/29 15 | */ 16 | @Configuration 17 | public class QuartzConfig { 18 | 19 | /** 20 | * 配置JobDetail 21 | * 22 | *

23 | * 24 | *

25 | * 26 | *

27 | * 28 | *

FactoryBean可以简化Bean的实例化过程: 29 | * 30 | *

31 | * 32 | *

33 | * 34 | *

35 | * 36 | *

1.通过FactoryBean封装Bean的实例化过程。 37 | * 38 | *

39 | * 40 | *

41 | * 42 | *

43 | * 44 | *

2.将FactoryBean装配到Spring容器里。 45 | * 46 | *

47 | * 48 | *

49 | * 50 | *

51 | * 52 | *

3.将FactoryBean注入给其他的Bean。 53 | * 54 | *

55 | * 56 | *

57 | * 58 | *

59 | * 60 | *

4.该Bean得到的是FactoryBean所管理的对象实例。 61 | * 62 | * @return 63 | */ 64 | // @Bean 65 | public JobDetailFactoryBean alphaJobDetail() { 66 | JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); 67 | factoryBean.setJobClass(AlphaJob.class); 68 | factoryBean.setName("alphaJob"); 69 | factoryBean.setGroup("alphaJobGroup"); 70 | factoryBean.setDurability(true); 71 | factoryBean.setRequestsRecovery(true); 72 | return factoryBean; 73 | } 74 | 75 | /** 76 | * 配置Trigger(SimpleTriggerFactoryBean, CronTriggerFactoryBean) 77 | * 78 | * @param alphaJobDetail 79 | * @return 80 | */ 81 | // @Bean 82 | public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) { 83 | SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); 84 | factoryBean.setJobDetail(alphaJobDetail); 85 | factoryBean.setName("alphaTrigger"); 86 | factoryBean.setGroup("alphaTriggerGroup"); 87 | factoryBean.setRepeatInterval(3000); 88 | factoryBean.setJobDataMap(new JobDataMap()); 89 | 90 | return factoryBean; 91 | } 92 | 93 | /** 94 | * 刷新帖子分数任务 95 | * 96 | * @return 97 | */ 98 | @Bean 99 | public JobDetailFactoryBean postScoreRefreshJobDetail() { 100 | JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); 101 | factoryBean.setJobClass(PostScoreRefreshJob.class); 102 | factoryBean.setName("postScoreRefreshJob"); 103 | factoryBean.setGroup("communityJobGroup"); 104 | factoryBean.setDurability(true); 105 | factoryBean.setRequestsRecovery(true); 106 | return factoryBean; 107 | } 108 | 109 | /** 110 | * @param postScoreRefreshJobDetail 111 | * @return 112 | */ 113 | @Bean 114 | public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) { 115 | SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); 116 | factoryBean.setJobDetail(postScoreRefreshJobDetail); 117 | factoryBean.setName("postScoreRefreshTrigger"); 118 | factoryBean.setGroup("communityTriggerGroup"); 119 | factoryBean.setRepeatInterval(1000 * 60 * 5); 120 | factoryBean.setJobDataMap(new JobDataMap()); 121 | return factoryBean; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/service/DataService.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.redis.connection.RedisStringCommands; 5 | import org.springframework.data.redis.core.RedisCallback; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.stereotype.Service; 8 | import tech.turl.community.util.RedisKeyUtil; 9 | 10 | import java.text.SimpleDateFormat; 11 | import java.util.ArrayList; 12 | import java.util.Calendar; 13 | import java.util.Date; 14 | import java.util.List; 15 | 16 | /** 17 | * @author zhengguohuang 18 | * @date 2021/03/29 19 | */ 20 | @Service 21 | public class DataService { 22 | @Autowired private RedisTemplate redisTemplate; 23 | private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd"); 24 | 25 | /** 26 | * 统计UV,将指定ip计入UV 27 | * 28 | * @param ip 29 | */ 30 | public void recordUV(String ip) { 31 | String redisKey = RedisKeyUtil.getUvKey(df.format(new Date())); 32 | redisTemplate.opsForHyperLogLog().add(redisKey, ip); 33 | } 34 | 35 | /** 36 | * 统计指定日期范围内的UV 37 | * 38 | * @param startDate 39 | * @param endDate 40 | * @return 41 | */ 42 | public long calculateUV(Date startDate, Date endDate) { 43 | if (startDate == null || endDate == null) { 44 | throw new IllegalArgumentException("参数不能为空"); 45 | } 46 | // 整理该日期范围内的Key 47 | List keyList = new ArrayList<>(); 48 | Calendar calendar = Calendar.getInstance(); 49 | calendar.setTime(startDate); 50 | while (!calendar.getTime().after(endDate)) { 51 | String key = RedisKeyUtil.getUvKey(df.format(calendar.getTime())); 52 | keyList.add(key); 53 | calendar.add(Calendar.DATE, 1); 54 | } 55 | // 合并这些数据 56 | String redisKey = RedisKeyUtil.getUvKey(df.format(startDate), df.format(endDate)); 57 | redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray()); 58 | // 统计数据 59 | return redisTemplate.opsForHyperLogLog().size(redisKey); 60 | } 61 | 62 | /** 63 | * 统计DAU,将指定用户计入DAU 64 | * 65 | * @param 66 | */ 67 | public void recordDAU(int userId) { 68 | String redisKey = RedisKeyUtil.getDauKey(df.format(new Date())); 69 | redisTemplate.opsForValue().setBit(redisKey, userId, true); 70 | } 71 | 72 | /** 73 | * 统计指定日期范围内的DAU 74 | * 75 | * @param startDate 76 | * @param endDate 77 | * @return 78 | */ 79 | public long calculateDAU(Date startDate, Date endDate) { 80 | // 整理该日期范围内的Key 81 | List keyList = new ArrayList<>(); 82 | Calendar calendar = Calendar.getInstance(); 83 | calendar.setTime(startDate); 84 | while (!calendar.getTime().after(endDate)) { 85 | String key = RedisKeyUtil.getDauKey(df.format(calendar.getTime())); 86 | keyList.add(key.getBytes()); 87 | calendar.add(Calendar.DATE, 1); 88 | } 89 | // 合并这些数据 90 | String redisKey = RedisKeyUtil.getDauKey(df.format(startDate), df.format(endDate)); 91 | return (long) 92 | redisTemplate.execute( 93 | (RedisCallback) 94 | redisConnection -> { 95 | redisConnection.bitOp( 96 | RedisStringCommands.BitOperation.OR, 97 | redisKey.getBytes(), 98 | keyList.toArray(new byte[0][0])); 99 | return redisConnection.bitCount(redisKey.getBytes()); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/tech/turl/community/ThreadPoolTests.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 9 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import tech.turl.community.service.AlphaService; 12 | 13 | import java.util.Date; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.ScheduledExecutorService; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | /** 20 | * @author zhengguohuang 21 | * @date 2021/03/29 22 | */ 23 | @SpringBootTest 24 | @ContextConfiguration(classes = CommunityApplication.class) 25 | public class ThreadPoolTests { 26 | private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolTests.class); 27 | // 1.JDK普通线程 28 | private ExecutorService executorService = Executors.newFixedThreadPool(5); 29 | 30 | // 2.JDK可执行定时任务的线程池 31 | private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); 32 | 33 | // 3.Spring普通线程池 34 | @Autowired private ThreadPoolTaskExecutor taskExecutor; 35 | 36 | // 4.Spring可执行定时任务的线程池 37 | @Autowired private ThreadPoolTaskScheduler taskScheduler; 38 | 39 | @Autowired private AlphaService alphaService; 40 | 41 | /** 42 | * 封装的休眠函数 43 | * 44 | * @param m 45 | */ 46 | private void sleep(long m) { 47 | try { 48 | Thread.sleep(m); 49 | } catch (InterruptedException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | 54 | /** 1.JDK普通线程池执行任务 */ 55 | @Test 56 | public void testExecutorService() { 57 | Worker worker = new Worker("ExecutorService"); 58 | for (int i = 0; i < 10; ++i) { 59 | executorService.execute(worker); 60 | } 61 | sleep(10000); 62 | } 63 | 64 | /** 2.JDK定时任务线程池 */ 65 | @Test 66 | public void testScheduledExecutorService() { 67 | Worker worker = new Worker("ScheduledExecutorService"); 68 | scheduledExecutorService.scheduleAtFixedRate(worker, 10000, 1000, TimeUnit.MILLISECONDS); 69 | sleep(30000); 70 | } 71 | 72 | /** 3.Spring普通线程池 */ 73 | @Test 74 | public void testThreadPoolTaskExecutor() { 75 | Worker worker = new Worker("ThreadPoolTaskExecutor"); 76 | for (int i = 0; i < 10; i++) { 77 | taskExecutor.submit(worker); 78 | } 79 | sleep(10000); 80 | } 81 | 82 | /** 4.Spring定时线程池 */ 83 | @Test 84 | public void testThreadPoolTaskScheduler() { 85 | Worker worker = new Worker("ThreadPoolTaskScheduler"); 86 | Date startTime = new Date(System.currentTimeMillis() + 10_000); 87 | taskScheduler.scheduleAtFixedRate(worker, startTime, 1_000); 88 | sleep(30_000); 89 | } 90 | 91 | /** 5.Spring普通线程池,简单版本 */ 92 | @Test 93 | public void testThreadPoolTaskExecutorSimple() { 94 | for (int i = 0; i < 10; i++) { 95 | alphaService.execute1(); 96 | } 97 | sleep(10_000); 98 | } 99 | 100 | /** 6.Spring定时任务线程池(简化) */ 101 | @Test 102 | public void testThreadPoolTaskSchedulerSimple() { 103 | sleep(30_000); 104 | } 105 | 106 | class Worker implements Runnable { 107 | private String log; 108 | 109 | public Worker(String log) { 110 | this.log = log; 111 | } 112 | 113 | @Override 114 | public void run() { 115 | LOGGER.debug("hello " + log); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/resources/mapper/message-mapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 20 | 29 | 30 | 39 | 46 | 56 | 57 | INSERT INTO message (from_id, to_id, conversation_id, content, status, create_time) 58 | VALUES (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime}) 59 | 60 | 61 | 62 | UPDATE message SET status = #{status} 63 | WHERE id IN 64 | 65 | #{id} 66 | 67 | 68 | 69 | 80 | 87 | 96 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/service/ElasticsearchService.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.service; 2 | 3 | import org.elasticsearch.index.query.QueryBuilders; 4 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; 5 | import org.elasticsearch.search.sort.SortBuilders; 6 | import org.elasticsearch.search.sort.SortOrder; 7 | import org.springframework.beans.BeanUtils; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.PageImpl; 11 | import org.springframework.data.domain.PageRequest; 12 | import org.springframework.data.domain.Pageable; 13 | import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; 14 | import org.springframework.data.elasticsearch.core.SearchHit; 15 | import org.springframework.data.elasticsearch.core.SearchHits; 16 | import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; 17 | import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; 18 | import org.springframework.stereotype.Service; 19 | import tech.turl.community.dao.elasticsearch.DiscussPostRepository; 20 | import tech.turl.community.entity.DiscussPost; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | /** 26 | * @author zhengguohuang 27 | * @date 2021/03/26 28 | */ 29 | @Service 30 | public class ElasticsearchService { 31 | @Autowired private DiscussPostRepository discussPostRepository; 32 | 33 | @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; 34 | 35 | /** 36 | * 插入或修改帖子 37 | * 38 | * @param post 39 | */ 40 | public void saveDiscussPost(DiscussPost post) { 41 | discussPostRepository.save(post); 42 | } 43 | 44 | /** 45 | * 删除帖子 46 | * 47 | * @param id 48 | */ 49 | public void deleteDiscussPost(int id) { 50 | discussPostRepository.deleteById(id); 51 | } 52 | 53 | /** 54 | * 搜索并高亮分页显示 55 | * 56 | * @param keyword 57 | * @param current 58 | * @param limit 59 | * @return 60 | */ 61 | public Page searchDiscussPost(String keyword, int current, int limit) { 62 | Pageable pageable = PageRequest.of(current, limit); 63 | NativeSearchQuery searchQuery = 64 | new NativeSearchQueryBuilder() 65 | .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content")) 66 | .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) 67 | .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)) 68 | .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) 69 | .withPageable(pageable) 70 | .withHighlightFields( 71 | new HighlightBuilder.Field("title") 72 | .preTags("") 73 | .postTags(""), 74 | new HighlightBuilder.Field("content") 75 | .preTags("") 76 | .postTags("")) 77 | .build(); 78 | 79 | SearchHits searchHits = 80 | elasticsearchRestTemplate.search(searchQuery, DiscussPost.class); 81 | if (searchHits.getTotalHits() <= 0) { 82 | return null; 83 | } 84 | List list = new ArrayList<>(); 85 | for (SearchHit hit : searchHits) { 86 | DiscussPost content = hit.getContent(); 87 | DiscussPost post = new DiscussPost(); 88 | BeanUtils.copyProperties(content, post); 89 | List list1 = hit.getHighlightFields().get("title"); 90 | if (list1 != null) { 91 | post.setTitle(list1.get(0)); 92 | } 93 | List list2 = hit.getHighlightFields().get("content"); 94 | if (list2 != null) { 95 | post.setContent(list2.get(0)); 96 | } 97 | list.add(post); 98 | } 99 | return new PageImpl<>(list, pageable, searchHits.getTotalHits()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | /** 4 | * @author zhengguohuang 5 | * @date 2021/03/21 6 | */ 7 | public class RedisKeyUtil { 8 | private static final String SPLIT = ":"; 9 | private static final String PREFIX_ENTITY_LIKE = "like:entity"; 10 | private static final String PREFIX_USER_LIKE = "like:user"; 11 | private static final String PREFIX_FOLLOWEE = "followee"; 12 | private static final String PREFIX_FOLLOWER = "follower"; 13 | private static final String PREFIX_KAPTCHA = "kaptcha"; 14 | private static final String PREFIX_TICKET = "ticket"; 15 | private static final String PREFIX_USER = "user"; 16 | private static final String PREFIX_UV = "uv"; 17 | private static final String PREFIX_DAU = "dau"; 18 | private static final String PREFIX_POST = "post"; 19 | private static final String PREFIX_RATE_LIMIT_PUBLISH = "rate:limit:publish"; 20 | 21 | /** 22 | * 某个实体的赞 like:entity:entityType:entityId->set(userId) 23 | * 24 | * @param entityType 25 | * @param entityId 26 | * @return 27 | */ 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 | * 35 | * @param userId 36 | * @return 37 | */ 38 | public static String getUserLikeKey(int userId) { 39 | return PREFIX_USER_LIKE + SPLIT + userId; 40 | } 41 | 42 | /** 43 | * 某个用户关注的实体 followee:userId:entityType -> zset(entityId, now) 44 | * 45 | * @param userId 46 | * @param entityType 47 | * @return 48 | */ 49 | public static String getFolloweeKey(int userId, int entityType) { 50 | return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; 51 | } 52 | 53 | /** 54 | * 某个实体拥有的粉丝 follower:entityType:entityId -> zset(userId, now) 55 | * 56 | * @param entityType 57 | * @param entityId 58 | * @return 59 | */ 60 | public static String getFollowerKey(int entityType, int entityId) { 61 | return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId; 62 | } 63 | 64 | /** 65 | * 登录验证码 66 | * 67 | * @param owner 服务器发给客户端的验证码 68 | * @return 69 | */ 70 | public static String getKaptchaKey(String owner) { 71 | return PREFIX_KAPTCHA + SPLIT + owner; 72 | } 73 | 74 | /** 75 | * 登录的凭证 76 | * 77 | * @param ticket 78 | * @return 79 | */ 80 | public static String getTicketKey(String ticket) { 81 | return PREFIX_TICKET + SPLIT + ticket; 82 | } 83 | 84 | /** 85 | * 用户 86 | * 87 | * @param userId 88 | * @return 89 | */ 90 | public static String getUserKey(int userId) { 91 | return PREFIX_USER + SPLIT + userId; 92 | } 93 | 94 | /** 95 | * 单日UV 96 | * 97 | * @param date 98 | * @return 99 | */ 100 | public static String getUvKey(String date) { 101 | return PREFIX_UV + SPLIT + date; 102 | } 103 | 104 | /** 105 | * 区间UV 106 | * 107 | * @param startDate 108 | * @param endDate 109 | * @return 110 | */ 111 | public static String getUvKey(String startDate, String endDate) { 112 | return PREFIX_UV + SPLIT + startDate + SPLIT + endDate; 113 | } 114 | 115 | /** 116 | * 单日DAU 117 | * 118 | * @param date 119 | * @return 120 | */ 121 | public static String getDauKey(String date) { 122 | return PREFIX_DAU + SPLIT + date; 123 | } 124 | 125 | /** 126 | * 区间DAU 127 | * 128 | * @param startDate 129 | * @param endDate 130 | * @return 131 | */ 132 | public static String getDauKey(String startDate, String endDate) { 133 | return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate; 134 | } 135 | 136 | /** 137 | * 帖子分数 138 | * 139 | * @return 140 | */ 141 | public static String getPostScoreKey() { 142 | return PREFIX_POST + SPLIT + "score"; 143 | } 144 | 145 | /** 146 | * 发帖限流 147 | * 148 | * @param userId 149 | * @return 150 | */ 151 | public static String getRateLimitKey(int userId) { 152 | return PREFIX_RATE_LIMIT_PUBLISH + SPLIT + userId; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | community 4 | 5 | 6 | 7 | 8 | 9 | 10 | ${LOG_PATH}/${APPDIR}/log_error.log 11 | 12 | ${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log 13 | 14 | 5MB 15 | 16 | 30 17 | 18 | true 19 | 20 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 21 | utf-8 22 | 23 | 24 | error 25 | ACCEPT 26 | DENY 27 | 28 | 29 | 30 | 31 | 32 | ${LOG_PATH}/${APPDIR}/log_warn.log 33 | 34 | ${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log 35 | 36 | 5MB 37 | 38 | 30 39 | 40 | true 41 | 42 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 43 | utf-8 44 | 45 | 46 | warn 47 | ACCEPT 48 | DENY 49 | 50 | 51 | 52 | 53 | 54 | ${LOG_PATH}/${APPDIR}/log_info.log 55 | 56 | ${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log 57 | 58 | 5MB 59 | 60 | 30 61 | 62 | true 63 | 64 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 65 | utf-8 66 | 67 | 68 | info 69 | ACCEPT 70 | DENY 71 | 72 | 73 | 74 | 75 | 76 | 77 | %d %level [%thread] %logger{10} [%file:%line] %msg%n 78 | utf-8 79 | 80 | 81 | debug 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/service/AlphaService.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.scheduling.annotation.Async; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.TransactionDefinition; 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.transaction.support.TransactionCallback; 13 | import org.springframework.transaction.support.TransactionTemplate; 14 | import tech.turl.community.dao.AlphaDao; 15 | import tech.turl.community.dao.DiscussPostMapper; 16 | import tech.turl.community.dao.UserMapper; 17 | import tech.turl.community.entity.DiscussPost; 18 | import tech.turl.community.entity.User; 19 | import tech.turl.community.util.CommunityUtil; 20 | 21 | import javax.annotation.PostConstruct; 22 | import javax.annotation.PreDestroy; 23 | import java.util.Date; 24 | 25 | /** 26 | * @author zhengguohuang 27 | * @date 2021/03/17 28 | */ 29 | @Service 30 | public class AlphaService { 31 | 32 | @Autowired private AlphaDao alphaDao; 33 | 34 | @Autowired private UserMapper userMapper; 35 | 36 | @Autowired DiscussPostMapper discussPostMapper; 37 | 38 | private static final Logger LOGGER = LoggerFactory.getLogger(AlphaService.class); 39 | 40 | public AlphaService() { 41 | System.out.println("实例化AlphaService"); 42 | } 43 | 44 | @PostConstruct 45 | public void init() { 46 | System.out.println("AlphaService init()."); 47 | } 48 | 49 | @PreDestroy 50 | public void destory() { 51 | System.out.println("销毁AlphaService"); 52 | } 53 | 54 | public String find() { 55 | return alphaDao.select(); 56 | } 57 | 58 | /** 59 | * REQUIRED:支持当前事务(外部事务),如果不存在则创建新事务 REQUIRES_NEW:创建一个新事务,并且暂停当前事务(外部事务) NESTED: 60 | * 如果当前存在事务(外部事务)则嵌套在该事务中执行(独立的提交和回滚),否则就和REQUIRED一样 61 | * 62 | * @return 63 | */ 64 | @Transactional( 65 | isolation = Isolation.READ_COMMITTED, 66 | propagation = Propagation.REQUIRED, 67 | rollbackFor = Exception.class) 68 | public Object save1() { 69 | // 新增用户 70 | User user = new User(); 71 | user.setUsername("alpha"); 72 | user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); 73 | user.setPassword(CommunityUtil.md5("123" + user.getSalt())); 74 | user.setEmail("alpha@qq.com"); 75 | user.setHeaderUrl("http://image.nowcoder.com/head/99t.png"); 76 | user.setCreateTime(new Date()); 77 | userMapper.insertUser(user); 78 | 79 | // 新增帖子 80 | DiscussPost post = new DiscussPost(); 81 | post.setUserId(user.getId()); 82 | post.setTitle("hello"); 83 | post.setContent("新人报道!"); 84 | post.setCreateTime(new Date()); 85 | discussPostMapper.insertDiscussPost(post); 86 | 87 | Integer.valueOf("abc"); 88 | return "ok"; 89 | } 90 | 91 | @Autowired private TransactionTemplate transactionTemplate; 92 | 93 | public Object save2() { 94 | transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); 95 | transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); 96 | return transactionTemplate.execute( 97 | (TransactionCallback) 98 | status -> { 99 | // 新增用户 100 | User user = new User(); 101 | user.setUsername("alpha2"); 102 | user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); 103 | user.setPassword(CommunityUtil.md5("123" + user.getSalt())); 104 | user.setEmail("alpha2@qq.com"); 105 | user.setHeaderUrl("http://image.nowcoder.com/head/999t.png"); 106 | user.setCreateTime(new Date()); 107 | userMapper.insertUser(user); 108 | 109 | // 新增帖子 110 | DiscussPost post = new DiscussPost(); 111 | post.setUserId(user.getId()); 112 | post.setTitle("hello2"); 113 | post.setContent("新人报道2!"); 114 | post.setCreateTime(new Date()); 115 | discussPostMapper.insertDiscussPost(post); 116 | 117 | Integer.valueOf("abc"); 118 | return "ok"; 119 | }); 120 | } 121 | 122 | /** 让该方法在多线性环境下,被异步的调用 */ 123 | @Async 124 | public void execute1() { 125 | LOGGER.debug("execute1"); 126 | } 127 | 128 | // @Scheduled(initialDelay = 10_000, fixedRate = 1_000) 129 | public void execute2() { 130 | LOGGER.debug("execute2"); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.access.AccessDeniedException; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.web.AuthenticationEntryPoint; 10 | import org.springframework.security.web.access.AccessDeniedHandler; 11 | import tech.turl.community.util.CommunityConstant; 12 | import tech.turl.community.util.CommunityUtil; 13 | 14 | import javax.servlet.ServletException; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.io.IOException; 18 | import java.io.PrintWriter; 19 | 20 | /** 21 | * @author zhengguohuang 22 | * @date 2021/03/27 23 | */ 24 | @Configuration 25 | public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant { 26 | @Override 27 | public void configure(WebSecurity web) throws Exception { 28 | web.ignoring().antMatchers("/resources/**"); 29 | } 30 | 31 | @Override 32 | protected void configure(HttpSecurity http) throws Exception { 33 | // 授权 34 | http.authorizeRequests() 35 | .antMatchers( 36 | "/user/setting", 37 | "/user/upload", 38 | "/user/forgetPassword", 39 | "/discuss/add", 40 | "/discuss/my", 41 | "/comment/add/**", 42 | "/letter/**", 43 | "/notice/**", 44 | "/like", 45 | "/follow", 46 | "/unfollow") 47 | .hasAnyAuthority(AUTHORITY_USER, AUTHORITY_ADMIN, AUTHORITY_MODERATOR) 48 | .antMatchers("/discuss/top", "/discuss/wonderful") 49 | .hasAnyAuthority(AUTHORITY_MODERATOR) 50 | .antMatchers("/discuss/delete", "/data/**", "/data/uv", "/data/dau", "/actuator/**") 51 | .hasAnyAuthority(AUTHORITY_ADMIN) 52 | .anyRequest() 53 | .permitAll() 54 | .and() 55 | .csrf() 56 | .disable(); 57 | // 权限不够时的处理 58 | // 1.没有登录,这里有bug,登录后不刷新点赞,关注会显示没有登录 59 | // 2.权限不足 60 | http.exceptionHandling() 61 | .authenticationEntryPoint( 62 | new AuthenticationEntryPoint() { 63 | // 没有登录 64 | @Override 65 | public void commence( 66 | HttpServletRequest request, 67 | HttpServletResponse response, 68 | AuthenticationException e) 69 | throws IOException, ServletException { 70 | String xRequestedWith = request.getHeader("x-requested-with"); 71 | if ("XMLHttpRequest".equals(xRequestedWith)) { 72 | response.setContentType("application/plain;charset=utf-8"); 73 | PrintWriter writer = response.getWriter(); 74 | writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!")); 75 | } else { 76 | response.sendRedirect(request.getContextPath() + "/login"); 77 | } 78 | } 79 | }) 80 | .accessDeniedHandler( 81 | new AccessDeniedHandler() { 82 | // 权限不足 83 | @Override 84 | public void handle( 85 | HttpServletRequest request, 86 | HttpServletResponse response, 87 | AccessDeniedException e) 88 | throws IOException, ServletException { 89 | String xRequestedWith = request.getHeader("x-requested-with"); 90 | if ("XMLHttpRequest".equals(xRequestedWith)) { 91 | response.setContentType("application/plain;charset=utf-8"); 92 | PrintWriter writer = response.getWriter(); 93 | writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!")); 94 | } else { 95 | response.sendRedirect(request.getContextPath() + "/denied"); 96 | } 97 | } 98 | }); 99 | 100 | // Security底层默认会拦截/logout请求,进行退出处理. 101 | // 覆盖它默认的逻辑,才能执行我们自己的退出代码. 102 | http.logout().logoutUrl("/securitylogout"); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/operate-result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 牛客网-操作结果 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 63 |
64 |
65 | 66 | 67 |
68 |
69 |
70 |

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

71 |
72 |

73 | 系统会在 8 秒后自动跳转, 74 | 您也可以点此 链接, 手动跳转! 75 |

76 |
77 |
78 |
79 | 80 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /sql/init_schema.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 8.0.15, for Win64 (x86_64) 2 | -- 3 | -- Host: localhost Database: community 4 | -- ------------------------------------------------------ 5 | -- Server version 8.0.15 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | SET NAMES utf8 ; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `comment` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `comment`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | SET character_set_client = utf8mb4 ; 25 | CREATE TABLE `comment` ( 26 | `id` int(11) NOT NULL AUTO_INCREMENT, 27 | `user_id` int(11) DEFAULT NULL, 28 | `entity_type` int(11) DEFAULT NULL, 29 | `entity_id` int(11) DEFAULT NULL, 30 | `target_id` int(11) DEFAULT NULL, 31 | `content` text, 32 | `status` int(11) DEFAULT NULL, 33 | `create_time` timestamp NULL DEFAULT NULL, 34 | PRIMARY KEY (`id`), 35 | KEY `index_user_id` (`user_id`) /*!80000 INVISIBLE */, 36 | KEY `index_entity_id` (`entity_id`) 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 38 | /*!40101 SET character_set_client = @saved_cs_client */; 39 | 40 | -- 41 | -- Table structure for table `discuss_post` 42 | -- 43 | 44 | DROP TABLE IF EXISTS `discuss_post`; 45 | /*!40101 SET @saved_cs_client = @@character_set_client */; 46 | SET character_set_client = utf8mb4 ; 47 | CREATE TABLE `discuss_post` ( 48 | `id` int(11) NOT NULL AUTO_INCREMENT, 49 | `user_id` varchar(45) DEFAULT NULL, 50 | `title` varchar(100) DEFAULT NULL, 51 | `content` text, 52 | `type` int(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;', 53 | `status` int(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;', 54 | `create_time` timestamp NULL DEFAULT NULL, 55 | `comment_count` int(11) DEFAULT NULL, 56 | `score` double DEFAULT NULL, 57 | PRIMARY KEY (`id`), 58 | KEY `index_user_id` (`user_id`) 59 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 60 | /*!40101 SET character_set_client = @saved_cs_client */; 61 | 62 | -- 63 | -- Table structure for table `login_ticket` 64 | -- 65 | 66 | DROP TABLE IF EXISTS `login_ticket`; 67 | /*!40101 SET @saved_cs_client = @@character_set_client */; 68 | SET character_set_client = utf8mb4 ; 69 | CREATE TABLE `login_ticket` ( 70 | `id` int(11) NOT NULL AUTO_INCREMENT, 71 | `user_id` int(11) NOT NULL, 72 | `ticket` varchar(45) NOT NULL, 73 | `status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;', 74 | `expired` timestamp NOT NULL, 75 | PRIMARY KEY (`id`), 76 | KEY `index_ticket` (`ticket`(20)) 77 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 78 | /*!40101 SET character_set_client = @saved_cs_client */; 79 | 80 | -- 81 | -- Table structure for table `message` 82 | -- 83 | 84 | DROP TABLE IF EXISTS `message`; 85 | /*!40101 SET @saved_cs_client = @@character_set_client */; 86 | SET character_set_client = utf8mb4 ; 87 | CREATE TABLE `message` ( 88 | `id` int(11) NOT NULL AUTO_INCREMENT, 89 | `from_id` int(11) DEFAULT NULL, 90 | `to_id` int(11) DEFAULT NULL, 91 | `conversation_id` varchar(45) NOT NULL, 92 | `content` text, 93 | `status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;', 94 | `create_time` timestamp NULL DEFAULT NULL, 95 | PRIMARY KEY (`id`), 96 | KEY `index_from_id` (`from_id`), 97 | KEY `index_to_id` (`to_id`), 98 | KEY `index_conversation_id` (`conversation_id`) 99 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 100 | /*!40101 SET character_set_client = @saved_cs_client */; 101 | 102 | -- 103 | -- Table structure for table `user` 104 | -- 105 | 106 | DROP TABLE IF EXISTS `user`; 107 | /*!40101 SET @saved_cs_client = @@character_set_client */; 108 | SET character_set_client = utf8mb4 ; 109 | CREATE TABLE `user` ( 110 | `id` int(11) NOT NULL AUTO_INCREMENT, 111 | `username` varchar(50) DEFAULT NULL, 112 | `password` varchar(50) DEFAULT NULL, 113 | `salt` varchar(50) DEFAULT NULL, 114 | `email` varchar(100) DEFAULT NULL, 115 | `type` int(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;', 116 | `status` int(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;', 117 | `activation_code` varchar(100) DEFAULT NULL, 118 | `header_url` varchar(200) DEFAULT NULL, 119 | `create_time` timestamp NULL DEFAULT NULL, 120 | PRIMARY KEY (`id`), 121 | KEY `index_username` (`username`(20)), 122 | KEY `index_email` (`email`(20)) 123 | ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8; 124 | /*!40101 SET character_set_client = @saved_cs_client */; 125 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 126 | 127 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 128 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 129 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 130 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 131 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 132 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 133 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 134 | 135 | -- Dump completed on 2019-05-05 19:37:53 136 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/controller/FollowController.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.ui.Model; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | import tech.turl.community.entity.Event; 11 | import tech.turl.community.entity.Page; 12 | import tech.turl.community.entity.User; 13 | import tech.turl.community.event.EventProducer; 14 | import tech.turl.community.service.FollowService; 15 | import tech.turl.community.service.UserService; 16 | import tech.turl.community.util.CommunityConstant; 17 | import tech.turl.community.util.CommunityUtil; 18 | import tech.turl.community.util.HostHolder; 19 | 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | /** 24 | * @author zhengguohuang 25 | * @date 2021/03/22 26 | */ 27 | @Controller 28 | public class FollowController implements CommunityConstant { 29 | 30 | @Autowired private HostHolder hostHolder; 31 | 32 | @Autowired private FollowService followService; 33 | 34 | @Autowired private UserService userService; 35 | 36 | @Autowired private EventProducer eventProducer; 37 | 38 | /** 39 | * 关注 40 | * 41 | * @param entityType 42 | * @param entityId 43 | * @return 44 | */ 45 | @PostMapping("/follow") 46 | @ResponseBody 47 | public String follow(int entityType, int entityId) { 48 | User user = hostHolder.getUser(); 49 | followService.follow(user.getId(), entityType, entityId); 50 | 51 | // 触发关注事件 52 | Event event = 53 | new Event() 54 | .setTopic(TOPIC_FOLLOW) 55 | .setUserId(hostHolder.getUser().getId()) 56 | .setEntityType(entityType) 57 | .setEntityId(entityId) 58 | .setEntityUserId(entityId); 59 | eventProducer.fireEvent(event); 60 | 61 | return CommunityUtil.getJSONString(0, "已关注!"); 62 | } 63 | 64 | /** 65 | * 取消关注 66 | * 67 | * @param entityType 68 | * @param entityId 69 | * @return 70 | */ 71 | @PostMapping("/unfollow") 72 | @ResponseBody 73 | public String unfollow(int entityType, int entityId) { 74 | User user = hostHolder.getUser(); 75 | followService.unfollow(user.getId(), entityType, entityId); 76 | return CommunityUtil.getJSONString(0, "已取消关注!"); 77 | } 78 | 79 | /** 80 | * 当前用户是否关注id为userId的用户 81 | * 82 | * @param userId 83 | * @return 84 | */ 85 | private boolean hasFollowed(int userId) { 86 | if (hostHolder.getUser() == null) { 87 | return false; 88 | } 89 | return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); 90 | } 91 | 92 | /** 93 | * 获取关注的人的列表 94 | * 95 | * @param userId 96 | * @param page 97 | * @param model 98 | * @return 99 | */ 100 | @GetMapping("/followees/{userId}") 101 | public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) { 102 | User user = userService.findUserById(userId); 103 | if (user == null) { 104 | throw new RuntimeException("该用户不存在!"); 105 | } 106 | model.addAttribute("user", user); 107 | page.setLimit(5); 108 | page.setPath("/followees/" + userId); 109 | page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER)); 110 | 111 | List> userList = 112 | followService.findFollowees(userId, page.getOffset(), page.getLimit()); 113 | if (userList != null) { 114 | for (Map map : userList) { 115 | User u = (User) map.get("user"); 116 | map.put("hasFollowed", hasFollowed(u.getId())); 117 | } 118 | } 119 | model.addAttribute("users", userList); 120 | return "site/followee"; 121 | } 122 | 123 | /** 124 | * 获取关注的人的列表 125 | * 126 | * @param userId 127 | * @param page 128 | * @param model 129 | * @return 130 | */ 131 | @GetMapping("/followers/{userId}") 132 | public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) { 133 | User user = userService.findUserById(userId); 134 | if (user == null) { 135 | throw new RuntimeException("该用户不存在!"); 136 | } 137 | model.addAttribute("user", user); 138 | page.setLimit(5); 139 | page.setPath("/followers/" + userId); 140 | page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId)); 141 | 142 | List> userList = 143 | followService.findFollowers(userId, page.getOffset(), page.getLimit()); 144 | if (userList != null) { 145 | for (Map map : userList) { 146 | User u = (User) map.get("user"); 147 | map.put("hasFollowed", hasFollowed(u.getId())); 148 | } 149 | } 150 | model.addAttribute("users", userList); 151 | return "site/follower"; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 仿牛客网社区项目 2 | 3 | ![IDE](https://img.shields.io/badge/IDE-IntelliJ%20IDEA-brightgreen.svg) ![Java](https://img.shields.io/badge/Java-1.8-blue.svg) ![Database](https://img.shields.io/badge/Database-MySQL-lightgrey.svg) 4 | 5 | 欢迎加入QQ交流群: 792364202,目前已经上传了项目开发用到的相关资料、工具等文件。 6 | 7 | ## 项目简介 8 | 9 | 一个仿照牛客网实现的讨论区,不仅实现了基本的注册、登录、发帖、评论、回复、私信等功能,同时使用前缀树实现敏感词过滤;使用 Redis 实现点赞与关注;使用 Kafka 处理发送评论、点赞和关注等系统通知;使用 Elasticsearch 实现全文搜索,关键词高亮显示;使用 wkhtmltopdf 生成长图和 PDF;实现网站 UV 和 DAU 统计;并将用户头像等信息存于七牛云服务器。 10 | 11 | ## 在线演示地址 12 | 13 | http://community.turl.tech 14 | 15 | ## 测试账号 16 | 17 | | 用户类型 | 用户名 | 密码 | 18 | | -------- | ------ | ------ | 19 | | 普通用户 | user | 123456 | 20 | | 版主 | banzhu | 123456 | 21 | | 管理员 | admin | 123456 | 22 | 23 | ## 功能列表 24 | 25 | ### 已经实现了的功能 26 | 27 | - [x] 邮件发送 28 | - [x] 注册 29 | - [x] 验证码 30 | - [x] 登录 31 | - [x] 修改头像 32 | - [x] 修改密码 33 | - [x] 敏感词过滤 34 | - [x] 发布帖子 35 | - [x] 我的帖子 36 | - [x] 帖子详情 37 | - [x] 评论 38 | - [x] 私信 39 | - [x] 统一异常处理 40 | - [x] 统一日志处理 41 | - [x] 点赞 42 | - [x] 关注 43 | - [x] 系统通知 44 | - [x] 搜索 45 | - [x] 权限控制 46 | - [x] 置顶、加精、删除 47 | - [x] 网站统计 48 | - [x] 定时执行任务计算热门帖子 49 | - [x] 生成长图 50 | - [x] 文件上传至七牛云 51 | - [x] 监控 52 | - [x] 限流 53 | 54 | ### TO DO List 55 | 56 | - [ ] 积分模块 57 | - [ ] 收藏模块 58 | - [ ] 浏览量 59 | 60 | ## 功能简介 61 | 62 | - 使用 Redis 的 set 实现点赞,zset 实现关注,并使用 Redis 存储登录ticket和验证码,解决分布式 Session 问题,使用 Redis 的高级数据类型 HyperLogLog 统计 UV (Unique Visitor),使用 Bitmap 统计 DAU (Daily Active User)。 63 | - 使用Redis Cell模块对用户发帖进行限流,防止恶意灌水。 64 | - 使用 Kafka 处理发送评论、点赞、关注等系统通知、将新发布的帖子异步传输至Elasticsearch服务器,并使用事件进行封装,构建了强大的异步消息系统。 65 | - 使用Elasticsearch做全局搜索,增加关键词高亮显示等功能。 66 | - 热帖排行模块,使用本地缓存 Caffeine作为一级缓存和分布式缓存 Redis作为二级缓存构建多级缓存,避免了缓存雪崩,同时使用使用压测工具测试优化前后性能,将 QPS 提升了4.4倍 (7.6/sec -> 33.5/sec),大大提升了网站访问速度。 67 | - 使用 Quartz 定时更新热帖排行。 68 | - 使用 Spring Security 做权限控制,替代拦截器的拦截控制,并使用自己的认证方案替代 Security 认证流程,使权限认证和控制更加方便灵活。 69 | 70 | ## 技术栈 71 | 72 | | 技术 | 链接 | 版本 | 73 | | --------------- | ------------------------------------------------------------ | -------------- | 74 | | Spring Boot | https://spring.io/projects/spring-boot | 2.4.3 | 75 | | Spring | https://spring.io/projects/spring-framework | 5.3.4 | 76 | | Spring MVC | https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#spring-web | 5.3.4 | 77 | | MyBatis | http://www.mybatis.org/mybatis-3 | 3.5.1 | 78 | | Redis | https://redis.io/ | 5.0.3 | 79 | | Kafka | http://kafka.apache.org/ | 2.7.0 | 80 | | Elasticsearch | https://www.elastic.co/cn/elasticsearch/ | 7.9.3 | 81 | | Spring Security | https://spring.io/projects/spring-security | 5.4.5 | 82 | | Spring Quartz | https://www.baeldung.com/spring-quartz-schedule | 2.3.2 | 83 | | wkhtmltopdf | https://wkhtmltopdf.org | 0.12.6 | 84 | | kaptcha | https://github.com/penggle/kaptcha | 2.3.2 | 85 | | Thymeleaf | https://www.thymeleaf.org/ | 3.0.12.RELEASE | 86 | | MySQL | https://www.mysql.com/ | 5.7.17 | 87 | | JDK | https://www.oracle.com/java/technologies/javase-downloads.html | 1.8 | 88 | 89 | ## 系统架构 90 | 91 | ![image-20210331103427522](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331103427522.png) 92 | 93 | ![网站架构图](https://gitee.com/zhengguohuang/img/raw/master/img/%E7%BD%91%E7%AB%99%E6%9E%B6%E6%9E%84%E5%9B%BE.png) 94 | 95 | ## 数据库初始化 96 | 97 | ```sql 98 | create database community; 99 | use community; 100 | source /path/to/sql/init_schema.sql; 101 | source /path/to/sql/init_data.sql; 102 | source /path/to/sql/tables_mysql_innodb.sql; 103 | ``` 104 | 105 | ## 运行 106 | 107 | 1. 安装JDK,Maven 108 | 109 | 2. 克隆代码到本地 110 | 111 | ```bash 112 | git clone https://github.com/zhengguohuang/community.git 113 | ``` 114 | 115 | 3. 配置mysql、七牛云、kafka、ElasticSearch 116 | 117 | 4. 启动zookeeper 118 | 119 | 5. 启动Kafka 120 | 121 | 6. 启动Elasticsearch 122 | 123 | 7. 运行打包命令 124 | 125 | ```bash 126 | mvn package 127 | ``` 128 | 129 | 8. 运行项目 130 | 131 | ```bash 132 | java -jar xxx.jar 133 | ``` 134 | 135 | 9. 访问项目 136 | 137 | ``` 138 | http://localhost:8080 139 | ``` 140 | 141 | ## 运行效果展示 142 | 143 | #### 发帖 144 | 145 | ![image-20210331105217867](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105217867.png) 146 | 147 | #### 帖子详情、评论 148 | 149 | ![image-20210331105241026](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105241026.png) 150 | 151 | #### 私信 152 | 153 | ![image-20210331105316546](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105316546.png) 154 | 155 | #### 系统通知 156 | 157 | ![image-20210331105333140](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105333140.png) 158 | 159 | #### 搜索 160 | 161 | ![image-20210331105404130](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105404130.png) 162 | 163 | #### 网站统计 164 | 165 | ![image-20210331105420047](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105420047.png) 166 | 167 | #### 限流 168 | 169 | ![image-20210427152933772](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210427152933772.png) 170 | 171 | ## 文档 172 | 173 | https://easydoc.top/doc/70562937/SJuuLoYS/cI3Atvte 174 | 175 | ## 更新日志 176 | 177 | * 2021-3-8 创建项目 178 | * 2021-4-27增加发帖限流功能 179 | 180 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.4.3 9 | 10 | 11 | tech.turl.community 12 | community 13 | 0.0.1-SNAPSHOT 14 | community 15 | community 16 | 17 | 1.8 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-aop 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-devtools 36 | runtime 37 | true 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-test 42 | test 43 | 44 | 45 | mysql 46 | mysql-connector-java 47 | 8.0.16 48 | 49 | 50 | org.mybatis.spring.boot 51 | mybatis-spring-boot-starter 52 | 2.0.1 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-mail 58 | 59 | 60 | org.apache.commons 61 | commons-lang3 62 | 3.12.0 63 | 64 | 65 | com.github.penggle 66 | kaptcha 67 | 2.3.2 68 | 69 | 70 | com.alibaba 71 | fastjson 72 | 1.2.75 73 | 74 | 75 | org.springframework.data 76 | spring-data-redis 77 | 2.4.3 78 | 79 | 80 | io.lettuce 81 | lettuce-core 82 | 6.0.2.RELEASE 83 | 84 | 85 | redis.clients 86 | jedis 87 | 3.3.0 88 | 89 | 90 | 91 | org.springframework.kafka 92 | spring-kafka 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-starter-data-elasticsearch 97 | 98 | 99 | org.springframework.boot 100 | spring-boot-starter-security 101 | 102 | 103 | 104 | org.thymeleaf.extras 105 | thymeleaf-extras-springsecurity5 106 | 107 | 108 | org.springframework.boot 109 | spring-boot-starter-quartz 110 | 111 | 112 | 113 | com.qiniu 114 | qiniu-java-sdk 115 | 7.4.0 116 | 117 | 118 | com.github.ben-manes.caffeine 119 | caffeine 120 | 2.7.0 121 | 122 | 123 | org.springframework.boot 124 | spring-boot-starter-actuator 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | org.springframework.boot 134 | spring-boot-maven-plugin 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-resources-plugin 139 | 2.4.3 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/main/java/tech/turl/community/util/SensitiveFilter.java: -------------------------------------------------------------------------------- 1 | package tech.turl.community.util; 2 | 3 | import org.apache.commons.lang3.CharUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * 敏感词过滤器,使用前缀树实现 19 | * 20 | * @author zhengguohuang 21 | * @date 2021/03/16 22 | */ 23 | @Component 24 | public class SensitiveFilter { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(SensitiveFilter.class); 27 | 28 | /** 29 | * 替换符 30 | */ 31 | private static final String REPLACEMENT = "***"; 32 | 33 | /** 34 | * 根节点 35 | */ 36 | private TrieNode rootNode = new TrieNode(); 37 | 38 | /** 39 | * 程序启动时,或初次调用时初始化好,只需要初始化一次, 40 | * 容器实例化以后,调用构造器,这个方法就会被调用, 41 | * 这个bean在服务启动时候初始化,所以这个方法在服务启动时候 42 | * 就会被调用。 43 | */ 44 | @PostConstruct 45 | public void init() { 46 | long start = System.currentTimeMillis(); 47 | try ( 48 | InputStream is = this.getClass().getClassLoader() 49 | .getResourceAsStream("sensitive-words.txt"); 50 | BufferedReader reader = new BufferedReader(new InputStreamReader(is)) 51 | ) { 52 | String keyword; 53 | while ((keyword = reader.readLine()) != null) { 54 | // 添加到前缀树 55 | this.addKeyword(keyword); 56 | } 57 | 58 | } catch (IOException e) { 59 | LOGGER.error("加载敏感词文件失败:" + e.getMessage()); 60 | e.printStackTrace(); 61 | } 62 | LOGGER.info("构建前缀树时间:" + String.valueOf(System.currentTimeMillis() - start) + "ms"); 63 | 64 | } 65 | 66 | /** 67 | * 将一个敏感词添加到前缀树中 68 | * 69 | * @param keyword 70 | */ 71 | private void addKeyword(String keyword) { 72 | TrieNode tempNode = rootNode; 73 | for (int i = 0; i < keyword.length(); i++) { 74 | char c = keyword.charAt(i); 75 | TrieNode subNode = tempNode.getSubNode(c); 76 | 77 | if (subNode == null) { 78 | subNode = new TrieNode(); 79 | tempNode.addSubNode(c, subNode); 80 | } 81 | 82 | // 指向子节点,进入下一轮循环 83 | tempNode = subNode; 84 | 85 | // 设置结束标志 86 | if (i == keyword.length() - 1) { 87 | tempNode.setKeywordEnd(true); 88 | } 89 | } 90 | 91 | } 92 | 93 | /** 94 | * 过滤敏感词 95 | *

96 | * 对于敏感词中有符号的先去除符号 97 | * 98 | * @param text 待过滤的文本 99 | * @return 过滤后的文本 100 | */ 101 | public String filter(String text) { 102 | if (StringUtils.isBlank(text)) { 103 | return null; 104 | } 105 | // 指针1 106 | TrieNode tempNode = rootNode; 107 | // 指针2 108 | int begin = 0; 109 | // 指针3 110 | int position = 0; 111 | // 结果 112 | StringBuilder sb = new StringBuilder(); 113 | while (position < text.length()) { 114 | char c = text.charAt(position); 115 | 116 | // 跳过符号 117 | if (isSymbol(c)) { 118 | // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步 119 | if (tempNode == rootNode) { 120 | sb.append(c); 121 | begin++; 122 | } 123 | // 无论符号在开头或者中间,指针3都向下走一步 124 | position++; 125 | continue; 126 | } 127 | // 检查下级节点 128 | tempNode = tempNode.getSubNode(c); 129 | if (tempNode == null) { 130 | // 以begin开头的字符串不是敏感词 131 | sb.append(text.charAt(begin)); 132 | // 进入下一个位置 133 | position = ++begin; 134 | // 重新指向根节点 135 | tempNode = rootNode; 136 | } else if (tempNode.isKeywordEnd()) { 137 | // 发现敏感词,将begin-position字符串替换掉 138 | sb.append(REPLACEMENT); 139 | // 进入下一个位置 140 | begin = ++position; 141 | // 重新指向根节点 142 | tempNode = rootNode; 143 | } else { 144 | // 检查下一个字符 145 | position++; 146 | 147 | } 148 | } 149 | 150 | sb.append(text.substring(begin)); 151 | return sb.toString(); 152 | 153 | } 154 | 155 | private boolean isSymbol(char c) { 156 | // 0x2E80 到 0x9FFF为东亚文字 157 | return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF); 158 | } 159 | 160 | /** 161 | * 前缀树节点 162 | */ 163 | private class TrieNode { 164 | 165 | // 关键词结束标志 166 | private boolean isKeywordEnd = false; 167 | 168 | // 子节点(key是下级字符,value是下级节点) 169 | private Map subNodes = new HashMap<>(16); 170 | 171 | public boolean isKeywordEnd() { 172 | return isKeywordEnd; 173 | } 174 | 175 | public void setKeywordEnd(boolean keywordEnd) { 176 | isKeywordEnd = keywordEnd; 177 | } 178 | 179 | /** 180 | * 添加子节点 181 | * 182 | * @param c 字符 183 | * @param node 前缀树节点 184 | */ 185 | public void addSubNode(Character c, TrieNode node) { 186 | subNodes.put(c, node); 187 | } 188 | 189 | /** 190 | * 获取子节点 191 | * 192 | * @param c 字符 193 | * @return 子节点的引用 194 | */ 195 | public TrieNode getSubNode(Character c) { 196 | return subNodes.get(c); 197 | } 198 | 199 | } 200 | } 201 | --------------------------------------------------------------------------------