├── .gitignore ├── README.md ├── pom.xml ├── sql ├── community-init-sql-1.5 │ ├── init_data.sql │ ├── init_schema.sql │ ├── my.ini │ └── tables_mysql_innodb.sql └── community.sql ├── src ├── main │ ├── java │ │ └── com │ │ │ └── tassel │ │ │ ├── CommunityApplication.java │ │ │ ├── annotation │ │ │ └── LoginRequired.java │ │ │ ├── config │ │ │ ├── KaptchaConfig.java │ │ │ ├── RedisConfig.java │ │ │ └── WebMvcConfig.java │ │ │ ├── controller │ │ │ ├── CommentController.java │ │ │ ├── DiscussPostController.java │ │ │ ├── FollowController.java │ │ │ ├── HomeController.java │ │ │ ├── LikeController.java │ │ │ ├── LoginController.java │ │ │ ├── MessageController.java │ │ │ ├── SearchController.java │ │ │ ├── UserController.java │ │ │ └── advice │ │ │ │ └── ExceptionAdvice.java │ │ │ ├── entity │ │ │ ├── Comment.java │ │ │ ├── DiscussPost.java │ │ │ ├── Event.java │ │ │ ├── LoginTicket.java │ │ │ ├── Message.java │ │ │ └── User.java │ │ │ ├── event │ │ │ ├── EventConsumer.java │ │ │ └── EventProducer.java │ │ │ ├── interceptor │ │ │ ├── LoginInterceptor.java │ │ │ ├── LoginRequiredInterceptor.java │ │ │ └── MessageInterceptor.java │ │ │ ├── mapper │ │ │ ├── CommentMapper.java │ │ │ ├── DiscussPostMapper.java │ │ │ ├── LoginTicketMapper.java │ │ │ ├── MessageMapper.java │ │ │ ├── UserMapper.java │ │ │ └── elasticsearch │ │ │ │ └── DiscussPostRepository.java │ │ │ ├── service │ │ │ ├── CommentService.java │ │ │ ├── DiscussPostService.java │ │ │ ├── ElasticsearchService.java │ │ │ ├── FollowService.java │ │ │ ├── LikeService.java │ │ │ ├── MessageService.java │ │ │ ├── UserService.java │ │ │ └── impl │ │ │ │ ├── CommentServiceImpl.java │ │ │ │ ├── DiscussPostServiceImpl.java │ │ │ │ ├── ElasticsearchServiceImpl.java │ │ │ │ ├── FollowServiceImpl.java │ │ │ │ ├── LikeServiceImpl.java │ │ │ │ ├── MessageServiceImpl.java │ │ │ │ └── UserServiceImpl.java │ │ │ └── util │ │ │ ├── CommunityConstant.java │ │ │ ├── CommunityUtil.java │ │ │ ├── CookieUtil.java │ │ │ ├── HostHolder.java │ │ │ ├── MailClient.java │ │ │ ├── Page.java │ │ │ ├── RedisKeyUtil.java │ │ │ └── SensitiveFilter.java │ └── resources │ │ ├── application.yaml │ │ ├── mapper │ │ ├── comment-mapper.xml │ │ ├── discusspost-mapper.xml │ │ ├── message-mapper.xml │ │ └── user-mapper.xml │ │ ├── sensitive-words.txt │ │ ├── static │ │ ├── css │ │ │ ├── bootstrap.min.css │ │ │ ├── discuss-detail.css │ │ │ ├── global.css │ │ │ ├── letter.css │ │ │ └── login.css │ │ ├── img │ │ │ ├── 404.png │ │ │ ├── captcha.png │ │ │ └── error.png │ │ └── js │ │ │ ├── bootstrap.min.js │ │ │ ├── discuss.js │ │ │ ├── global.js │ │ │ ├── index.js │ │ │ ├── jquery-3.1.0.min.js │ │ │ ├── letter.js │ │ │ ├── popper.min.js │ │ │ ├── profile.js │ │ │ └── register.js │ │ └── templates │ │ ├── error │ │ ├── 404.html │ │ └── 500.html │ │ ├── index.html │ │ ├── mail │ │ ├── activation.html │ │ └── forget.html │ │ └── site │ │ ├── admin │ │ └── data.html │ │ ├── discuss-detail.html │ │ ├── followee.html │ │ ├── follower.html │ │ ├── forget.html │ │ ├── letter-detail.html │ │ ├── letter.html │ │ ├── login.html │ │ ├── my-post.html │ │ ├── my-reply.html │ │ ├── notice-detail.html │ │ ├── notice.html │ │ ├── operate-result.html │ │ ├── profile.html │ │ ├── register.html │ │ ├── search.html │ │ └── setting.html └── test │ └── java │ └── com │ └── tassel │ ├── CommunityApplicationTests.java │ ├── ElasticsearchTests.java │ ├── KafkaTest.java │ ├── MailTest.java │ ├── RedisTest.java │ ├── SensitiveTests.java │ ├── mapper │ ├── DiscussPostMapperTest.java │ ├── LoginTicketMapperTest.java │ └── UserMapperTest.java │ └── queue │ └── BlockingQueueTest.java └── upload ├── 4ee51b01858d4fe39513918f09b19928.jpg └── e41d6ebb2d1e499a9b132537ce55da1d.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 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 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.7.RELEASE 9 | 10 | 11 | com.tassel 12 | community 13 | 0.0.1-SNAPSHOT 14 | community 15 | Community project for SpringBoot study. 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | org.mybatis.spring.boot 29 | mybatis-spring-boot-starter 30 | 1.3.2 31 | 32 | 33 | mysql 34 | mysql-connector-java 35 | runtime 36 | 5.1.47 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-thymeleaf 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-data-redis 47 | 48 | 49 | 50 | org.springframework.kafka 51 | spring-kafka 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-data-elasticsearch 57 | 58 | 59 | 60 | com.alibaba 61 | fastjson 62 | 1.2.58 63 | 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | true 69 | 70 | 71 | 72 | org.apache.commons 73 | commons-lang3 74 | 3.9 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-mail 79 | 80 | 81 | 82 | com.github.penggle 83 | kaptcha 84 | 2.3.2 85 | 86 | 87 | 88 | 89 | 90 | com.github.ulisesbocchio 91 | jasypt-spring-boot-starter 92 | 2.1.2 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-starter-test 98 | test 99 | 100 | 101 | org.junit.vintage 102 | junit-vintage-engine 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.springframework.boot 112 | spring-boot-maven-plugin 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /sql/community-init-sql-1.5/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 | -------------------------------------------------------------------------------- /sql/community-init-sql-1.5/my.ini: -------------------------------------------------------------------------------- 1 | [mysql] 2 | default-character-set=utf8 3 | [mysqld] 4 | port=3306 5 | basedir=C:\\work\\mysql-8.0.15-winx64 6 | max_connections=20 7 | character-set-server=utf8 8 | default-storage-engine=INNODB -------------------------------------------------------------------------------- /sql/community-init-sql-1.5/tables_mysql_innodb.sql: -------------------------------------------------------------------------------- 1 | # 2 | # In your Quartz properties file, you'll need to set 3 | # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate 4 | # 5 | # 6 | # By: Ron Cordell - roncordell 7 | # I didn't see this anywhere, so I thought I'd post it here. This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM. 8 | 9 | DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; 10 | DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; 11 | DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; 12 | DROP TABLE IF EXISTS QRTZ_LOCKS; 13 | DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; 14 | DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; 15 | DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; 16 | DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; 17 | DROP TABLE IF EXISTS QRTZ_TRIGGERS; 18 | DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; 19 | DROP TABLE IF EXISTS QRTZ_CALENDARS; 20 | 21 | CREATE TABLE QRTZ_JOB_DETAILS( 22 | SCHED_NAME VARCHAR(120) NOT NULL, 23 | JOB_NAME VARCHAR(190) NOT NULL, 24 | JOB_GROUP VARCHAR(190) NOT NULL, 25 | DESCRIPTION VARCHAR(250) NULL, 26 | JOB_CLASS_NAME VARCHAR(250) NOT NULL, 27 | IS_DURABLE VARCHAR(1) NOT NULL, 28 | IS_NONCONCURRENT VARCHAR(1) NOT NULL, 29 | IS_UPDATE_DATA VARCHAR(1) NOT NULL, 30 | REQUESTS_RECOVERY VARCHAR(1) NOT NULL, 31 | JOB_DATA BLOB NULL, 32 | PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)) 33 | ENGINE=InnoDB; 34 | 35 | CREATE TABLE QRTZ_TRIGGERS ( 36 | SCHED_NAME VARCHAR(120) NOT NULL, 37 | TRIGGER_NAME VARCHAR(190) NOT NULL, 38 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 39 | JOB_NAME VARCHAR(190) NOT NULL, 40 | JOB_GROUP VARCHAR(190) NOT NULL, 41 | DESCRIPTION VARCHAR(250) NULL, 42 | NEXT_FIRE_TIME BIGINT(13) NULL, 43 | PREV_FIRE_TIME BIGINT(13) NULL, 44 | PRIORITY INTEGER NULL, 45 | TRIGGER_STATE VARCHAR(16) NOT NULL, 46 | TRIGGER_TYPE VARCHAR(8) NOT NULL, 47 | START_TIME BIGINT(13) NOT NULL, 48 | END_TIME BIGINT(13) NULL, 49 | CALENDAR_NAME VARCHAR(190) NULL, 50 | MISFIRE_INSTR SMALLINT(2) NULL, 51 | JOB_DATA BLOB NULL, 52 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 53 | FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 54 | REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)) 55 | ENGINE=InnoDB; 56 | 57 | CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( 58 | SCHED_NAME VARCHAR(120) NOT NULL, 59 | TRIGGER_NAME VARCHAR(190) NOT NULL, 60 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 61 | REPEAT_COUNT BIGINT(7) NOT NULL, 62 | REPEAT_INTERVAL BIGINT(12) NOT NULL, 63 | TIMES_TRIGGERED BIGINT(10) NOT NULL, 64 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 65 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 66 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 67 | ENGINE=InnoDB; 68 | 69 | CREATE TABLE QRTZ_CRON_TRIGGERS ( 70 | SCHED_NAME VARCHAR(120) NOT NULL, 71 | TRIGGER_NAME VARCHAR(190) NOT NULL, 72 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 73 | CRON_EXPRESSION VARCHAR(120) NOT NULL, 74 | TIME_ZONE_ID VARCHAR(80), 75 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 76 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 77 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 78 | ENGINE=InnoDB; 79 | 80 | CREATE TABLE QRTZ_SIMPROP_TRIGGERS 81 | ( 82 | SCHED_NAME VARCHAR(120) NOT NULL, 83 | TRIGGER_NAME VARCHAR(190) NOT NULL, 84 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 85 | STR_PROP_1 VARCHAR(512) NULL, 86 | STR_PROP_2 VARCHAR(512) NULL, 87 | STR_PROP_3 VARCHAR(512) NULL, 88 | INT_PROP_1 INT NULL, 89 | INT_PROP_2 INT NULL, 90 | LONG_PROP_1 BIGINT NULL, 91 | LONG_PROP_2 BIGINT NULL, 92 | DEC_PROP_1 NUMERIC(13,4) NULL, 93 | DEC_PROP_2 NUMERIC(13,4) NULL, 94 | BOOL_PROP_1 VARCHAR(1) NULL, 95 | BOOL_PROP_2 VARCHAR(1) NULL, 96 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 97 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 98 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 99 | ENGINE=InnoDB; 100 | 101 | CREATE TABLE QRTZ_BLOB_TRIGGERS ( 102 | SCHED_NAME VARCHAR(120) NOT NULL, 103 | TRIGGER_NAME VARCHAR(190) NOT NULL, 104 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 105 | BLOB_DATA BLOB NULL, 106 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 107 | INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP), 108 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 109 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 110 | ENGINE=InnoDB; 111 | 112 | CREATE TABLE QRTZ_CALENDARS ( 113 | SCHED_NAME VARCHAR(120) NOT NULL, 114 | CALENDAR_NAME VARCHAR(190) NOT NULL, 115 | CALENDAR BLOB NOT NULL, 116 | PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)) 117 | ENGINE=InnoDB; 118 | 119 | CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( 120 | SCHED_NAME VARCHAR(120) NOT NULL, 121 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 122 | PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)) 123 | ENGINE=InnoDB; 124 | 125 | CREATE TABLE QRTZ_FIRED_TRIGGERS ( 126 | SCHED_NAME VARCHAR(120) NOT NULL, 127 | ENTRY_ID VARCHAR(95) NOT NULL, 128 | TRIGGER_NAME VARCHAR(190) NOT NULL, 129 | TRIGGER_GROUP VARCHAR(190) NOT NULL, 130 | INSTANCE_NAME VARCHAR(190) NOT NULL, 131 | FIRED_TIME BIGINT(13) NOT NULL, 132 | SCHED_TIME BIGINT(13) NOT NULL, 133 | PRIORITY INTEGER NOT NULL, 134 | STATE VARCHAR(16) NOT NULL, 135 | JOB_NAME VARCHAR(190) NULL, 136 | JOB_GROUP VARCHAR(190) NULL, 137 | IS_NONCONCURRENT VARCHAR(1) NULL, 138 | REQUESTS_RECOVERY VARCHAR(1) NULL, 139 | PRIMARY KEY (SCHED_NAME,ENTRY_ID)) 140 | ENGINE=InnoDB; 141 | 142 | CREATE TABLE QRTZ_SCHEDULER_STATE ( 143 | SCHED_NAME VARCHAR(120) NOT NULL, 144 | INSTANCE_NAME VARCHAR(190) NOT NULL, 145 | LAST_CHECKIN_TIME BIGINT(13) NOT NULL, 146 | CHECKIN_INTERVAL BIGINT(13) NOT NULL, 147 | PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)) 148 | ENGINE=InnoDB; 149 | 150 | CREATE TABLE QRTZ_LOCKS ( 151 | SCHED_NAME VARCHAR(120) NOT NULL, 152 | LOCK_NAME VARCHAR(40) NOT NULL, 153 | PRIMARY KEY (SCHED_NAME,LOCK_NAME)) 154 | ENGINE=InnoDB; 155 | 156 | CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY); 157 | CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP); 158 | 159 | CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); 160 | CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP); 161 | CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME); 162 | CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); 163 | CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE); 164 | CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); 165 | CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); 166 | CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME); 167 | CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); 168 | CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); 169 | CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); 170 | CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); 171 | 172 | CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME); 173 | CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); 174 | CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); 175 | CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP); 176 | CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); 177 | CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); 178 | 179 | commit; 180 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/CommunityApplication.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | import javax.annotation.PostConstruct; 7 | 8 | /** 9 | * @author Ep流苏 10 | */ 11 | @SpringBootApplication 12 | public class CommunityApplication { 13 | 14 | @PostConstruct 15 | public void init() { 16 | // 解决 Netty 启动冲突问题 17 | // see Netty4Utils.setAvailableProcessors() 18 | System.setProperty("es.set.netty.runtime.available.processors", "false"); 19 | } 20 | 21 | public static void main(String[] args) { 22 | SpringApplication.run(CommunityApplication.class, args); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/annotation/LoginRequired.java: -------------------------------------------------------------------------------- 1 | package com.tassel.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 shuaiyin.zhang 10 | * @description 自定义注解:标注必须登录才能访问的方法 11 | * @date 2020/07/18 12 | */ 13 | @Target(ElementType.METHOD) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface LoginRequired { 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/config/KaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package com.tassel.config; 2 | 3 | import com.google.code.kaptcha.Producer; 4 | import com.google.code.kaptcha.impl.DefaultKaptcha; 5 | import com.google.code.kaptcha.util.Config; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.util.Properties; 10 | 11 | /** 12 | * @author Ep流苏 13 | * @Date: 2020/6/14 21:25 14 | * @Description: 15 | */ 16 | @Configuration 17 | public class KaptchaConfig { 18 | 19 | @Bean 20 | public Producer kaptchaProducer() { 21 | Properties properties = new Properties(); 22 | properties.setProperty("kaptcha.image.width", "100"); 23 | properties.setProperty("kaptcha.image.height", "40"); 24 | properties.setProperty("kaptcha.textproducer.font.size", "32"); 25 | properties.setProperty("kaptcha.textproducer.font.color", "0,0,0"); 26 | properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 27 | properties.setProperty("kaptcha.textproducer.char.length", "4"); 28 | properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); 29 | 30 | DefaultKaptcha kaptcha = new DefaultKaptcha(); 31 | Config config = new Config(properties); 32 | kaptcha.setConfig(config); 33 | return kaptcha; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.tassel.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 shuaiyin.zhang 11 | * @description 12 | * @date 2020/09/11 13 | */ 14 | @Configuration 15 | public class RedisConfig { 16 | 17 | @Bean 18 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 19 | RedisTemplate template = new RedisTemplate(); 20 | template.setConnectionFactory(redisConnectionFactory); 21 | 22 | // 设置 key 的序列化方式 23 | template.setKeySerializer(RedisSerializer.string()); 24 | // 设置普通 value 的序列化方式 25 | template.setValueSerializer(RedisSerializer.json()); 26 | // 设置 hash key 的序列化方式 27 | template.setHashKeySerializer(RedisSerializer.string()); 28 | // 设置 hash value 的序列化方式 29 | template.setHashValueSerializer(RedisSerializer.json()); 30 | 31 | template.afterPropertiesSet(); 32 | return template; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.tassel.config; 2 | 3 | import com.tassel.interceptor.LoginInterceptor; 4 | import com.tassel.interceptor.LoginRequiredInterceptor; 5 | import com.tassel.interceptor.MessageInterceptor; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | import javax.annotation.Resource; 12 | 13 | /** 14 | * @Description 15 | * @Author Zhang Shuaiyin 16 | * @Date 2020/07/05 17 | */ 18 | @Configuration 19 | public class WebMvcConfig implements WebMvcConfigurer { 20 | 21 | @Resource 22 | LoginInterceptor loginInterceptor; 23 | 24 | @Resource 25 | LoginRequiredInterceptor loginRequiredInterceptor; 26 | 27 | @Resource 28 | MessageInterceptor messageInterceptor; 29 | 30 | @Override 31 | public void addInterceptors(InterceptorRegistry registry) { 32 | registry.addInterceptor(loginInterceptor) 33 | .excludePathPatterns("/css/**", "/js/**", "/img/**"); 34 | 35 | registry.addInterceptor(loginRequiredInterceptor) 36 | .excludePathPatterns("/css/**", "/js/**", "/img/**"); 37 | 38 | registry.addInterceptor(messageInterceptor) 39 | .excludePathPatterns("/css/**", "/js/**", "/img/**"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.entity.Comment; 4 | import com.tassel.entity.DiscussPost; 5 | import com.tassel.entity.Event; 6 | import com.tassel.event.EventProducer; 7 | import com.tassel.service.CommentService; 8 | import com.tassel.service.DiscussPostService; 9 | import com.tassel.util.CommunityConstant; 10 | import com.tassel.util.HostHolder; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | 16 | import javax.annotation.Resource; 17 | import java.util.Date; 18 | 19 | /** 20 | * @author shuaiyin.zhang 21 | * @description 22 | * @date 2020/09/08 23 | */ 24 | @Controller 25 | @RequestMapping("/comment") 26 | public class CommentController implements CommunityConstant { 27 | @Resource 28 | CommentService commentService; 29 | 30 | @Resource 31 | HostHolder hostHolder; 32 | 33 | @Resource 34 | EventProducer eventProducer; 35 | 36 | @Resource 37 | DiscussPostService discussPostService; 38 | 39 | @PostMapping("/add/{discussPostId}") 40 | public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) { 41 | comment.setUserId(hostHolder.getUser().getId()); 42 | comment.setStatus(0); 43 | comment.setCreateTime(new Date()); 44 | if (comment.getTargetId() == null) { 45 | comment.setTargetId(0); 46 | } 47 | commentService.insertComment(comment); 48 | 49 | // 触发评论事件 50 | Event event = new Event().setTopic(TOPIC_COMMENT).setUserId(hostHolder.getUser().getId()).setEntityType(comment.getEntityType()).setEntityId(comment.getEntityId()).setData("postId", discussPostId); 51 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 52 | DiscussPost target = discussPostService.selectDiscussPostById(comment.getEntityId()); 53 | event.setEntityUserId(target.getUserId()); 54 | } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) { 55 | Comment target = commentService.selectCommentById(comment.getEntityId()); 56 | event.setEntityUserId(target.getUserId()); 57 | } 58 | eventProducer.fireEvent(event); 59 | 60 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 61 | // 触发发帖事件 62 | event = new Event() 63 | .setTopic(TOPIC_PUBLISH) 64 | .setUserId(comment.getUserId()) 65 | .setEntityType(ENTITY_TYPE_POST) 66 | .setEntityId(discussPostId); 67 | eventProducer.fireEvent(event); 68 | } 69 | 70 | return "redirect:/discuss/detail/" + discussPostId; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/DiscussPostController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.entity.Comment; 4 | import com.tassel.entity.DiscussPost; 5 | import com.tassel.entity.Event; 6 | import com.tassel.entity.User; 7 | import com.tassel.event.EventProducer; 8 | import com.tassel.service.CommentService; 9 | import com.tassel.service.DiscussPostService; 10 | import com.tassel.service.LikeService; 11 | import com.tassel.service.UserService; 12 | import com.tassel.util.CommunityConstant; 13 | import com.tassel.util.CommunityUtil; 14 | import com.tassel.util.HostHolder; 15 | import com.tassel.util.Page; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.ui.Model; 18 | import org.springframework.web.bind.annotation.*; 19 | 20 | import javax.annotation.Resource; 21 | import java.util.*; 22 | 23 | /** 24 | * @author shuaiyin.zhang 25 | * @description 26 | * @date 2020/09/07 27 | */ 28 | @Controller 29 | @RequestMapping("/discuss") 30 | public class DiscussPostController implements CommunityConstant { 31 | 32 | @Resource 33 | DiscussPostService discussPostService; 34 | 35 | @Resource 36 | HostHolder hostHolder; 37 | 38 | @Resource 39 | UserService userService; 40 | 41 | @Resource 42 | CommentService commentService; 43 | 44 | @Resource 45 | LikeService likeService; 46 | 47 | @Resource 48 | EventProducer eventProducer; 49 | 50 | @PostMapping("/insert") 51 | @ResponseBody 52 | public String insertDiscussPost(String title, String content) { 53 | User user = hostHolder.getUser(); 54 | if (user == null) { 55 | return CommunityUtil.getJSONString(403, "你还没有登录!"); 56 | } 57 | 58 | DiscussPost post = new DiscussPost(); 59 | post.setUserId(user.getId()); 60 | post.setTitle(title); 61 | post.setContent(content); 62 | post.setCreateTime(new Date()); 63 | post.setStatus(0); 64 | post.setType(0); 65 | post.setCommentCount(0); 66 | post.setScore(0.0); 67 | discussPostService.insertDiscussPost(post); 68 | 69 | // 触发发帖事件 70 | Event event = new Event() 71 | .setTopic(TOPIC_PUBLISH) 72 | .setUserId(user.getId()) 73 | .setEntityType(ENTITY_TYPE_POST) 74 | .setEntityId(post.getId()); 75 | eventProducer.fireEvent(event); 76 | 77 | // 报错异常情况,后续统一处理 78 | return CommunityUtil.getJSONString(0, "帖子发布成功!"); 79 | } 80 | 81 | @GetMapping("/detail/{discussPostId}") 82 | public String selectDiscussPostById(@PathVariable("discussPostId") Integer discussPostId, Model model, Page page) { 83 | // 帖子 84 | DiscussPost post = discussPostService.selectDiscussPostById(discussPostId); 85 | model.addAttribute("post", post); 86 | // 作者 87 | User user = userService.queryUserById(post.getUserId()); 88 | model.addAttribute("user", user); 89 | 90 | // 点赞数量 91 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId); 92 | model.addAttribute("likeCount", likeCount); 93 | //点赞状态 94 | int likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId); 95 | model.addAttribute("likeStatus", likeStatus); 96 | 97 | // 评论分页 98 | page.setLimit(5); 99 | page.setPath("/discuss/detail/" + discussPostId); 100 | page.setRows(post.getCommentCount()); 101 | 102 | // 评论: 给帖子的评论 103 | // 回复: 给评论的评论 104 | 105 | // 评论列表 106 | List commentList = commentService.selectCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit()); 107 | // 评论 VO 列表 108 | List> commentVoList = new ArrayList<>(); 109 | if (commentList != null) { 110 | for (Comment comment : commentList) { 111 | // 评论 VO 112 | Map commentVo = new HashMap<>(); 113 | // 评论 114 | commentVo.put("comment", comment); 115 | // 作者 116 | commentVo.put("user", userService.queryUserById(comment.getUserId())); 117 | 118 | // 点赞数量 119 | likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId()); 120 | commentVo.put("likeCount", likeCount); 121 | //点赞状态 122 | likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId()); 123 | commentVo.put("likeStatus", likeStatus); 124 | 125 | // 回复列表 126 | List replyList = commentService.selectCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE); 127 | // 回复 VO 列表 128 | List> replyVoList = new ArrayList<>(); 129 | if (replyList != null) { 130 | for (Comment reply : replyList) { 131 | Map replyVo = new HashMap<>(); 132 | // 回复 133 | replyVo.put("reply", reply); 134 | // 作者 135 | replyVo.put("user", userService.queryUserById(reply.getUserId())); 136 | // 回复目标 137 | User target = reply.getTargetId() == 0 ? null : userService.queryUserById(reply.getTargetId()); 138 | replyVo.put("target", target); 139 | 140 | // 点赞数量 141 | likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId()); 142 | replyVo.put("likeCount", likeCount); 143 | //点赞状态 144 | likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId()); 145 | replyVo.put("likeStatus", likeStatus); 146 | 147 | replyVoList.add(replyVo); 148 | } 149 | } 150 | 151 | // 回复 152 | commentVo.put("replys", replyVoList); 153 | // 回复数量 154 | int replyCount = commentService.selectCountByEntity(ENTITY_TYPE_COMMENT, comment.getId()); 155 | commentVo.put("replyCount", replyCount); 156 | commentVoList.add(commentVo); 157 | } 158 | } 159 | 160 | model.addAttribute("comments", commentVoList); 161 | 162 | return "site/discuss-detail"; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/FollowController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.entity.Event; 4 | import com.tassel.entity.User; 5 | import com.tassel.event.EventProducer; 6 | import com.tassel.service.FollowService; 7 | import com.tassel.service.UserService; 8 | import com.tassel.util.CommunityConstant; 9 | import com.tassel.util.CommunityUtil; 10 | import com.tassel.util.HostHolder; 11 | import com.tassel.util.Page; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.ui.Model; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.ResponseBody; 18 | 19 | import javax.annotation.Resource; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | /** 24 | * @author shuaiyin.zhang 25 | * @description 26 | * @date 2020/09/12 27 | */ 28 | @Controller 29 | public class FollowController implements CommunityConstant { 30 | 31 | @Resource 32 | FollowService followService; 33 | 34 | @Resource 35 | HostHolder hostHolder; 36 | 37 | @Resource 38 | UserService userService; 39 | 40 | @Resource 41 | EventProducer eventProducer; 42 | 43 | @PostMapping("/follow") 44 | @ResponseBody 45 | public String follow(int entityType, int entityId) { 46 | User user = hostHolder.getUser(); 47 | followService.follow(user.getId(), entityType, entityId); 48 | 49 | // 触发关注事件 50 | Event event = new Event().setTopic(TOPIC_FOLLOW).setUserId(hostHolder.getUser().getId()).setEntityType(entityType).setEntityId(entityId).setEntityUserId(entityId); 51 | eventProducer.fireEvent(event); 52 | 53 | return CommunityUtil.getJSONString(0, "已关注"); 54 | } 55 | 56 | @PostMapping("/unFollow") 57 | @ResponseBody 58 | public String unFollow(int entityType, int entityId) { 59 | User user = hostHolder.getUser(); 60 | followService.unFollow(user.getId(), entityType, entityId); 61 | return CommunityUtil.getJSONString(0, "已取消关注"); 62 | } 63 | 64 | @GetMapping("/followees/{userId}") 65 | public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) { 66 | User user = userService.queryUserById(userId); 67 | if (user == null) { 68 | throw new RuntimeException("用户不存在"); 69 | } 70 | model.addAttribute("user", user); 71 | 72 | page.setLimit(5); 73 | page.setPath("/followees/" + userId); 74 | page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER)); 75 | 76 | List> userList = followService.findFolloweeList(userId, page.getOffset(), page.getLimit()); 77 | if (userList != null) { 78 | for (Map map : userList) { 79 | User u = (User) map.get("user"); 80 | map.put("hasFollowed", hasFollowed(u.getId())); 81 | } 82 | } 83 | model.addAttribute("users", userList); 84 | 85 | return "/site/followee"; 86 | } 87 | 88 | @GetMapping("/followers/{userId}") 89 | public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) { 90 | User user = userService.queryUserById(userId); 91 | if (user == null) { 92 | throw new RuntimeException("用户不存在"); 93 | } 94 | model.addAttribute("user", user); 95 | 96 | page.setLimit(5); 97 | page.setPath("/followers/" + userId); 98 | page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId)); 99 | 100 | List> userList = followService.findFollowerList(userId, page.getOffset(), page.getLimit()); 101 | if (userList != null) { 102 | for (Map map : userList) { 103 | User u = (User) map.get("user"); 104 | map.put("hasFollowed", hasFollowed(u.getId())); 105 | } 106 | } 107 | model.addAttribute("users", userList); 108 | 109 | return "/site/follower"; 110 | } 111 | 112 | private boolean hasFollowed(int userId) { 113 | if (hostHolder.getUser() == null) { 114 | return false; 115 | } 116 | return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import com.tassel.entity.User; 5 | import com.tassel.service.DiscussPostService; 6 | import com.tassel.service.LikeService; 7 | import com.tassel.service.UserService; 8 | import com.tassel.util.CommunityConstant; 9 | import com.tassel.util.Page; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.ResponseBody; 14 | 15 | import javax.annotation.Resource; 16 | import java.util.ArrayList; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author Ep流苏 23 | * @Date: 2020/6/14 10:46 24 | * @Description: 25 | */ 26 | @Controller 27 | public class HomeController implements CommunityConstant { 28 | @Resource 29 | DiscussPostService discussPostService; 30 | @Resource 31 | UserService userService; 32 | 33 | @Resource 34 | LikeService likeService; 35 | 36 | @GetMapping(value = {"/", "/index"}) 37 | public String toIndexPage(Model model, Page page) { 38 | // 方法调用前,SpringMVC 会自动实例化 Model 和 Page(实体对象),并将 Page 注入 Model, 39 | // 所以,在 thymeleaf 中可以直接访问到 page 对象中的数据 40 | page.setRows(discussPostService.selectDiscussPostRows(0)); 41 | page.setPath("/index"); 42 | List list = discussPostService.selectDiscussPosts(0, page.getOffset(), page.getLimit()); 43 | List> discussPosts = new ArrayList<>(); 44 | Map map; 45 | if (list != null) { 46 | for (DiscussPost post : list) { 47 | map = new HashMap<>(); 48 | map.put("post", post); 49 | User user = userService.queryUserById(post.getUserId()); 50 | map.put("user", user); 51 | 52 | // 点赞数量 53 | long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()); 54 | map.put("likeCount", likeCount); 55 | 56 | discussPosts.add(map); 57 | } 58 | } 59 | model.addAttribute("discussPosts", discussPosts); 60 | return "/index"; 61 | } 62 | 63 | @GetMapping("/error") 64 | public String getErrorPage(){ 65 | return "/error/500"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/LikeController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.entity.Event; 4 | import com.tassel.entity.User; 5 | import com.tassel.event.EventProducer; 6 | import com.tassel.service.LikeService; 7 | import com.tassel.util.CommunityConstant; 8 | import com.tassel.util.CommunityUtil; 9 | import com.tassel.util.HostHolder; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | 14 | import javax.annotation.Resource; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | /** 19 | * @author shuaiyin.zhang 20 | * @description 21 | * @date 2020/09/11 22 | */ 23 | @Controller 24 | public class LikeController implements CommunityConstant { 25 | 26 | @Resource 27 | LikeService likeService; 28 | 29 | @Resource 30 | HostHolder hostHolder; 31 | 32 | @Resource 33 | EventProducer eventProducer; 34 | 35 | @PostMapping("/like") 36 | @ResponseBody 37 | public String like(int entityType, int entityId, int entityUserId, int postId) { 38 | User user = hostHolder.getUser(); 39 | 40 | // 点赞 41 | likeService.like(user.getId(), entityType, entityId, entityUserId); 42 | // 数量 43 | long likeCount = likeService.findEntityLikeCount(entityType, entityId); 44 | // 状态 45 | int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId); 46 | // 返回结果 47 | Map map = new HashMap<>(); 48 | map.put("likeCount", likeCount); 49 | map.put("likeStatus", likeStatus); 50 | 51 | // 触发点赞事件 52 | if (likeStatus == 1) { 53 | Event event = new Event().setTopic(TOPIC_LIKE).setUserId(hostHolder.getUser().getId()).setEntityType(entityType).setEntityId(entityId).setEntityUserId(entityUserId).setData("postId", postId); 54 | eventProducer.fireEvent(event); 55 | } 56 | 57 | return CommunityUtil.getJSONString(0, null, map); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.google.code.kaptcha.Producer; 4 | import com.tassel.entity.User; 5 | import com.tassel.service.UserService; 6 | import com.tassel.util.CommunityConstant; 7 | import com.tassel.util.CommunityUtil; 8 | import com.tassel.util.RedisKeyUtil; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.ui.Model; 17 | import org.springframework.web.bind.annotation.CookieValue; 18 | import org.springframework.web.bind.annotation.GetMapping; 19 | import org.springframework.web.bind.annotation.PathVariable; 20 | import org.springframework.web.bind.annotation.PostMapping; 21 | 22 | import javax.annotation.Resource; 23 | import javax.imageio.ImageIO; 24 | import javax.servlet.ServletOutputStream; 25 | import javax.servlet.http.Cookie; 26 | import javax.servlet.http.HttpServletResponse; 27 | import javax.servlet.http.HttpSession; 28 | import java.awt.image.BufferedImage; 29 | import java.io.IOException; 30 | import java.util.Map; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | /** 34 | * @author Ep流苏 35 | * @Date: 2020/6/14 16:17 36 | * @Description: 37 | */ 38 | @Controller 39 | public class LoginController implements CommunityConstant { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(LoginController.class); 42 | 43 | @Resource 44 | UserService userService; 45 | 46 | @Resource 47 | Producer kaptchaProducer; 48 | 49 | @Value("${server.servlet.context-path}") 50 | private String contextPath; 51 | 52 | @Resource 53 | RedisTemplate redisTemplate; 54 | 55 | @GetMapping("/register") 56 | public String toRegisterPage() { 57 | return "site/register"; 58 | } 59 | 60 | @GetMapping("/login") 61 | public String toLoginPage() { 62 | return "site/login"; 63 | } 64 | 65 | /** 66 | * 注册 67 | * 68 | * @param model 69 | * @param user 70 | * @return 71 | */ 72 | @PostMapping("/register") 73 | public String register(Model model, User user) { 74 | Map map = userService.register(user); 75 | if (map == null || map.isEmpty()) { 76 | model.addAttribute("msg", "注册成功,已经向您的邮箱发送了一封激活邮件,请尽快激活!"); 77 | model.addAttribute("target", "/index"); 78 | logger.info("邮件发送成功,需要查看请配置自己的邮箱信息,或与我联系:qq 594983498"); 79 | return "/site/operate-result"; 80 | } else { 81 | model.addAttribute("usernameMsg", map.get("usernameMsg")); 82 | model.addAttribute("passwordMsg", map.get("passwordMsg")); 83 | model.addAttribute("emailMsg", map.get("emailMsg")); 84 | return "site/register"; 85 | } 86 | } 87 | 88 | /** 89 | * 账号激活 90 | * http://localhost:8023/activation/101/code 91 | * 92 | * @param model 93 | * @param userId 94 | * @param code 95 | * @return 96 | */ 97 | @GetMapping("/activation/{userId}/{code}") 98 | public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) { 99 | int result = userService.activation(userId, code); 100 | switch (result) { 101 | case ACTIVATION_SUCCESS: 102 | model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!"); 103 | model.addAttribute("target", "/login"); 104 | logger.info("如需自己手动操作请配置自己的邮箱信息,或与我联系:qq 594983498"); 105 | break; 106 | case ACTIVATION_REPEAT: 107 | model.addAttribute("msg", "重复操作,您的账号已经激活过了!"); 108 | model.addAttribute("target", "/index"); 109 | logger.info("如需自己手动操作请配置自己的邮箱信息,或与我联系:qq 594983498"); 110 | break; 111 | default: 112 | model.addAttribute("msg", "激活失败,请检查您的激活码是否正确!"); 113 | model.addAttribute("target", "/index"); 114 | logger.info("如需自己手动操作请配置自己的邮箱信息,或与我联系:qq 594983498"); 115 | break; 116 | } 117 | return "/site/operate-result"; 118 | } 119 | 120 | /** 121 | * 获取登录验证码图片 122 | * 123 | * @param response //* @param session 124 | */ 125 | @GetMapping("/kaptcha") 126 | public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) { 127 | // 生成验证码 128 | String text = kaptchaProducer.createText(); 129 | BufferedImage image = kaptchaProducer.createImage(text); 130 | 131 | // 将验证码存入 session 132 | //session.setAttribute("kaptcha", text); 133 | 134 | // 验证码的归属 135 | String kaptchaOwner = CommunityUtil.generateUUID(); 136 | Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner); 137 | cookie.setMaxAge(60); 138 | cookie.setPath(contextPath); 139 | response.addCookie(cookie); 140 | // 将验证码存入 redis 141 | String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); 142 | redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS); 143 | 144 | // 将图片输出给浏览器 145 | response.setContentType("image/png"); 146 | ServletOutputStream os = null; 147 | try { 148 | os = response.getOutputStream(); 149 | ImageIO.write(image, "png", os); 150 | } catch (IOException e) { 151 | logger.error("响应验证码失败" + e.getMessage()); 152 | } 153 | } 154 | 155 | /** 156 | * 登录逻辑 157 | * 对于 普通参数类型,不会自动封装到 Model 对象中,所以模板引擎中无法取到, 158 | * 可以使用 request 域对象取值 159 | * 160 | * @param username 161 | * @param password 162 | * @param code 163 | * @param rememberMe 164 | * @param model //* @param session 165 | * @param response 166 | * @return 167 | */ 168 | @PostMapping("/login") 169 | public String login(String username, String password, String code, boolean rememberMe, Model model/*, HttpSession session*/, HttpServletResponse response, @CookieValue("kaptchaOwner") String kaptchaOwner) { 170 | // 检查验证码 171 | //String kaptcha = (String) session.getAttribute("kaptcha"); 172 | String kaptcha = null; 173 | if (StringUtils.isNotBlank(kaptchaOwner)) { 174 | String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); 175 | kaptcha = (String) redisTemplate.opsForValue().get(redisKey); 176 | } 177 | if (StringUtils.isAnyBlank(kaptcha, code) || !code.equalsIgnoreCase(kaptcha)) { 178 | model.addAttribute("codeMsg", "验证码不正确!"); 179 | return "/site/login"; 180 | } 181 | 182 | //检查账号、密码、失效时间 判断登录 183 | int expiredSeconds = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS; 184 | Map map = userService.login(username, password, expiredSeconds); 185 | // 登录成功 186 | if (map.containsKey("ticket")) { 187 | Cookie cookie = new Cookie("ticket", map.get("ticket").toString()); 188 | cookie.setPath(contextPath); 189 | cookie.setMaxAge(expiredSeconds); 190 | response.addCookie(cookie); 191 | return "redirect:/index"; 192 | } else { // 登录失败 193 | model.addAttribute("usernameMsg", map.get("usernameMsg")); 194 | model.addAttribute("passwordMsg", map.get("passwordMsg")); 195 | return "/site/login"; 196 | } 197 | } 198 | 199 | @GetMapping("/logout") 200 | public String logout(@CookieValue("ticket") String ticket) { 201 | userService.logout(ticket); 202 | // 默认 get 请求路径 203 | return "redirect:/login"; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import com.tassel.service.ElasticsearchService; 5 | import com.tassel.service.LikeService; 6 | import com.tassel.service.UserService; 7 | import com.tassel.util.CommunityConstant; 8 | import com.tassel.util.Page; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | 13 | import javax.annotation.Resource; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author shuaiyin.zhang 21 | * @description 22 | * @date 2020/09/16 23 | */ 24 | @Controller 25 | public class SearchController implements CommunityConstant { 26 | 27 | @Resource 28 | ElasticsearchService elasticsearchService; 29 | 30 | @Resource 31 | UserService userService; 32 | 33 | @Resource 34 | LikeService likeService; 35 | 36 | @GetMapping("/search") 37 | public String search(String keyword, Page page, Model model) { 38 | // 搜索帖子 39 | org.springframework.data.domain.Page searchResult = elasticsearchService.searchDiscussPost(keyword, page.getCurrent() - 1, page.getLimit()); 40 | // 聚合数据 41 | List> discussPosts = new ArrayList<>(); 42 | if (searchResult != null) { 43 | for (DiscussPost post : searchResult) { 44 | Map map = new HashMap<>(); 45 | // 帖子 46 | map.put("post", post); 47 | // 作者 48 | map.put("user", userService.queryUserById(post.getUserId())); 49 | // 点赞数量 50 | map.put("likeCount", likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId())); 51 | 52 | discussPosts.add(map); 53 | } 54 | } 55 | model.addAttribute("discussPosts", discussPosts); 56 | model.addAttribute("keyword", keyword); 57 | 58 | // 分页信息 59 | page.setPath("/search?keyword=" + keyword); 60 | page.setRows(searchResult == null ? 0 : (int) searchResult.getTotalElements()); 61 | return "/site/search"; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller; 2 | 3 | import com.tassel.annotation.LoginRequired; 4 | import com.tassel.entity.User; 5 | import com.tassel.service.FollowService; 6 | import com.tassel.service.LikeService; 7 | import com.tassel.service.UserService; 8 | import com.tassel.util.CommunityConstant; 9 | import com.tassel.util.CommunityUtil; 10 | import com.tassel.util.HostHolder; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.ui.Model; 16 | import org.springframework.util.StringUtils; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.multipart.MultipartFile; 22 | 23 | import javax.annotation.Resource; 24 | import javax.servlet.http.HttpServletResponse; 25 | import java.io.File; 26 | import java.io.FileInputStream; 27 | import java.io.IOException; 28 | import java.io.OutputStream; 29 | 30 | /** 31 | * @author shuaiyin.zhang 32 | * @description 33 | * @date 2020/07/18 34 | */ 35 | @Controller 36 | @RequestMapping("/user") 37 | public class UserController implements CommunityConstant { 38 | private static final Logger logger = LoggerFactory.getLogger(UserController.class); 39 | 40 | @Value("${community.path.domain}") 41 | private String domain; 42 | @Value("${community.path.upload}") 43 | private String uploadPath; 44 | @Value("${server.servlet.context-path}") 45 | private String contextPath; 46 | 47 | @Resource 48 | UserService userService; 49 | @Resource 50 | HostHolder hostHolder; 51 | 52 | @Resource 53 | LikeService likeService; 54 | 55 | @Resource 56 | FollowService followService; 57 | 58 | @LoginRequired 59 | @GetMapping("/setting") 60 | public String toSettingPage() { 61 | logger.info("前往用户设置页面"); 62 | return "/site/setting"; 63 | } 64 | 65 | @LoginRequired 66 | @PostMapping("/upload") 67 | public String uploadHeader(MultipartFile headerImage, Model model) { 68 | if (headerImage == null) { 69 | logger.error("用户上传图片内容为空"); 70 | model.addAttribute("error", "用户上传图片内容为空"); 71 | return "/site/setting"; 72 | } 73 | 74 | String fileName = headerImage.getOriginalFilename(); 75 | String suffix = fileName.substring(fileName.lastIndexOf('.')); 76 | //String[] imageSuffix = new String[]{"png", "jpg", "bmp", "gif", "svg", "webp", "jpeg"}; 77 | //List imageList = Arrays.asList(imageSuffix); 78 | //if (StringUtils.isEmpty(suffix) || !imageList.contains(suffix)) { 79 | if (StringUtils.isEmpty(suffix)) { 80 | logger.error("图片文件格式不正确"); 81 | model.addAttribute("error", "图片文件格式不正确"); 82 | return "/site/setting"; 83 | } 84 | 85 | // 生成随机文件名 86 | fileName = CommunityUtil.generateUUID() + suffix; 87 | // 路径 88 | File dest = new File(uploadPath + "/" + fileName); 89 | try { 90 | headerImage.transferTo(dest); 91 | } catch (IOException e) { 92 | logger.error("上传文件失败" + e.getMessage()); 93 | throw new RuntimeException("上传文件失败, 服务器发生异常 " + e); 94 | } 95 | 96 | // 上传成功,更新当前用户的头像路径(web访问路径) 97 | // http://localhost:8023/user/header/xxx.png 98 | User user = hostHolder.getUser(); 99 | String headerUrl = domain + CommunityUtil.contextPathJudge(contextPath) + "/user/header/" + fileName; 100 | userService.updateHeader(user.getId(), headerUrl); 101 | 102 | return "redirect:/index"; 103 | } 104 | 105 | @GetMapping("/header/{fileName}") 106 | public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) { 107 | // 文件存储路径 108 | fileName = uploadPath + "/" + fileName; 109 | // 文件后缀类型 110 | String suffix = fileName.substring(fileName.lastIndexOf(".")); 111 | // 响应图片 112 | response.setContentType("image/" + suffix); 113 | try (OutputStream os = response.getOutputStream(); FileInputStream fis = new FileInputStream(fileName)) { 114 | byte[] buffer = new byte[1024]; 115 | int b = 0; 116 | while ((b = fis.read(buffer)) != -1) { 117 | os.write(buffer, 0, b); 118 | } 119 | } catch (IOException e) { 120 | logger.error("读取头像文件失败: " + e.getMessage()); 121 | } 122 | } 123 | 124 | /** 125 | * 个人主页 126 | * 127 | * @param userId 128 | * @param model 129 | * @return 130 | */ 131 | @GetMapping("/profile/{userId}") 132 | public String getProfilePage(@PathVariable("userId") int userId, Model model) { 133 | User user = userService.queryUserById(userId); 134 | if (user == null) { 135 | throw new RuntimeException("该用户不存在"); 136 | } 137 | 138 | // 用户 139 | model.addAttribute("user", user); 140 | // 获赞数量 141 | int likeCount = likeService.findUserLikeCount(userId); 142 | model.addAttribute("likeCount", likeCount); 143 | 144 | // 关注数量 145 | long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER); 146 | model.addAttribute("followeeCount", followeeCount); 147 | // 粉丝数量 148 | long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId); 149 | model.addAttribute("followerCount", followerCount); 150 | // 是否当前用户已关注此用户 151 | boolean hasFollowed = false; 152 | if (hostHolder.getUser() != null) { 153 | hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); 154 | } 155 | model.addAttribute("hasFollowed", hasFollowed); 156 | 157 | return "/site/profile"; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/controller/advice/ExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package com.tassel.controller.advice; 2 | 3 | import com.tassel.util.CommunityUtil; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.io.PrintWriter; 14 | 15 | /** 16 | * @author shuaiyin.zhang 17 | * @description 18 | * @date 2020/09/09 19 | */ 20 | @ControllerAdvice(annotations = Controller.class) //只对标注Controller注解的类进行处理 21 | public class ExceptionAdvice { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class); 24 | 25 | @ExceptionHandler({Exception.class}) //注解参数是拦截的异常类型 26 | public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException { 27 | logger.error("服务器发生异常: {}", e.getMessage()); 28 | for (StackTraceElement element : e.getStackTrace()) { 29 | logger.error(element.toString()); 30 | } 31 | String header = request.getHeader("x-requested-with"); 32 | // 当前请求为异步请求 33 | if ("XMLHttpRequest".equals(header)) { 34 | response.setContentType("application/json;charset=utf-8"); 35 | PrintWriter writer = response.getWriter(); 36 | writer.write(CommunityUtil.getJSONString(1, "服务器异常!")); 37 | } else { 38 | // 请求为网页请求 39 | response.sendRedirect(CommunityUtil.contextPathJudge(request.getContextPath()) + "/error"); 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package com.tassel.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * (Comment)实体类 10 | * 11 | * @author Ep流苏 12 | * @since 2020-09-07 16:24:29 13 | */ 14 | @Data 15 | public class Comment implements Serializable { 16 | private static final long serialVersionUID = -66137005452989538L; 17 | 18 | private Integer id; 19 | 20 | private Integer userId; 21 | 22 | private Integer entityType; 23 | 24 | private Integer entityId; 25 | 26 | private Integer targetId; 27 | 28 | private String content; 29 | 30 | private Integer status; 31 | 32 | private Date createTime; 33 | } -------------------------------------------------------------------------------- /src/main/java/com/tassel/entity/DiscussPost.java: -------------------------------------------------------------------------------- 1 | package com.tassel.entity; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.elasticsearch.annotations.Document; 6 | import org.springframework.data.elasticsearch.annotations.Field; 7 | import org.springframework.data.elasticsearch.annotations.FieldType; 8 | 9 | import java.util.Date; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * (DiscussPost)实体类 14 | * 15 | * @author Ep流苏 16 | * @since 2020-06-14 09:43:07 17 | */ 18 | @Data 19 | @Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3) 20 | public class DiscussPost implements Serializable { 21 | private static final long serialVersionUID = -90583991338816189L; 22 | 23 | @Id 24 | private Integer id; 25 | 26 | @Field(type = FieldType.Integer) 27 | private Integer userId; 28 | 29 | @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 30 | private String title; 31 | 32 | @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 33 | private String content; 34 | 35 | /** 36 | * 0-普通; 1-置顶; 37 | */ 38 | @Field(type = FieldType.Integer) 39 | private Integer type; 40 | 41 | /** 42 | * 0-正常; 1-精华; 2-拉黑; 43 | */ 44 | @Field(type = FieldType.Integer) 45 | private Integer status; 46 | 47 | @Field(type = FieldType.Date) 48 | private Date createTime; 49 | 50 | @Field(type = FieldType.Integer) 51 | private Integer commentCount; 52 | 53 | @Field(type = FieldType.Double) 54 | private Double score; 55 | } -------------------------------------------------------------------------------- /src/main/java/com/tassel/entity/Event.java: -------------------------------------------------------------------------------- 1 | package com.tassel.entity; 2 | 3 | import lombok.Data; 4 | import org.apache.commons.lang3.ObjectUtils; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author shuaiyin.zhang 11 | * @description 12 | * @date 2020/09/13 13 | */ 14 | public class Event { 15 | private String topic; 16 | private int userId; 17 | private int entityType; 18 | private int entityId; 19 | private int entityUserId; 20 | private Map data = new HashMap<>(); 21 | 22 | public String getTopic() { 23 | return topic; 24 | } 25 | 26 | public Event setTopic(String topic) { 27 | this.topic = topic; 28 | return this; 29 | } 30 | 31 | public int getUserId() { 32 | return userId; 33 | } 34 | 35 | public Event setUserId(int userId) { 36 | this.userId = userId; 37 | return this; 38 | } 39 | 40 | public int getEntityType() { 41 | return entityType; 42 | } 43 | 44 | public Event setEntityType(int entityType) { 45 | this.entityType = entityType; 46 | return this; 47 | } 48 | 49 | public int getEntityId() { 50 | return entityId; 51 | } 52 | 53 | public Event setEntityId(int entityId) { 54 | this.entityId = entityId; 55 | return this; 56 | } 57 | 58 | public int getEntityUserId() { 59 | return entityUserId; 60 | } 61 | 62 | public Event setEntityUserId(int entityUserId) { 63 | this.entityUserId = entityUserId; 64 | return this; 65 | } 66 | 67 | public Map getData() { 68 | return data; 69 | } 70 | 71 | public Event setData(String key, Object value) { 72 | this.data.put(key, value); 73 | return this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/entity/LoginTicket.java: -------------------------------------------------------------------------------- 1 | package com.tassel.entity; 2 | 3 | import lombok.ToString; 4 | 5 | import java.util.Date; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * (LoginTicket)实体类 10 | * 11 | * @author Ep流苏 12 | * @since 2020-07-05 09:39:34 13 | */ 14 | @ToString 15 | public class LoginTicket implements Serializable { 16 | private static final long serialVersionUID = 438342979500803230L; 17 | 18 | private Integer id; 19 | 20 | private Integer userId; 21 | 22 | private String ticket; 23 | /** 24 | * 0-有效; 1-无效; 25 | */ 26 | private Integer status; 27 | 28 | private Date expired; 29 | 30 | public Integer getId() { 31 | return id; 32 | } 33 | 34 | public void setId(Integer id) { 35 | this.id = id; 36 | } 37 | 38 | public Integer getUserId() { 39 | return userId; 40 | } 41 | 42 | public void setUserId(Integer userId) { 43 | this.userId = userId; 44 | } 45 | 46 | public String getTicket() { 47 | return ticket; 48 | } 49 | 50 | public void setTicket(String ticket) { 51 | this.ticket = ticket; 52 | } 53 | 54 | public Integer getStatus() { 55 | return status; 56 | } 57 | 58 | public void setStatus(Integer status) { 59 | this.status = status; 60 | } 61 | 62 | public Date getExpired() { 63 | return expired; 64 | } 65 | 66 | public void setExpired(Date expired) { 67 | this.expired = expired; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/java/com/tassel/entity/Message.java: -------------------------------------------------------------------------------- 1 | package com.tassel.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * (Message)实体类 10 | * 11 | * @author Ep流苏 12 | * @since 2020-09-08 15:32:40 13 | */ 14 | @Data 15 | public class Message implements Serializable { 16 | private static final long serialVersionUID = -58858664784963809L; 17 | 18 | private Integer id; 19 | 20 | private Integer fromId; 21 | 22 | private Integer toId; 23 | 24 | private String conversationId; 25 | 26 | private String content; 27 | /** 28 | * 0-未读;1-已读;2-删除; 29 | */ 30 | private Integer status; 31 | 32 | private Date createTime; 33 | } -------------------------------------------------------------------------------- /src/main/java/com/tassel/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.tassel.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * (User)实体类 10 | * 11 | * @author Ep流苏 12 | * @since 2020-06-14 09:43:19 13 | */ 14 | @Data 15 | public class User implements Serializable { 16 | private static final long serialVersionUID = 708453489701305063L; 17 | 18 | private Integer id; 19 | 20 | private String username; 21 | 22 | private String password; 23 | 24 | private String salt; 25 | 26 | private String email; 27 | /** 28 | * 0-普通用户; 1-超级管理员; 2-版主; 29 | */ 30 | private Integer type; 31 | /** 32 | * 0-未激活; 1-已激活; 33 | */ 34 | private Integer status; 35 | 36 | private String activationCode; 37 | 38 | private String headerUrl; 39 | 40 | private Date createTime; 41 | 42 | } -------------------------------------------------------------------------------- /src/main/java/com/tassel/event/EventConsumer.java: -------------------------------------------------------------------------------- 1 | package com.tassel.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tassel.entity.DiscussPost; 5 | import com.tassel.entity.Event; 6 | import com.tassel.entity.Message; 7 | import com.tassel.service.DiscussPostService; 8 | import com.tassel.service.ElasticsearchService; 9 | import com.tassel.service.MessageService; 10 | import com.tassel.util.CommunityConstant; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.kafka.clients.consumer.ConsumerRecord; 13 | import org.springframework.kafka.annotation.KafkaListener; 14 | import org.springframework.stereotype.Component; 15 | 16 | import javax.annotation.Resource; 17 | import java.util.Date; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author shuaiyin.zhang 23 | * @description 24 | * @date 2020/09/13 25 | */ 26 | @Component 27 | @Slf4j 28 | public class EventConsumer implements CommunityConstant { 29 | 30 | @Resource 31 | MessageService messageService; 32 | 33 | @Resource 34 | DiscussPostService discussPostService; 35 | 36 | @Resource 37 | ElasticsearchService elasticsearchService; 38 | 39 | @KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW}) 40 | public void handleTopicMessage(ConsumerRecord record) { 41 | if (record == null || record.value() == null) { 42 | log.error("消息内容为空!"); 43 | } 44 | 45 | Event event = JSONObject.parseObject(record.value().toString(), Event.class); 46 | if (event == null) { 47 | log.error("消息格式错误!"); 48 | return; 49 | } 50 | 51 | // 发送站内通知 52 | Message message = new Message(); 53 | message.setFromId(SYSTEM_USER_ID); 54 | message.setToId(event.getEntityUserId()); 55 | message.setConversationId(event.getTopic()); 56 | message.setStatus(0); 57 | message.setCreateTime(new Date()); 58 | 59 | Map content = new HashMap<>(); 60 | content.put("userId", event.getUserId()); 61 | content.put("entityType", event.getEntityType()); 62 | content.put("entityId", event.getEntityId()); 63 | 64 | if (!event.getData().isEmpty()) { 65 | for (Map.Entry entry : event.getData().entrySet()) { 66 | content.put(entry.getKey(), entry.getValue()); 67 | } 68 | } 69 | message.setContent(JSONObject.toJSONString(content)); 70 | 71 | messageService.insertMessage(message); 72 | } 73 | 74 | /** 75 | * 消费发帖事件 76 | */ 77 | @KafkaListener(topics = {TOPIC_PUBLISH}) 78 | public void handlePublishMessage(ConsumerRecord record) { 79 | if (record == null || record.value() == null) { 80 | log.error("消息内容为空!"); 81 | } 82 | 83 | Event event = JSONObject.parseObject(record.value().toString(), Event.class); 84 | if (event == null) { 85 | log.error("消息格式错误!"); 86 | return; 87 | } 88 | 89 | DiscussPost post = discussPostService.selectDiscussPostById(event.getEntityId()); 90 | elasticsearchService.saveDiscussPost(post); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/event/EventProducer.java: -------------------------------------------------------------------------------- 1 | package com.tassel.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tassel.entity.Event; 5 | import org.springframework.kafka.core.KafkaTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.Resource; 9 | 10 | /** 11 | * @author shuaiyin.zhang 12 | * @description 13 | * @date 2020/09/13 14 | */ 15 | @Component 16 | public class EventProducer { 17 | 18 | @Resource 19 | KafkaTemplate kafkaTemplate; 20 | 21 | /** 22 | * 处理事件 23 | * @param event 24 | */ 25 | public void fireEvent(Event event) { 26 | // 将事件发布到指定的主题 27 | kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/interceptor/LoginInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.tassel.interceptor; 2 | 3 | import com.tassel.entity.LoginTicket; 4 | import com.tassel.entity.User; 5 | import com.tassel.service.UserService; 6 | import com.tassel.util.CookieUtil; 7 | import com.tassel.util.HostHolder; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | import org.springframework.web.servlet.ModelAndView; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.util.Date; 16 | 17 | /** 18 | * @Description 19 | * @Author Zhang Shuaiyin 20 | * @Date 2020/07/12 21 | */ 22 | @Component 23 | public class LoginInterceptor implements HandlerInterceptor { 24 | 25 | @Autowired 26 | UserService userService; 27 | @Autowired 28 | HostHolder hostHolder; 29 | 30 | @Override 31 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 32 | // 从 cookie 中获取登录凭证 33 | String ticket = CookieUtil.getValue(request, "ticket"); 34 | if (ticket != null) { 35 | // 查询凭证 36 | LoginTicket loginTicket = userService.findLoginTicket(ticket); 37 | // 检查凭证是否有效 38 | if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { 39 | // 根据凭证查询用户 40 | User user = userService.queryUserById(loginTicket.getUserId()); 41 | // 在本次请求中持有用户--保存用户信息 42 | hostHolder.setUser(user); 43 | } 44 | } 45 | return true; 46 | } 47 | 48 | @Override 49 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 50 | User user = hostHolder.getUser(); 51 | if (user != null && modelAndView != null) { 52 | modelAndView.addObject("loginUser", user); 53 | } 54 | } 55 | 56 | @Override 57 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 58 | hostHolder.clear(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/interceptor/LoginRequiredInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.tassel.interceptor; 2 | 3 | import com.tassel.annotation.LoginRequired; 4 | import com.tassel.util.CommunityUtil; 5 | import com.tassel.util.HostHolder; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.method.HandlerMethod; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | 12 | import javax.annotation.Resource; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.lang.reflect.Method; 16 | 17 | /** 18 | * @author shuaiyin.zhang 19 | * @description 20 | * @date 2020/07/18 21 | */ 22 | @Component 23 | public class LoginRequiredInterceptor implements HandlerInterceptor { 24 | private static final Logger logger = LoggerFactory.getLogger(LoginRequiredInterceptor.class); 25 | 26 | @Resource 27 | private HostHolder hostHolder; 28 | 29 | @Override 30 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 31 | logger.info("请求拦截: {}", request.getRequestURI()); 32 | if (handler instanceof HandlerMethod) { 33 | HandlerMethod handlerMethod = (HandlerMethod) handler; 34 | Method method = handlerMethod.getMethod(); 35 | LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); 36 | if (loginRequired != null && hostHolder.getUser() == null) { 37 | logger.info("用户未登录,即将跳转到登录页面: {}", CommunityUtil.contextPathJudge(request.getContextPath()) + "/login"); 38 | response.sendRedirect(CommunityUtil.contextPathJudge(request.getContextPath()) + "/login"); 39 | return false; 40 | } 41 | } 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/interceptor/MessageInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.tassel.interceptor; 2 | 3 | import com.tassel.entity.User; 4 | import com.tassel.service.MessageService; 5 | import com.tassel.util.HostHolder; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | import org.springframework.web.servlet.ModelAndView; 9 | 10 | import javax.annotation.Resource; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | /** 15 | * @author shuaiyin.zhang 16 | * @description 17 | * @date 2020/09/13 18 | */ 19 | @Component 20 | public class MessageInterceptor implements HandlerInterceptor { 21 | 22 | @Resource 23 | HostHolder hostHolder; 24 | 25 | @Resource 26 | MessageService messageService; 27 | 28 | @Override 29 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 30 | User user = hostHolder.getUser(); 31 | if (user != null && modelAndView != null) { 32 | int letterUnreadCount = messageService.selectLetterUnreadCount(user.getId(), null); 33 | int noticeUnreadCount = messageService.selectNoticeUnreadCount(user.getId(), null); 34 | modelAndView.addObject("allUnreadCount", letterUnreadCount + noticeUnreadCount); } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.Comment; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Select; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author shuaiyin.zhang 11 | * @description 12 | * @date 2020/09/07 13 | */ 14 | @Mapper 15 | public interface CommentMapper { 16 | 17 | /** 18 | * 根据评论对象实体分页查询评论列表 19 | * 20 | * @param entityType 评论对象,用户或者帖子 21 | * @param entityId 评论对象 id 22 | * @param offset 23 | * @param limit 24 | * @return 25 | */ 26 | List selectCommentsByEntity(int entityType, int entityId, int offset, int limit); 27 | 28 | /** 29 | * 查询评论条目数 30 | * 31 | * @param entityType 32 | * @param entityId 33 | * @return 34 | */ 35 | Integer selectCountByEntity(int entityType, int entityId); 36 | 37 | /** 38 | * 新增评论 39 | * 40 | * @param comment 41 | * @return 42 | */ 43 | Integer insertComment(Comment comment); 44 | 45 | /** 46 | * 查询帖子根据 id 47 | * 48 | * @param id 49 | * @return 50 | */ 51 | @Select("select `id`, `user_id`, `entity_type`, `entity_id`, `target_id`, `content`, `status`, `create_time` from community.comment where `id` = #{id}") 52 | Comment selectCommentById(int id); 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/mapper/DiscussPostMapper.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import org.apache.ibatis.annotations.Insert; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | import org.apache.ibatis.annotations.Update; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * @author Ep流苏 13 | * @Date: 2020/6/14 9:45 14 | * @Description: 15 | */ 16 | @Mapper 17 | public interface DiscussPostMapper { 18 | 19 | /** 20 | * 查询列表 21 | * 22 | * @param userId 可选值, 用户 id 23 | * @param offset 起始数据号 24 | * @param limit 每页数据条数 25 | * @return 26 | */ 27 | List selectDiscussPosts(int userId, int offset, int limit); 28 | 29 | /** 30 | * 查询行数 31 | * 32 | * @param userId 如果参数只有一个,而且可能要动态拼接 SQL (等),必须加 @Param 注解 33 | * @return 34 | */ 35 | Integer selectDiscussPostRows(@Param("userId") int userId); 36 | 37 | /** 38 | * 增加帖子 39 | * 40 | * @param discussPost 41 | * @return 42 | */ 43 | Integer insertDiscussPost(DiscussPost discussPost); 44 | 45 | /** 46 | * 查看帖子详情 47 | * 48 | * @param id 49 | * @return 50 | */ 51 | DiscussPost selectDiscussPostById(int id); 52 | 53 | /** 54 | * 更新帖子评论数量 55 | * 56 | * @param id 57 | * @param commentCount 58 | * @return 59 | */ 60 | Integer updateCommentCount(int id, int commentCount); 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/mapper/LoginTicketMapper.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.LoginTicket; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | /** 7 | * @Description 8 | * @Author Zhang Shuaiyin 9 | * @Date 2020/07/05 10 | */ 11 | @Mapper 12 | @Deprecated 13 | public interface LoginTicketMapper { 14 | 15 | /** 16 | * 添加登录凭证 17 | * 18 | * @param loginTicket 19 | * @return 20 | */ 21 | @Insert({ 22 | "insert into login_ticket(user_id, ticket, status, expired) ", 23 | "values(#{userId}, #{ticket}, #{status}, #{expired})" 24 | }) 25 | @Options(useGeneratedKeys = true, keyColumn = "id") 26 | int insertLoginTicket(LoginTicket loginTicket); 27 | 28 | /** 29 | * 通过登录凭证获取登录信息 30 | * 31 | * @param ticket 32 | * @return 33 | */ 34 | @Select({ 35 | "select id, user_id, ticket, status, expired ", 36 | "from login_ticket where ticket = #{ticket}" 37 | }) 38 | LoginTicket selectByTicket(String ticket); 39 | 40 | /** 41 | * 更新状态 42 | * 43 | * @param ticket 44 | * @param status 45 | * @return 46 | */ 47 | @Update({ //演示动态SQL使用方法,if无实际意义 48 | "" 55 | }) 56 | int updateStatus(String ticket, int status); 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/mapper/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.Message; 4 | import org.apache.ibatis.annotations.Mapper; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author shuaiyin.zhang 10 | * @description 11 | * @date 2020/09/08 12 | */ 13 | @Mapper 14 | public interface MessageMapper { 15 | 16 | /** 17 | * 查询当前用户的会话列表,针对每个会话只返回一条最新的私信。 18 | * 19 | * @param userId 20 | * @param offset 21 | * @param limit 22 | * @return 23 | */ 24 | List selectConversations(int userId, int offset, int limit); 25 | 26 | /** 27 | * 查询当前用户的会话数量 28 | * 29 | * @param userId 30 | * @return 31 | */ 32 | Integer selectConversationCount(int userId); 33 | 34 | /** 35 | * 查询某个会话所包含的私信列表 36 | * 37 | * @param conversationId 38 | * @param offset 39 | * @param limit 40 | * @return 41 | */ 42 | List selectLetters(String conversationId, int offset, int limit); 43 | 44 | /** 45 | * 查询某个会话所包含的私信数量 46 | * 47 | * @param conversationId 48 | * @return 49 | */ 50 | Integer selectLetterCount(String conversationId); 51 | 52 | /** 53 | * 查询未读私信数量 54 | * 55 | * @param userId 56 | * @param conversationId 57 | * @return 58 | */ 59 | Integer selectLetterUnreadCount(int userId, String conversationId); 60 | 61 | /** 62 | * 新增消息 63 | * 64 | * @param message 65 | * @return 66 | */ 67 | Integer insertMessage(Message message); 68 | 69 | /** 70 | * 修改消息的状态 71 | * 72 | * @param ids 73 | * @param status 74 | * @return 75 | */ 76 | Integer updateStatus(List ids, int status); 77 | 78 | /** 79 | * 查询某个主题下最新的通知 80 | * 81 | * @param userId 82 | * @param topic 83 | * @return 84 | */ 85 | Message selectLatestNotice(int userId, String topic); 86 | 87 | /** 88 | * 查询某个主题所包含的通知的数量 89 | * 90 | * @param userId 91 | * @param topic 92 | * @return 93 | */ 94 | Integer selectNoticeCount(int userId, String topic); 95 | 96 | /** 97 | * 查询未读的通知的数量 98 | * 99 | * @param userId 100 | * @param topic 101 | * @return 102 | */ 103 | int selectNoticeUnreadCount(int userId, String topic); 104 | 105 | /** 106 | * 查询某个主题所包含的通知列表 107 | * 108 | * @param userId 109 | * @param topic 110 | * @param offset 111 | * @param limit 112 | * @return 113 | */ 114 | List selectNotices(int userId, String topic, int offset, int limit); 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.User; 4 | import org.apache.ibatis.annotations.Mapper; 5 | 6 | /** 7 | * @author Ep流苏 8 | * @Date: 2020/6/14 10:25 9 | * @Description: 10 | */ 11 | @Mapper 12 | public interface UserMapper { 13 | 14 | /** 15 | * 根据 id 查询用户 16 | * 17 | * @param id 18 | * @return 19 | */ 20 | User queryUserById(int id); 21 | 22 | /** 23 | * 根据名字查询用户 24 | * 25 | * @param username 26 | * @return 27 | */ 28 | User queryByName(String username); 29 | 30 | /** 31 | * 根据邮箱查询用户 32 | * 33 | * @param email 34 | * @return 35 | */ 36 | User queryByEmail(String email); 37 | 38 | 39 | /** 40 | * 插入用户 41 | * 42 | * @param user 43 | * @return 44 | */ 45 | Integer insertUser(User user); 46 | 47 | Integer updateStatus(Integer status, Integer id); 48 | 49 | Integer updateHeader(String headerUrl, Integer id); 50 | 51 | Integer updatePassword(String password, Integer id); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/mapper/elasticsearch/DiscussPostRepository.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper.elasticsearch; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * @author shuaiyin.zhang 9 | * @description 10 | * @date 2020/09/15 11 | */ 12 | @Repository 13 | public interface DiscussPostRepository extends ElasticsearchRepository { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | import com.tassel.entity.Comment; 4 | import org.apache.ibatis.annotations.Select; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author shuaiyin.zhang 10 | * @description 11 | * @date 2020/09/07 12 | */ 13 | public interface CommentService { 14 | 15 | /** 16 | * 根据评论对象实体分页查询评论列表 17 | * 18 | * @param entityType 评论对象,评论或者帖子 对帖子评论或对评论进行评论 19 | * @param entityId 评论对象 id 20 | * @param offset 21 | * @param limit 22 | * @return 23 | */ 24 | List selectCommentsByEntity(int entityType, int entityId, int offset, int limit); 25 | 26 | /** 27 | * 查询评论条目数 28 | * 29 | * @param entityType 30 | * @param entityId 31 | * @return 32 | */ 33 | Integer selectCountByEntity(int entityType, int entityId); 34 | 35 | /** 36 | * 新增评论 37 | * 38 | * @param comment 39 | * @return 40 | */ 41 | Integer insertComment(Comment comment); 42 | 43 | /** 44 | * 查询帖子根据 id 45 | * 46 | * @param id 47 | * @return 48 | */ 49 | Comment selectCommentById(int id); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/DiscussPostService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import org.apache.ibatis.annotations.Param; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Ep流苏 10 | * @Date: 2020/6/14 10:33 11 | * @Description: 12 | */ 13 | public interface DiscussPostService { 14 | 15 | /** 16 | * 查询 17 | * 18 | * @param userId 可选值, 用户 id 19 | * @param offset 起始数据号 20 | * @param limit 每页数据条数 21 | * @return 22 | */ 23 | List selectDiscussPosts(int userId, int offset, int limit); 24 | 25 | /** 26 | * 查询行数 27 | * 28 | * @param userId 如果参数只有一个,而且可能要动态拼接SQL,必须加 @Param 注解 29 | * @return 30 | */ 31 | Integer selectDiscussPostRows(@Param("userId") int userId); 32 | 33 | /** 34 | * 发布帖子 35 | * 36 | * @param post 37 | * @return 38 | */ 39 | Integer insertDiscussPost(DiscussPost post); 40 | 41 | /** 42 | * 查看帖子详情 43 | * 44 | * @param id 45 | * @return 46 | */ 47 | DiscussPost selectDiscussPostById(int id); 48 | 49 | /** 50 | * 更新帖子评论数量 51 | * 52 | * @param id 53 | * @param commentCount 54 | * @return 55 | */ 56 | Integer updateCommentCount(int id, int commentCount); 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/ElasticsearchService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import org.springframework.data.domain.Page; 5 | 6 | /** 7 | * @author shuaiyin.zhang 8 | * @description 9 | * @date 2020/09/16 10 | */ 11 | public interface ElasticsearchService { 12 | 13 | /** 14 | * 向 Elasticsearch 服务器增加帖子信息 15 | * 16 | * @param post 17 | */ 18 | void saveDiscussPost(DiscussPost post); 19 | 20 | /** 21 | * 通过 id 从 Elasticsearch 服务器删除记录 22 | * 23 | * @param id 24 | */ 25 | void deleteDiscussPost(int id); 26 | 27 | /** 28 | * 查询 29 | * 30 | * @param keyword 31 | * @param current 32 | * @param limit 33 | * @return 34 | */ 35 | Page searchDiscussPost(String keyword, int current, int limit); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/FollowService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author shuaiyin.zhang 8 | * @description 9 | * @date 2020/09/12 10 | */ 11 | public interface FollowService { 12 | 13 | /** 14 | * 关注 15 | * 16 | * @param userId 17 | * @param entityType 18 | * @param entityId 19 | */ 20 | void follow(int userId, int entityType, int entityId); 21 | 22 | /** 23 | * 取消关注 24 | * 25 | * @param userId 26 | * @param entityType 27 | * @param entityId 28 | */ 29 | void unFollow(int userId, int entityType, int entityId); 30 | 31 | /** 32 | * 查询关注的实体的数量 33 | * 34 | * @param userId 35 | * @param entityType 36 | * @return 37 | */ 38 | long findFolloweeCount(int userId, int entityType); 39 | 40 | /** 41 | * 查询实体的粉丝的数量 42 | * 43 | * @param entityType 44 | * @param entityId 45 | * @return 46 | */ 47 | long findFollowerCount(int entityType, int entityId); 48 | 49 | /** 50 | * 查询当前用户是否已关注该实体 51 | * 52 | * @param userId 53 | * @param entityType 54 | * @param entityId 55 | * @return 56 | */ 57 | boolean hasFollowed(int userId, int entityType, int entityId); 58 | 59 | /** 60 | * 查询某用户关注的人 61 | * 62 | * @param userId 63 | * @param offset 64 | * @param limit 65 | * @return 66 | */ 67 | List> findFolloweeList(int userId, int offset, int limit); 68 | 69 | /** 70 | * 查询某用户的粉丝 71 | * 72 | * @param userId 73 | * @param offset 74 | * @param limit 75 | * @return 76 | */ 77 | List> findFollowerList(int userId, int offset, int limit); 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/LikeService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | /** 4 | * @author shuaiyin.zhang 5 | * @description 6 | * @date 2020/09/11 7 | */ 8 | public interface LikeService { 9 | 10 | /** 11 | * 点赞:存入 redis 根据 userId 判断该用户是否对 entity 点赞 12 | * 13 | * @param userId 14 | * @param entityType 15 | * @param entityId 16 | * @param entityUserId 17 | */ 18 | void like(int userId, int entityType, int entityId, int entityUserId); 19 | 20 | /** 21 | * 查询某实体点赞的数量 22 | * 23 | * @param entityType 24 | * @param entityId 25 | * @return 26 | */ 27 | long findEntityLikeCount(int entityType, int entityId); 28 | 29 | /** 30 | * 查询某人对某实体的点赞状态 31 | * 32 | * @param userId 33 | * @param entityType 34 | * @param entityId 35 | * @return 36 | */ 37 | int findEntityLikeStatus(int userId, int entityType, int entityId); 38 | 39 | /** 40 | * 查询某个用户获得赞的数量 41 | * 42 | * @param userId 43 | * @return 44 | */ 45 | int findUserLikeCount(int userId); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | import com.tassel.entity.Message; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author shuaiyin.zhang 9 | * @description 10 | * @date 2020/09/08 11 | */ 12 | public interface MessageService { 13 | 14 | /** 15 | * 查询当前用户的会话列表,针对每个会话只返回一条最新的私信。 16 | * 17 | * @param userId 18 | * @param offset 19 | * @param limit 20 | * @return 21 | */ 22 | List selectConversations(int userId, int offset, int limit); 23 | 24 | /** 25 | * 查询当前用户的会话数量 26 | * 27 | * @param userId 28 | * @return 29 | */ 30 | Integer selectConversationCount(int userId); 31 | 32 | /** 33 | * 查询某个会话所包含的私信列表 34 | * 35 | * @param conversationId 36 | * @param offset 37 | * @param limit 38 | * @return 39 | */ 40 | List selectLetters(String conversationId, int offset, int limit); 41 | 42 | /** 43 | * 查询某个会话所包含的私信数量 44 | * 45 | * @param conversationId 46 | * @return 47 | */ 48 | Integer selectLetterCount(String conversationId); 49 | 50 | /** 51 | * 查询未读私信数量 52 | * 53 | * @param userId 54 | * @param conversationId 55 | * @return 56 | */ 57 | Integer selectLetterUnreadCount(int userId, String conversationId); 58 | 59 | /** 60 | * 新增消息 61 | * 62 | * @param message 63 | * @return 64 | */ 65 | Integer insertMessage(Message message); 66 | 67 | /** 68 | * 读取消息 69 | * 70 | * @param ids 71 | * @return 72 | */ 73 | Integer readMessage(List ids); 74 | 75 | /** 76 | * 查询某个主题下最新的通知 77 | * 78 | * @param userId 79 | * @param topic 80 | * @return 81 | */ 82 | Message selectLatestNotice(int userId, String topic); 83 | 84 | /** 85 | * 查询某个主题所包含的通知的数量 86 | * 87 | * @param userId 88 | * @param topic 89 | * @return 90 | */ 91 | Integer selectNoticeCount(int userId, String topic); 92 | 93 | /** 94 | * 查询未读的通知的数量 95 | * 96 | * @param userId 97 | * @param topic 98 | * @return 99 | */ 100 | Integer selectNoticeUnreadCount(int userId, String topic); 101 | 102 | /** 103 | * 查询某个主题所包含的通知列表 104 | * 105 | * @param userId 106 | * @param topic 107 | * @param offset 108 | * @param limit 109 | * @return 110 | */ 111 | List selectNotices(int userId, String topic, int offset, int limit); 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service; 2 | 3 | import com.tassel.entity.LoginTicket; 4 | import com.tassel.entity.User; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * @author Ep流苏 10 | * @Date: 2020/6/14 10:37 11 | * @Description: 12 | */ 13 | public interface UserService { 14 | 15 | /** 16 | * 根据 id 查询用户 17 | * 18 | * @param id 19 | * @return 20 | */ 21 | User queryUserById(int id); 22 | 23 | /** 24 | * 根据用户名查询用户 25 | * 26 | * @param name 27 | * @return 28 | */ 29 | User queryUserByName(String name); 30 | 31 | /** 32 | * 注册 33 | * 34 | * @param user 35 | * @return 36 | */ 37 | Map register(User user); 38 | 39 | /** 40 | * 激活逻辑 41 | * 42 | * @param userId 43 | * @param code 激活码 44 | * @return 45 | */ 46 | Integer activation(int userId, String code); 47 | 48 | /** 49 | * 登录验证, 返回失败信息或登录凭证 50 | * 51 | * @param username 52 | * @param password 53 | * @param expiredSecond 54 | * @return 55 | */ 56 | Map login(String username, String password, int expiredSecond); 57 | 58 | /** 59 | * 退出登录 60 | * 61 | * @param ticket 62 | */ 63 | void logout(String ticket); 64 | 65 | /** 66 | * 查询登录凭证 67 | * 68 | * @param ticket 69 | * @return 70 | */ 71 | LoginTicket findLoginTicket(String ticket); 72 | 73 | /** 74 | * 更新(设置)用户头像地址 75 | * 76 | * @param userId 77 | * @param headerUrl 78 | * @return 79 | */ 80 | int updateHeader(Integer userId, String headerUrl); 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.entity.Comment; 4 | import com.tassel.mapper.CommentMapper; 5 | import com.tassel.service.CommentService; 6 | import com.tassel.service.DiscussPostService; 7 | import com.tassel.util.CommunityConstant; 8 | import com.tassel.util.SensitiveFilter; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Isolation; 11 | import org.springframework.transaction.annotation.Propagation; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.web.util.HtmlUtils; 14 | 15 | import javax.annotation.Resource; 16 | import java.util.List; 17 | 18 | /** 19 | * @author shuaiyin.zhang 20 | * @description 21 | * @date 2020/09/07 22 | */ 23 | @Service 24 | public class CommentServiceImpl implements CommentService, CommunityConstant { 25 | @Resource 26 | CommentMapper commentMapper; 27 | 28 | @Resource 29 | SensitiveFilter sensitiveFilter; 30 | 31 | @Resource 32 | DiscussPostService discussPostService; 33 | 34 | @Override 35 | public List selectCommentsByEntity(int entityType, int entityId, int offset, int limit) { 36 | return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit); 37 | } 38 | 39 | @Override 40 | public Integer selectCountByEntity(int entityType, int entityId) { 41 | return commentMapper.selectCountByEntity(entityType, entityId); 42 | } 43 | 44 | @Override 45 | @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) 46 | public Integer insertComment(Comment comment) { 47 | if (comment == null) { 48 | throw new IllegalArgumentException("参数不能为空!"); 49 | } 50 | // 过滤 HTML 标记和敏感词 51 | comment.setContent(HtmlUtils.htmlEscape(comment.getContent())); 52 | comment.setContent(sensitiveFilter.filter(comment.getContent())); 53 | // 添加评论 54 | int rows = commentMapper.insertComment(comment); 55 | 56 | // 更新帖子的评论数量 57 | if (comment.getEntityType() == ENTITY_TYPE_POST) { 58 | Integer count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId()); 59 | discussPostService.updateCommentCount(comment.getEntityId(), count); 60 | } 61 | return rows; 62 | } 63 | 64 | @Override 65 | public Comment selectCommentById(int id) { 66 | return commentMapper.selectCommentById(id); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/DiscussPostServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import com.tassel.mapper.DiscussPostMapper; 5 | import com.tassel.service.DiscussPostService; 6 | import com.tassel.util.SensitiveFilter; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.util.HtmlUtils; 10 | import org.unbescape.html.HtmlEscape; 11 | 12 | import javax.annotation.Resource; 13 | import java.util.List; 14 | 15 | /** 16 | * @author Ep流苏 17 | * @Date: 2020/6/14 10:36 18 | * @Description: 19 | */ 20 | @Service 21 | public class DiscussPostServiceImpl implements DiscussPostService { 22 | @Resource 23 | DiscussPostMapper discussPostMapper; 24 | 25 | @Resource 26 | SensitiveFilter sensitiveFilter; 27 | 28 | @Override 29 | public List selectDiscussPosts(int userId, int offset, int limit) { 30 | return discussPostMapper.selectDiscussPosts(userId, offset, limit); 31 | } 32 | 33 | @Override 34 | public Integer selectDiscussPostRows(int userId) { 35 | return discussPostMapper.selectDiscussPostRows(userId); 36 | } 37 | 38 | @Override 39 | public Integer insertDiscussPost(DiscussPost post) { 40 | if (post == null) { 41 | throw new IllegalArgumentException("参数不能为空!"); 42 | } 43 | 44 | // 转义 HTML 标记 45 | post.setTitle(HtmlUtils.htmlEscape(post.getTitle())); 46 | post.setContent(HtmlUtils.htmlEscape(post.getContent())); 47 | // 过滤敏感词 48 | post.setTitle(sensitiveFilter.filter(post.getTitle())); 49 | post.setContent(sensitiveFilter.filter(post.getContent())); 50 | 51 | return discussPostMapper.insertDiscussPost(post); 52 | } 53 | 54 | @Override 55 | public DiscussPost selectDiscussPostById(int id) { 56 | return discussPostMapper.selectDiscussPostById(id); 57 | } 58 | 59 | @Override 60 | public Integer updateCommentCount(int id, int commentCount) { 61 | return discussPostMapper.updateCommentCount(id, commentCount); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/ElasticsearchServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import com.tassel.mapper.elasticsearch.DiscussPostRepository; 5 | import com.tassel.service.ElasticsearchService; 6 | import org.elasticsearch.action.search.SearchResponse; 7 | import org.elasticsearch.index.query.QueryBuilders; 8 | import org.elasticsearch.search.SearchHit; 9 | import org.elasticsearch.search.SearchHits; 10 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; 11 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; 12 | import org.elasticsearch.search.sort.SortBuilders; 13 | import org.elasticsearch.search.sort.SortOrder; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.PageRequest; 16 | import org.springframework.data.domain.Pageable; 17 | import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; 18 | import org.springframework.data.elasticsearch.core.SearchResultMapper; 19 | import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage; 20 | import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl; 21 | import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; 22 | import org.springframework.data.elasticsearch.core.query.SearchQuery; 23 | import org.springframework.stereotype.Service; 24 | 25 | import javax.annotation.Resource; 26 | import java.util.ArrayList; 27 | import java.util.Date; 28 | import java.util.List; 29 | 30 | /** 31 | * @author shuaiyin.zhang 32 | * @description 33 | * @date 2020/09/16 34 | */ 35 | @Service 36 | public class ElasticsearchServiceImpl implements ElasticsearchService { 37 | 38 | @Resource 39 | DiscussPostRepository discussRepository; 40 | 41 | @Resource 42 | ElasticsearchTemplate elasticTemplate; 43 | 44 | @Override 45 | public void saveDiscussPost(DiscussPost post) { 46 | discussRepository.save(post); 47 | } 48 | 49 | @Override 50 | public void deleteDiscussPost(int id) { 51 | discussRepository.deleteById(id); 52 | } 53 | 54 | @Override 55 | public Page searchDiscussPost(String keyword, int current, int limit) { 56 | SearchQuery searchQuery = new NativeSearchQueryBuilder() 57 | .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content")) 58 | .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) 59 | .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)) 60 | .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) 61 | .withPageable(PageRequest.of(current, limit)) 62 | .withHighlightFields( 63 | new HighlightBuilder.Field("title").preTags("").postTags(""), 64 | new HighlightBuilder.Field("content").preTags("").postTags("") 65 | ).build(); 66 | 67 | return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() { 68 | @Override 69 | public AggregatedPage mapResults(SearchResponse response, Class aClass, Pageable pageable) { 70 | SearchHits hits = response.getHits(); 71 | if (hits.getTotalHits() <= 0) { 72 | return null; 73 | } 74 | 75 | List list = new ArrayList<>(); 76 | for (SearchHit hit : hits) { 77 | DiscussPost post = new DiscussPost(); 78 | 79 | String id = hit.getSourceAsMap().get("id").toString(); 80 | post.setId(Integer.valueOf(id)); 81 | 82 | String userId = hit.getSourceAsMap().get("userId").toString(); 83 | post.setUserId(Integer.valueOf(userId)); 84 | 85 | String title = hit.getSourceAsMap().get("title").toString(); 86 | post.setTitle(title); 87 | 88 | String content = hit.getSourceAsMap().get("content").toString(); 89 | post.setContent(content); 90 | 91 | String status = hit.getSourceAsMap().get("status").toString(); 92 | post.setStatus(Integer.valueOf(status)); 93 | 94 | String createTime = hit.getSourceAsMap().get("createTime").toString(); 95 | post.setCreateTime(new Date(Long.parseLong(createTime))); 96 | 97 | String commentCount = hit.getSourceAsMap().get("commentCount").toString(); 98 | post.setCommentCount(Integer.valueOf(commentCount)); 99 | 100 | // 处理高亮显示的结果 101 | HighlightField titleField = hit.getHighlightFields().get("title"); 102 | if (titleField != null) { 103 | post.setTitle(titleField.getFragments()[0].toString()); 104 | } 105 | 106 | HighlightField contentField = hit.getHighlightFields().get("content"); 107 | if (contentField != null) { 108 | post.setTitle(contentField.getFragments()[0].toString()); 109 | } 110 | 111 | list.add(post); 112 | } 113 | return new AggregatedPageImpl(list, pageable, hits.getTotalHits(), response.getAggregations(), 114 | response.getScrollId(), hits.getMaxScore()); 115 | } 116 | 117 | @Override 118 | public T mapSearchHit(SearchHit searchHit, Class aClass) { 119 | return null; 120 | } 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/FollowServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.entity.User; 4 | import com.tassel.service.FollowService; 5 | import com.tassel.service.UserService; 6 | import com.tassel.util.CommunityConstant; 7 | import com.tassel.util.RedisKeyUtil; 8 | import org.springframework.dao.DataAccessException; 9 | import org.springframework.data.redis.core.RedisOperations; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | import org.springframework.data.redis.core.SessionCallback; 12 | import org.springframework.stereotype.Service; 13 | 14 | import javax.annotation.Resource; 15 | import java.util.*; 16 | 17 | /** 18 | * @author shuaiyin.zhang 19 | * @description 20 | * @date 2020/09/12 21 | */ 22 | @Service 23 | public class FollowServiceImpl implements FollowService, CommunityConstant { 24 | 25 | @Resource 26 | RedisTemplate redisTemplate; 27 | 28 | @Resource 29 | UserService userService; 30 | 31 | @Override 32 | public void follow(int userId, int entityType, int entityId) { 33 | // 一个方法有两次操作请求,加入事务 34 | redisTemplate.execute(new SessionCallback() { 35 | @Override 36 | public Object execute(RedisOperations operations) throws DataAccessException { 37 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 38 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 39 | 40 | operations.multi(); 41 | 42 | operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis()); 43 | operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis()); 44 | 45 | return operations.exec(); 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | public void unFollow(int userId, int entityType, int entityId) { 52 | // 一个方法有两次操作请求,加入事务 53 | redisTemplate.execute(new SessionCallback() { 54 | @Override 55 | public Object execute(RedisOperations operations) throws DataAccessException { 56 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 57 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 58 | 59 | operations.multi(); 60 | 61 | operations.opsForZSet().remove(followeeKey, entityId); 62 | operations.opsForZSet().remove(followerKey, userId); 63 | 64 | return operations.exec(); 65 | } 66 | }); 67 | } 68 | 69 | @Override 70 | public long findFolloweeCount(int userId, int entityType) { 71 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 72 | return redisTemplate.opsForZSet().zCard(followeeKey); 73 | } 74 | 75 | @Override 76 | public long findFollowerCount(int entityType, int entityId) { 77 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 78 | return redisTemplate.opsForZSet().zCard(followerKey); 79 | } 80 | 81 | @Override 82 | public boolean hasFollowed(int userId, int entityType, int entityId) { 83 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 84 | return redisTemplate.opsForZSet().score(followeeKey, entityId) != null; 85 | } 86 | 87 | @Override 88 | public List> findFolloweeList(int userId, int offset, int limit) { 89 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER); 90 | Set targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1); 91 | 92 | if (targetIds == null) { 93 | return null; 94 | } 95 | 96 | List> list = new ArrayList<>(); 97 | for (Integer targetId : targetIds) { 98 | Map map = new HashMap<>(); 99 | User user = userService.queryUserById(targetId); 100 | map.put("user", user); 101 | Double score = redisTemplate.opsForZSet().score(followeeKey, targetId); 102 | map.put("followTime", new Date(score.longValue())); 103 | list.add(map); 104 | } 105 | return list; 106 | } 107 | 108 | @Override 109 | public List> findFollowerList(int userId, int offset, int limit) { 110 | String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId); 111 | Set targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1); 112 | 113 | if (targetIds == null) { 114 | return null; 115 | } 116 | 117 | List> list = new ArrayList<>(); 118 | for (Integer targetId : targetIds) { 119 | Map map = new HashMap<>(); 120 | User user = userService.queryUserById(targetId); 121 | map.put("user", user); 122 | Double score = redisTemplate.opsForZSet().score(followerKey, targetId); 123 | map.put("followTime", new Date(score.longValue())); 124 | list.add(map); 125 | } 126 | return list; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/LikeServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.service.LikeService; 4 | import com.tassel.util.RedisKeyUtil; 5 | import org.springframework.dao.DataAccessException; 6 | import org.springframework.data.redis.core.RedisOperations; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.data.redis.core.SessionCallback; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javax.annotation.Resource; 12 | import javax.print.attribute.standard.CopiesSupported; 13 | 14 | /** 15 | * @author shuaiyin.zhang 16 | * @description 17 | * @date 2020/09/11 18 | */ 19 | @Service 20 | public class LikeServiceImpl implements LikeService { 21 | 22 | @Resource 23 | RedisTemplate redisTemplate; 24 | 25 | @Override 26 | public void like(int userId, int entityType, int entityId, int entityUserId) { 27 | // 加入事务 28 | redisTemplate.execute(new SessionCallback() { 29 | @Override 30 | public Object execute(RedisOperations operations) throws DataAccessException { 31 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 32 | String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId); 33 | 34 | // 查询操作要在事务开启之前执行,否则在事务中不会立即执行。 35 | Boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId); 36 | 37 | // 开启事务 38 | operations.multi(); 39 | 40 | if (isMember) { 41 | operations.opsForSet().remove(entityLikeKey, userId); 42 | operations.opsForValue().decrement(userLikeKey); 43 | } else { 44 | operations.opsForSet().add(entityLikeKey, userId); 45 | operations.opsForValue().increment(userLikeKey); 46 | } 47 | 48 | // 执行 49 | return operations.exec(); 50 | } 51 | }); 52 | } 53 | 54 | @Override 55 | public long findEntityLikeCount(int entityType, int entityId) { 56 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 57 | return redisTemplate.opsForSet().size(entityLikeKey); 58 | } 59 | 60 | @Override 61 | public int findEntityLikeStatus(int userId, int entityType, int entityId) { 62 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 63 | return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0; 64 | } 65 | 66 | @Override 67 | public int findUserLikeCount(int userId) { 68 | String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); 69 | Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey); 70 | return count == null ? 0 : count; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/MessageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.entity.Message; 4 | import com.tassel.mapper.MessageMapper; 5 | import com.tassel.service.MessageService; 6 | import com.tassel.util.SensitiveFilter; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.util.HtmlUtils; 9 | 10 | import javax.annotation.Resource; 11 | import java.util.List; 12 | 13 | /** 14 | * @author shuaiyin.zhang 15 | * @description 16 | * @date 2020/09/08 17 | */ 18 | @Service 19 | public class MessageServiceImpl implements MessageService { 20 | 21 | @Resource 22 | MessageMapper messageMapper; 23 | 24 | @Resource 25 | SensitiveFilter sensitiveFilter; 26 | 27 | @Override 28 | public List selectConversations(int userId, int offset, int limit) { 29 | return messageMapper.selectConversations(userId, offset, limit); 30 | } 31 | 32 | @Override 33 | public Integer selectConversationCount(int userId) { 34 | return messageMapper.selectConversationCount(userId); 35 | } 36 | 37 | @Override 38 | public List selectLetters(String conversationId, int offset, int limit) { 39 | return messageMapper.selectLetters(conversationId, offset, limit); 40 | } 41 | 42 | @Override 43 | public Integer selectLetterCount(String conversationId) { 44 | return messageMapper.selectLetterCount(conversationId); 45 | } 46 | 47 | @Override 48 | public Integer selectLetterUnreadCount(int userId, String conversationId) { 49 | return messageMapper.selectLetterUnreadCount(userId, conversationId); 50 | } 51 | 52 | @Override 53 | public Integer insertMessage(Message message) { 54 | message.setContent(HtmlUtils.htmlEscape(message.getContent())); 55 | message.setContent(sensitiveFilter.filter(message.getContent())); 56 | return messageMapper.insertMessage(message); 57 | } 58 | 59 | @Override 60 | public Integer readMessage(List ids) { 61 | return messageMapper.updateStatus(ids, 1); 62 | } 63 | 64 | @Override 65 | public Message selectLatestNotice(int userId, String topic) { 66 | return messageMapper.selectLatestNotice(userId, topic); 67 | } 68 | 69 | @Override 70 | public Integer selectNoticeCount(int userId, String topic) { 71 | return messageMapper.selectNoticeCount(userId, topic); 72 | } 73 | 74 | @Override 75 | public Integer selectNoticeUnreadCount(int userId, String topic) { 76 | return messageMapper.selectNoticeUnreadCount(userId, topic); 77 | } 78 | 79 | @Override 80 | public List selectNotices(int userId, String topic, int offset, int limit) { 81 | return messageMapper.selectNotices(userId, topic, offset, limit); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.tassel.service.impl; 2 | 3 | import com.tassel.entity.LoginTicket; 4 | import com.tassel.entity.User; 5 | import com.tassel.mapper.LoginTicketMapper; 6 | import com.tassel.mapper.UserMapper; 7 | import com.tassel.service.UserService; 8 | import com.tassel.util.*; 9 | import org.apache.commons.lang3.ObjectUtils; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.stereotype.Service; 14 | import org.thymeleaf.TemplateEngine; 15 | import org.thymeleaf.context.Context; 16 | 17 | import javax.annotation.Resource; 18 | import java.util.Date; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | import java.util.Random; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | /** 25 | * @author Ep流苏 26 | * @Date: 2020/6/14 10:37 27 | * @Description: 28 | */ 29 | @Service 30 | public class UserServiceImpl implements UserService, CommunityConstant { 31 | @Resource 32 | UserMapper userMapper; 33 | @Resource 34 | MailClient mailClient; 35 | @Resource 36 | TemplateEngine templateEngine; 37 | //@Resource 38 | //LoginTicketMapper loginTicketMapper; 39 | @Value("${community.path.domain}") 40 | private String domain; 41 | @Value("${server.servlet.context-path}") 42 | private String contextPath; 43 | 44 | @Resource 45 | RedisTemplate redisTemplate; 46 | 47 | /** 48 | * 1. 优先从缓存中取值。 49 | * 2. 取不到,则初始化缓存. 50 | * 3. 数据变更时,清除缓存数据。 51 | */ 52 | 53 | /** 54 | * 1. 优先从缓存中取值。 55 | * 56 | * @param userId 57 | * @return 58 | */ 59 | private User getCache(int userId) { 60 | String redisKey = RedisKeyUtil.getUserKey(userId); 61 | return (User) redisTemplate.opsForValue().get(redisKey); 62 | } 63 | 64 | /** 65 | * 2. 取不到,则初始化缓存. 66 | * 67 | * @param userId 68 | * @return 69 | */ 70 | private User initCache(int userId) { 71 | User user = userMapper.queryUserById(userId); 72 | String redisKey = RedisKeyUtil.getUserKey(userId); 73 | redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS); 74 | return user; 75 | } 76 | 77 | /** 78 | * 3. 数据变更时,清除缓存数据。 79 | * 80 | * @param userId 81 | */ 82 | private void clearCache(int userId) { 83 | String redisKey = RedisKeyUtil.getUserKey(userId); 84 | redisTemplate.delete(redisKey); 85 | } 86 | 87 | @Override 88 | public User queryUserById(int id) { 89 | //return userMapper.queryUserById(id); 90 | User user = getCache(id); 91 | if (ObjectUtils.isEmpty(user)) { 92 | user = initCache(id); 93 | } 94 | return user; 95 | } 96 | 97 | @Override 98 | public User queryUserByName(String name) { 99 | return userMapper.queryByName(name); 100 | } 101 | 102 | @Override 103 | public Map register(User user) { 104 | Map map = new HashMap<>(); 105 | // 空值处理 106 | if (user == null) { 107 | throw new IllegalArgumentException("参数不能为空!"); 108 | } 109 | if (StringUtils.isBlank(user.getUsername())) { 110 | map.put("usernameMsg", "账号不能为空!"); 111 | return map; 112 | } 113 | if (StringUtils.isBlank(user.getPassword())) { 114 | map.put("passwordMsg", "密码不能为空!"); 115 | return map; 116 | } 117 | if (StringUtils.isBlank(user.getEmail())) { 118 | map.put("emailMsg", "邮箱不能为空!"); 119 | return map; 120 | } 121 | 122 | // 验证账号 123 | User u = userMapper.queryByName(user.getUsername()); 124 | if (u != null) { 125 | map.put("usernameMsg", "该账号已存在!"); 126 | return map; 127 | } 128 | // 验证邮箱 129 | u = userMapper.queryByEmail(user.getEmail()); 130 | if (u != null) { 131 | map.put("emailMsg", "该邮箱已被注册!"); 132 | return map; 133 | } 134 | 135 | // 注册用户 136 | user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); 137 | user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); 138 | user.setType(0); 139 | user.setStatus(0); 140 | user.setActivationCode(CommunityUtil.generateUUID()); 141 | user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); 142 | user.setCreateTime(new Date()); 143 | userMapper.insertUser(user); 144 | 145 | // 激活邮件 146 | Context context = new Context(); 147 | context.setVariable("email", user.getEmail()); 148 | // http://localhost:8023/activation/101/code 149 | //String url = domain + ("/".equals(contextPath) ? "" : contextPath) + "/activation/" + user.getId() + "/" + user.getActivationCode(); 150 | String url = domain + CommunityUtil.contextPathJudge(contextPath) + "/activation/" + user.getId() + "/" + user.getActivationCode(); 151 | context.setVariable("url", url); 152 | String content = templateEngine.process("/mail/activation", context); 153 | mailClient.sendMail(user.getEmail(), "激活账号", content); 154 | 155 | return map; 156 | } 157 | 158 | @Override 159 | public Integer activation(int userId, String code) { 160 | User user = userMapper.queryUserById(userId); 161 | if (user.getStatus() == 1) { 162 | return ACTIVATION_REPEAT; 163 | } else if (user.getActivationCode().equals(code)) { 164 | userMapper.updateStatus(1, userId); 165 | clearCache(userId); 166 | return ACTIVATION_SUCCESS; 167 | } else { 168 | return ACTIVATION_FAILURE; 169 | } 170 | } 171 | 172 | @Override 173 | public Map login(String username, String password, int expiredSecond) { 174 | Map map = new HashMap<>(0); 175 | 176 | // 空值处理 177 | if (StringUtils.isBlank(username)) { 178 | map.put("usernameMsg", "用户名不能为空!"); 179 | return map; 180 | } 181 | if (StringUtils.isBlank(password)) { 182 | map.put("passwordMsg", "密码不能为空!"); 183 | return map; 184 | } 185 | 186 | // 验证账号 187 | User user = userMapper.queryByName(username); 188 | if (ObjectUtils.isEmpty(user)) { 189 | map.put("usernameMsg", "该账号不存在!"); 190 | return map; 191 | } 192 | 193 | // 验证激活状态 194 | if (user.getStatus() == 0) { 195 | map.put("usernameMsg", "该账号未激活!"); 196 | return map; 197 | } 198 | 199 | // 验证密码 200 | password = CommunityUtil.md5(password + user.getSalt()); 201 | if (!user.getPassword().equals(password)) { 202 | map.put("passwordMsg", "密码不正确!"); 203 | return map; 204 | } 205 | 206 | // 成功登录, 生成登录凭证 207 | LoginTicket loginTicket = new LoginTicket(); 208 | loginTicket.setUserId(user.getId()); 209 | loginTicket.setStatus(0); 210 | loginTicket.setTicket(CommunityUtil.generateUUID()); 211 | loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSecond * 1000)); 212 | //loginTicketMapper.insertLoginTicket(loginTicket); 213 | 214 | String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket()); 215 | redisTemplate.opsForValue().set(redisKey, loginTicket); 216 | 217 | map.put("ticket", loginTicket.getTicket()); 218 | return map; 219 | } 220 | 221 | @Override 222 | public void logout(String ticket) { 223 | //loginTicketMapper.updateStatus(ticket, 1); 224 | String redisKey = RedisKeyUtil.getTicketKey(ticket); 225 | LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey); 226 | loginTicket.setStatus(1); 227 | redisTemplate.opsForValue().set(redisKey, loginTicket); 228 | } 229 | 230 | @Override 231 | public LoginTicket findLoginTicket(String ticket) { 232 | //return loginTicketMapper.selectByTicket(ticket); 233 | String redisKey = RedisKeyUtil.getTicketKey(ticket); 234 | return (LoginTicket) redisTemplate.opsForValue().get(redisKey); 235 | } 236 | 237 | @Override 238 | public int updateHeader(Integer userId, String headerUrl) { 239 | //return userMapper.updateHeader(headerUrl, userId); 240 | Integer rows = userMapper.updateHeader(headerUrl, userId); 241 | clearCache(userId); 242 | return rows; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/CommunityConstant.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | /** 4 | * @author Ep流苏 5 | * @Date: 2020/6/14 18:46 6 | * @Description: 7 | */ 8 | public interface CommunityConstant { 9 | 10 | /** 11 | * 注册邮件激活成功 12 | */ 13 | int ACTIVATION_SUCCESS = 0; 14 | /** 15 | * 注册邮件重复激活 16 | */ 17 | int ACTIVATION_REPEAT = 1; 18 | /** 19 | * 注册邮件激活失败 20 | */ 21 | int ACTIVATION_FAILURE = 2; 22 | 23 | /** 24 | * 默认状态下登录凭证失效时间: 12 小时 25 | */ 26 | int DEFAULT_EXPIRED_SECONDS = 60 * 60 * 12; 27 | /** 28 | * 记住状态下登录凭证失效时间: 100 天 29 | */ 30 | int REMEMBER_EXPIRED_SECONDS = 60 * 60 * 24 * 100; 31 | 32 | /** 33 | * 评论实体类型:帖子 34 | */ 35 | int ENTITY_TYPE_POST = 1; 36 | /** 37 | * 评论实体类型:评论 对评论进行评论 38 | */ 39 | int ENTITY_TYPE_COMMENT = 2; 40 | 41 | /** 42 | * 关注实体类型:用户 关注用户 43 | */ 44 | int ENTITY_TYPE_USER = 3; 45 | 46 | /** 47 | * Kafka 评论 点赞 关注 发帖 Topic 48 | */ 49 | String TOPIC_COMMENT = "comment"; 50 | String TOPIC_LIKE = "like"; 51 | String TOPIC_FOLLOW = "follow"; 52 | 53 | String TOPIC_PUBLISH = "publish"; 54 | 55 | /** 56 | * 系统用户 ID 57 | */ 58 | int SYSTEM_USER_ID = 1; 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/CommunityUtil.java: -------------------------------------------------------------------------------- 1 | package com.tassel.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.Map; 8 | import java.util.UUID; 9 | 10 | /** 11 | * @author Ep流苏 12 | * @Date: 2020/6/14 16:29 13 | * @Description: 14 | */ 15 | public class CommunityUtil { 16 | 17 | /** 18 | * 生成随机字符串 19 | * 20 | * @return 21 | */ 22 | public static String generateUUID() { 23 | return UUID.randomUUID().toString().replace("-", ""); 24 | } 25 | 26 | /** 27 | * MD5 加密 28 | * 加盐值 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 | /** 38 | * @param contextPath 39 | * @return 判断contextPath 是否为 / 防止url 多个 / 导致拼接失败 40 | */ 41 | public static String contextPathJudge(String contextPath) { 42 | return "/".equals(contextPath) ? "" : contextPath; 43 | } 44 | 45 | /** 46 | * 生成 json 字符串 47 | * 48 | * @param code 49 | * @param msg 50 | * @param map 51 | * @return 52 | */ 53 | public static String getJSONString(int code, String msg, Map map) { 54 | JSONObject json = new JSONObject(); 55 | json.put("code", code); 56 | json.put("msg", msg); 57 | if (map != null) { 58 | for (String key : map.keySet()) { 59 | json.put(key, map.get(key)); 60 | } 61 | } 62 | return json.toJSONString(); 63 | } 64 | 65 | public static String getJSONString(int code, String msg) { 66 | return getJSONString(code, msg, null); 67 | } 68 | 69 | public static String getJSONString(int code) { 70 | return getJSONString(code, null, null); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | import javax.servlet.http.Cookie; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | /** 7 | * @Description 8 | * @Author Zhang Shuaiyin 9 | * @Date 2020/07/12 10 | */ 11 | public class CookieUtil { 12 | public static String getValue(HttpServletRequest request, String name) { 13 | if (request == null || name == null || "".equals(name)) { 14 | throw new IllegalArgumentException("参数为空!"); 15 | } 16 | 17 | Cookie[] cookies = request.getCookies(); 18 | if (cookies.length != 0 || 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 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/HostHolder.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | import com.tassel.entity.User; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * @Description 持有用户信息,代替 Session 对象 ---线程隔离 8 | * @Author Zhang Shuaiyin 9 | * @Date 2020/07/12 10 | */ 11 | @Component 12 | public class HostHolder { 13 | 14 | /** 15 | * 线程隔离 16 | */ 17 | private ThreadLocal users = new ThreadLocal<>(); 18 | 19 | public void setUser(User user) { 20 | users.set(user); 21 | } 22 | 23 | public User getUser() { 24 | return users.get(); 25 | } 26 | 27 | public void clear() { 28 | users.remove(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/MailClient.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | import org.jasypt.encryption.StringEncryptor; 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.mail.javamail.JavaMailSender; 9 | import org.springframework.mail.javamail.MimeMessageHelper; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.Resource; 13 | import javax.mail.MessagingException; 14 | import javax.mail.internet.MimeMessage; 15 | 16 | /** 17 | * @author Ep流苏 18 | * @Date: 2020/6/14 16:35 19 | * @Description: 20 | */ 21 | @Component 22 | public class MailClient { 23 | private static final Logger logger = LoggerFactory.getLogger(MailClient.class); 24 | 25 | @Resource 26 | private JavaMailSender mailSender; 27 | 28 | @Autowired 29 | private StringEncryptor encryptor; 30 | 31 | @Value("${spring.mail.username}") 32 | private String from; 33 | 34 | public void sendMail(String to, String subject, String content) { 35 | try { 36 | MimeMessage message = mailSender.createMimeMessage(); 37 | MimeMessageHelper helper = new MimeMessageHelper(message); 38 | helper.setFrom(from); 39 | helper.setTo(to); 40 | helper.setSubject(subject); 41 | helper.setText(content, true); 42 | mailSender.send(helper.getMimeMessage()); 43 | logger.info("邮件发送成功,需要查看请配置自己的邮箱信息,或与我联系:qq 594983498"); 44 | } catch (MessagingException e) { 45 | logger.error("发送邮件失败:" + e.getMessage()); 46 | } 47 | } 48 | 49 | private String decode(String code) { 50 | return encryptor.decrypt(code); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/Page.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | /** 4 | * @author Ep流苏 5 | * @Date: 2020/6/14 11:17 6 | * @Description: 封装分页 7 | */ 8 | public class Page { 9 | 10 | /** 11 | * 当前页码 12 | */ 13 | private Integer current = 1; 14 | /** 15 | * 页记录上限 16 | */ 17 | private Integer limit = 10; 18 | /** 19 | * 数据总数(用于计算总页数) 20 | */ 21 | private Integer rows; 22 | /** 23 | * 查询路径:用于复用分页路径 24 | */ 25 | private String path; 26 | 27 | public Integer getCurrent() { 28 | return current; 29 | } 30 | 31 | public void setCurrent(Integer current) { 32 | if (current >= 1) { 33 | this.current = current; 34 | } 35 | } 36 | 37 | public Integer getLimit() { 38 | return limit; 39 | } 40 | 41 | public void setLimit(Integer limit) { 42 | if (limit >= 1 && limit <= 100) { 43 | this.limit = limit; 44 | } 45 | } 46 | 47 | public Integer getRows() { 48 | return rows; 49 | } 50 | 51 | public void setRows(Integer rows) { 52 | if (rows >= 0) { 53 | this.rows = rows; 54 | } 55 | } 56 | 57 | public String getPath() { 58 | return path; 59 | } 60 | 61 | public void setPath(String path) { 62 | this.path = path; 63 | } 64 | 65 | /** 66 | * 数据起始行 current * limit - limit 67 | */ 68 | public Integer getOffset() { 69 | return (current - 1) * limit; 70 | } 71 | 72 | /** 73 | * 总页数 rows / limit [+ 1] 74 | */ 75 | public Integer getTotal() { 76 | if (rows % limit == 0) { 77 | return rows / limit; 78 | } else { 79 | return rows / limit + 1; 80 | } 81 | } 82 | 83 | /** 84 | * 起始页码 85 | */ 86 | public Integer getFrom() { 87 | int from = current - 2; 88 | return Math.max(from, 1); 89 | } 90 | 91 | /** 92 | * 终止页码 93 | */ 94 | public Integer getTo() { 95 | int to = current + 2; 96 | int total = getTotal(); 97 | return Math.min(to, total); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | /** 4 | * @author shuaiyin.zhang 5 | * @description 6 | * @date 2020/09/11 7 | */ 8 | public class RedisKeyUtil { 9 | 10 | private static final String SPLIT = ":"; 11 | 12 | /** 13 | * 点赞 key 前缀 14 | */ 15 | private static final String PREFIX_ENTITY_LIKE = "like:entity"; 16 | private static final String PREFIX_USER_LIKE = "like:user"; 17 | 18 | /** 19 | * 关注 key 前缀 20 | */ 21 | private static final String PREFIX_FOLLOWEE = "followee"; 22 | private static final String PREFIX_FOLLOWER = "follower"; 23 | 24 | /** 25 | * 验证码前缀 26 | */ 27 | private static final String PREFIX_KAPTCHA = "kaptcha"; 28 | 29 | /** 30 | * 登录凭证 31 | */ 32 | private static final String PREFIX_TICKET = "ticket"; 33 | 34 | /** 35 | * 对用户做缓存 36 | */ 37 | private static final String PREFIX_USER = "user"; 38 | 39 | /** 40 | * 某个实体的赞 41 | * like:entity:entityType:entityId -> set(userId) 42 | * 43 | * @param entityType 44 | * @param entityId 45 | * @return 46 | */ 47 | public static String getEntityLikeKey(int entityType, int entityId) { 48 | return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId; 49 | } 50 | 51 | /** 52 | * 某个用户的赞 53 | * like:user:user:Id -> int 54 | * 55 | * @param userId 56 | * @return 57 | */ 58 | public static String getUserLikeKey(int userId) { 59 | return PREFIX_USER_LIKE + SPLIT + userId; 60 | } 61 | 62 | /** 63 | * 某个用户关注的实体 64 | * followee:userId:entityType -> zset(entityId, now) 65 | * 66 | * @param userId 67 | * @param entityType 68 | * @return 69 | */ 70 | public static String getFolloweeKey(int userId, int entityType) { 71 | return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; 72 | } 73 | 74 | /** 75 | * 某个实体拥有的粉丝 76 | * follower:entityType:entityId -> zset(userId, now) 77 | * 78 | * @param entityType 79 | * @param entityId 80 | * @return 81 | */ 82 | public static String getFollowerKey(int entityType, int entityId) { 83 | return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId; 84 | } 85 | 86 | /** 87 | * 登录验证码 88 | * 89 | * @param owner 90 | * @return 91 | */ 92 | public static String getKaptchaKey(String owner) { 93 | return PREFIX_KAPTCHA + SPLIT + owner; 94 | } 95 | 96 | /** 97 | * 登录凭证 98 | * 99 | * @param ticket 100 | * @return 101 | */ 102 | public static String getTicketKey(String ticket) { 103 | return PREFIX_TICKET + SPLIT + ticket; 104 | } 105 | 106 | /** 107 | * 用户 108 | * 109 | * @param userId 110 | * @return 111 | */ 112 | public static String getUserKey(int userId) { 113 | return PREFIX_USER + SPLIT + userId; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/tassel/util/SensitiveFilter.java: -------------------------------------------------------------------------------- 1 | package com.tassel.util; 2 | 3 | import org.apache.commons.lang3.CharUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * @author shuaiyin.zhang 19 | * @description 敏感词过滤器 20 | * @date 2020/07/18 21 | */ 22 | @Component 23 | public class SensitiveFilter { 24 | private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class); 25 | 26 | /** 27 | * 敏感词替换符 28 | */ 29 | private static final String REPLACEMENT = "***"; 30 | 31 | /** 32 | * 根节点 33 | */ 34 | private TrieNode rootNode = new TrieNode(); 35 | 36 | /** 37 | * 初始化前缀树 38 | * 39 | * @PostConstruct:标注一个初始化方法,在容器初始化 bean 时,该初始化方法自动执行 40 | */ 41 | @PostConstruct 42 | public void init() { 43 | // 按行读取保存敏感词数据的文件 44 | try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { 45 | String keyword; 46 | while ((keyword = reader.readLine()) != null) { 47 | // 添加到前缀树 48 | this.addKeyword(keyword); 49 | } 50 | } catch (IOException e) { 51 | logger.error("加载敏感词文件失败: {}", e.getMessage()); 52 | e.printStackTrace(); 53 | } 54 | } 55 | 56 | /** 57 | * 将一个敏感词添加到前缀树中去 58 | */ 59 | private void addKeyword(String keyword) { 60 | TrieNode tempNode = rootNode; 61 | for (int i = 0; i < keyword.length(); i++) { 62 | char c = keyword.charAt(i); 63 | TrieNode subNode = tempNode.getSubNode(c); 64 | 65 | if (subNode == null) { 66 | // 初始化子节点 67 | subNode = new TrieNode(); 68 | tempNode.setSubNode(c, subNode); 69 | } 70 | 71 | // 指向子节点,进入下一轮循环 72 | tempNode = subNode; 73 | 74 | // 设置结束标识 75 | if (i == keyword.length() - 1) { 76 | tempNode.isKeywordEnd = true; 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * 过滤敏感词算法 83 | * 84 | * @param text 待过滤文本 85 | * @return 过滤后的文本 86 | */ 87 | public String filter(String text) { 88 | if (StringUtils.isBlank(text)) { 89 | return null; 90 | } 91 | 92 | // 指针1 默认指向前缀树根节点 93 | TrieNode tempNode = rootNode; 94 | // 指针2 默认指向文本字符串首位 只向后移动,到敏感词开始时停下 95 | int begin = 0; 96 | // 指针3 默认指向文本字符串首位,跟着指针2, 当指针2遇到敏感词停下时,指针3继续移动判断是否是敏感词 97 | int position = 0; 98 | // 结果文本 99 | StringBuilder sb = new StringBuilder(); 100 | 101 | while (position < text.length()) { 102 | char c = text.charAt(position); 103 | // 跳过符号 104 | if (isSymbol(c)) { 105 | // 若指针1 处于根节点,将此符号计入结果不跳过,让指针2向下走一步 106 | if (tempNode == rootNode) { 107 | sb.append(c); 108 | begin++; 109 | } 110 | // 无论符号在开头或中间,指针3都向下走一步 111 | position++; 112 | continue; 113 | } 114 | // 检查下级节点 115 | tempNode = tempNode.getSubNode(c); 116 | if (tempNode == null) { 117 | // 以begin为开头的字符串不是敏感词 118 | sb.append(text.charAt(begin)); 119 | // 进入下一个位置 120 | position = ++begin; 121 | // 重新指向根节点 122 | tempNode = rootNode; 123 | } else if (tempNode.isKeywordEnd) { 124 | // 发现了敏感词,将begin到position字符串替换掉 125 | sb.append(REPLACEMENT); 126 | // 进入下一个位置 127 | begin = ++position; 128 | // 重新指向根节点 129 | tempNode = rootNode; 130 | } else { 131 | // 继续检查下一个字符 132 | position++; 133 | } 134 | } 135 | // 将最后一批字符计入结果 136 | sb.append(text.substring(begin)); 137 | return sb.toString(); 138 | } 139 | 140 | /** 141 | * 判断字符是否是符号 142 | * 0x2E80 ~ 0x9FFF 东亚文字范围 143 | * 144 | * @param c 145 | * @return 146 | */ 147 | private boolean isSymbol(Character c) { 148 | return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF); 149 | } 150 | 151 | /** 152 | * 定义前缀树 数据结构 153 | */ 154 | private class TrieNode { 155 | // 关键词结束标识 156 | private boolean isKeywordEnd = false; 157 | 158 | // 子节点(key 是下级字符,value 是下级节点) 159 | private Map subNodes = new HashMap<>(); 160 | 161 | // 添加子节点 162 | public void setSubNode(Character c, TrieNode node) { 163 | subNodes.put(c, node); 164 | } 165 | 166 | // 获取子节点 167 | public TrieNode getSubNode(Character c) { 168 | return subNodes.get(c); 169 | } 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 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8023 3 | servlet: 4 | context-path: / 5 | 6 | spring: 7 | thymeleaf: 8 | cache: false 9 | datasource: 10 | driver-class-name: com.mysql.jdbc.Driver 11 | url: jdbc:mysql://localhost:3306/community?useUnicode=true&characterEncoding=utf8&useSSL=false 12 | username: root 13 | password: root 14 | type: com.zaxxer.hikari.HikariDataSource 15 | hikari: 16 | maximum-pool-size: 15 17 | minimum-idle: 5 18 | idle-timeout: 30000 19 | mail: 20 | host: smtp.qq.com 21 | port: 465 22 | username: ENC(2a34EAfYxma1BXFO4dZRCP3SdTUyz7+qyKmZ7QEKJrA=) 23 | password: ENC(qaB+zE7L5VObM1hK1YtcBMT1+brMcY0w8FeBpU/PiZo=) 24 | # protocol: smtps 25 | properties: 26 | mail: 27 | smtp: 28 | auth: true 29 | ssl: 30 | enable: true 31 | # redis 32 | redis: 33 | database: 11 34 | host: localhost 35 | port: 6379 36 | # kafka 37 | kafka: 38 | bootstrap-servers: localhost:9092 39 | consumer: 40 | group-id: test-consumer-group 41 | enable-auto-commit: true 42 | auto-commit-interval: 3000 #ms 43 | # elasticsearch 44 | data: 45 | elasticsearch: 46 | cluster-name: nowcoder 47 | cluster-nodes: 127.0.0.1:9300 48 | 49 | 50 | # 加密参数 51 | jasypt: 52 | encryptor: 53 | password: test 54 | 55 | # 配置sql打印日志 56 | logging: 57 | level: 58 | com.tassel.mapper: 59 | debug 60 | 61 | mybatis: 62 | mapper-locations: classpath:mapper/*.xml 63 | type-aliases-package: com.tassel.entity 64 | configuration: 65 | map-underscore-to-camel-case: true 66 | use-generated-keys: true 67 | 68 | community: 69 | path: 70 | domain: http://localhost:8023 71 | upload: E:\Documents\Projects\Community\upload 72 | -------------------------------------------------------------------------------- /src/main/resources/mapper/comment-mapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | `id`, `user_id`, `entity_type`, `entity_id`, `target_id`, `content`, `status`, `create_time` 8 | 9 | 10 | `user_id`, `entity_type`, `entity_id`, `target_id`, `content`, `status`, `create_time` 11 | 12 | 13 | 23 | 24 | 31 | 32 | 33 | insert into community.comment() 34 | values(#{userId}, #{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime}) 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/mapper/discusspost-mapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | `id`, `user_id`, `title`, `content`, `type`, `status`, `create_time`, `comment_count`, `score` 8 | 9 | 10 | 11 | `user_id`, `title`, `content`, `type`, `status`, `create_time`, `comment_count`, `score` 12 | 13 | 14 | 25 | 26 | 34 | 35 | 41 | 42 | 43 | insert into discuss_post() 44 | values (#{userId}, #{title}, #{content}, #{type}, #{status}, #{createTime}, #{commentCount}, #{score}) 45 | 46 | 47 | 48 | update community.discuss_post set comment_count = #{commentCount} where id = #{id} 49 | 50 | -------------------------------------------------------------------------------- /src/main/resources/mapper/message-mapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | `id`, `from_id`, `to_id`, `conversation_id`, `content`, `status`, `create_time` 8 | 9 | 10 | `from_id`, `to_id`, `conversation_id`, `content`, `status`, `create_time` 11 | 12 | 13 | 28 | 29 | 39 | 40 | 50 | 51 | 58 | 59 | 69 | 70 | 83 | 84 | 92 | 93 | 103 | 114 | 115 | 116 | insert into community.message() 117 | values(#{fromId}, #{toId}, #{conversationId}, #{content}, #{status}, #{createTime}) 118 | 119 | 120 | 121 | update community.message set status = #{status} 122 | where id in 123 | 124 | #{id} 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/main/resources/mapper/user-mapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 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 | 13 | 18 | 19 | 24 | 25 | 30 | 31 | 32 | insert into user () 33 | values(#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime}) 34 | 35 | 36 | 37 | update user set status = #{status} where id = #{id} 38 | 39 | 40 | 41 | update user set header_url = #{headerUrl} where id = #{id} 42 | 43 | 44 | 45 | update user set password = #{password} where id = #{id} 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/sensitive-words.txt: -------------------------------------------------------------------------------- 1 | 赌博 2 | 嫖娼 3 | 吸毒 4 | 开票 -------------------------------------------------------------------------------- /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/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/static/css/letter.css: -------------------------------------------------------------------------------- 1 | .main .nav .badge { 2 | position: absolute; 3 | top: -3px; 4 | left: 68px; 5 | } 6 | 7 | .main .media .badge { 8 | position: absolute; 9 | top: 12px; 10 | left: -3px; 11 | } 12 | 13 | .toast { 14 | max-width: 100%; 15 | width: 80%; 16 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/login.css: -------------------------------------------------------------------------------- 1 | .main .container { 2 | width: 720px; 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/static/img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsy0216/Community/bc2fb1573bd78179f631a9feece1207c11011674/src/main/resources/static/img/404.png -------------------------------------------------------------------------------- /src/main/resources/static/img/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsy0216/Community/bc2fb1573bd78179f631a9feece1207c11011674/src/main/resources/static/img/captcha.png -------------------------------------------------------------------------------- /src/main/resources/static/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsy0216/Community/bc2fb1573bd78179f631a9feece1207c11011674/src/main/resources/static/img/error.png -------------------------------------------------------------------------------- /src/main/resources/static/js/discuss.js: -------------------------------------------------------------------------------- 1 | function like(btn, entityType, entityId, entityUserId, postId) { 2 | $.post( 3 | "/like", 4 | {"entityType": entityType, "entityId": entityId, "entityUserId": entityUserId, "postId": postId}, 5 | function (data) { 6 | data = $.parseJSON(data); 7 | if (data.code === 0) { 8 | $(btn).children("i").text(data.likeCount); 9 | $(btn).children("b").text(data.likeStatus === 1 ? '已赞' : '赞'); 10 | } else { 11 | alert(data.msg); 12 | } 13 | } 14 | ) 15 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/global.js: -------------------------------------------------------------------------------- 1 | window.alert = function(message) { 2 | if(!$(".alert-box").length) { 3 | $("body").append( 4 | '' 22 | ); 23 | } 24 | 25 | var h = $(".alert-box").height(); 26 | var y = h / 2 - 100; 27 | if(h > 600) y -= 100; 28 | $(".alert-box .modal-dialog").css("margin", (y < 0 ? 0 : y) + "px auto"); 29 | 30 | $(".alert-box .modal-body p").text(message); 31 | $(".alert-box").modal("show"); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/static/js/index.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $("#publishBtn").click(publish); 3 | }); 4 | 5 | function publish() { 6 | $("#publishModal").modal("hide"); 7 | 8 | // 获取标题和内容 9 | let title = $("#recipient-name").val(); 10 | let content = $("#message-text").val(); 11 | // 发送异步请求 12 | $.post( 13 | "/discuss/insert", 14 | {"title": title, "content": content}, 15 | function (data) { 16 | data = $.parseJSON(data); 17 | // 在提示框中显示返回的信息 18 | $("#hintBody").text(data.msg); 19 | // 显示提示框 20 | $("#hintModal").modal("show"); 21 | // 2 秒后自动隐藏 22 | setTimeout(function () { 23 | $("#hintModal").modal("hide"); 24 | // 发布成功,刷新页面显示 25 | if (data.code == 0) { 26 | window.location.reload(); 27 | } 28 | }, 2000); 29 | } 30 | ) 31 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/letter.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $("#sendBtn").click(send_letter); 3 | $(".close").click(delete_msg); 4 | }); 5 | 6 | function send_letter() { 7 | $("#sendModal").modal("hide"); 8 | 9 | let toName = $("#recipient-name").val(); 10 | let content = $("#message-text").val(); 11 | $.post( 12 | "/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 | $("#hintModal").modal("show"); 22 | setTimeout(function () { 23 | $("#hintModal").modal("hide"); 24 | location.reload(); 25 | }, 2000); 26 | } 27 | ); 28 | } 29 | 30 | function delete_msg() { 31 | // TODO 删除数据 32 | $(this).parents(".media").remove(); 33 | } -------------------------------------------------------------------------------- /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 | "/follow", 11 | {"entityType": 3, "entityId": $(btn).prev().val()}, 12 | function (data) { 13 | data = $.parseJSON(data); 14 | if (data.code === 0) { 15 | window.location.reload(); 16 | } else { 17 | alert(data.msg); 18 | } 19 | } 20 | ); 21 | // $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary"); 22 | } else { 23 | // 取消关注 24 | $.post( 25 | "/unFollow", 26 | {"entityType": 3, "entityId": $(btn).prev().val()}, 27 | function (data) { 28 | data = $.parseJSON(data); 29 | if (data.code === 0) { 30 | window.location.reload(); 31 | } else { 32 | alert(data.msg); 33 | } 34 | } 35 | ); 36 | // $(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info"); 37 | } 38 | } -------------------------------------------------------------------------------- /src/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/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-404 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 | 106 |
107 |
108 |
109 |
110 | 126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-500 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 | 106 |
107 |
108 |
109 |
110 | 126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | 134 |
135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/activation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 牛客网-激活账号 7 | 8 | 9 |
10 |

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

13 |

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

18 |
19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/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/resources/templates/site/admin/data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-数据统计 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 58 |
59 |
60 | 61 | 62 |
63 | 64 |
65 |
网站 UV
66 |
67 | 68 | 69 | 70 |
71 |
    72 |
  • 73 | 统计结果 74 | 0 75 |
  • 76 |
77 |
78 | 79 |
80 |
活跃用户
81 |
82 | 83 | 84 | 85 |
86 |
    87 |
  • 88 | 统计结果 89 | 0 90 |
  • 91 |
92 |
93 |
94 | 95 | 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 | 104 |
105 |
106 |
107 | 130 |
131 |
132 |
133 |
134 | 150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/forget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 牛客网-忘记密码 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 59 |
60 |
61 | 62 | 63 |
64 |
65 |
66 |
67 | 68 |
69 | 70 |
71 | 该邮箱已被注册! 72 |
73 |
74 |
75 |
76 | 77 |
78 | 79 |
80 | 验证码不正确! 81 |
82 |
83 |
84 | 获取验证码 85 |
86 |
87 |
88 | 89 |
90 | 91 |
92 | 密码长度不能小于8位! 93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 |
101 |
102 |
103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 | 113 |
114 | 115 |
116 |
117 |
118 | 141 |
142 |
143 |
144 |
145 | 161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/operate-result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-操作结果 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 58 |
59 |
60 | 61 | 62 |
63 |
64 |
65 |

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

66 |
67 |

68 | 系统会在 8 秒后自动跳转, 69 | 您也可以点此 链接, 手动跳转! 70 |

71 |
72 |
73 |
74 | 75 | 76 |
77 |
78 |
79 | 80 |
81 | 82 |
83 | 84 |
85 |
86 |
87 | 110 |
111 |
112 |
113 |
114 | 130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | 138 | 139 | 140 | 141 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 牛客网-个人主页 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 |
68 | 79 |
80 | 81 |
82 | 用户头像 84 |
85 |
86 | nowcoder 87 | 88 | 92 |
93 |
94 | 注册于 2015-06-12 15:20:12 95 |
96 |
97 | 关注了 5 99 | 关注者 123 101 | 获得了 87 个赞 102 |
103 |
104 |
105 |
106 |
107 | 108 | 109 |
110 |
111 |
112 | 113 |
114 | 115 |
116 | 117 |
118 |
119 |
120 | 143 |
144 |
145 |
146 |
147 | 163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | 171 |
172 | 173 | 174 | 175 |
176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/CommunityApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | @SpringBootTest 11 | class CommunityApplicationTests { 12 | 13 | // 不能使用三目运算符 14 | @Value("${server.servlet.context-path = '/' ? '' : ${server.servlet.context-path}}") 15 | private String contextPath; 16 | 17 | @Test 18 | void contextLoads() { 19 | 20 | String[] imageSuffix = new String[] {"png", "jpg", "bmp", "gif", "svg", "webp"}; 21 | List list = Arrays.asList(imageSuffix); 22 | String suffix = "jpg"; 23 | System.out.println(list.contains(suffix)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/ElasticsearchTests.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import com.tassel.mapper.DiscussPostMapper; 5 | import org.elasticsearch.action.search.SearchResponse; 6 | import org.elasticsearch.index.query.QueryBuilder; 7 | import org.elasticsearch.index.query.QueryBuilders; 8 | import org.elasticsearch.search.SearchHit; 9 | import org.elasticsearch.search.SearchHits; 10 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; 11 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; 12 | import org.elasticsearch.search.sort.SortBuilders; 13 | import org.elasticsearch.search.sort.SortOrder; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.data.domain.Page; 17 | import org.springframework.data.domain.PageRequest; 18 | import org.springframework.data.domain.Pageable; 19 | import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; 20 | import org.springframework.data.elasticsearch.core.SearchResultMapper; 21 | import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage; 22 | import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl; 23 | import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; 24 | import org.springframework.data.elasticsearch.core.query.SearchQuery; 25 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 26 | 27 | import javax.annotation.Resource; 28 | import java.util.ArrayList; 29 | import java.util.Date; 30 | import java.util.List; 31 | 32 | /** 33 | * @author shuaiyin.zhang 34 | * @description 35 | * @date 2020/09/15 36 | */ 37 | @SpringBootTest 38 | public class ElasticsearchTests { 39 | 40 | @Resource 41 | DiscussPostMapper discussMapper; 42 | 43 | @Resource 44 | ElasticsearchRepository discussRepository; 45 | 46 | @Resource 47 | ElasticsearchTemplate elasticTemplate; 48 | 49 | @Test 50 | public void testInsert() { 51 | discussRepository.save(discussMapper.selectDiscussPostById(241)); 52 | discussRepository.save(discussMapper.selectDiscussPostById(242)); 53 | discussRepository.save(discussMapper.selectDiscussPostById(243)); 54 | } 55 | 56 | @Test 57 | public void testInsertAll() { 58 | discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100)); 59 | discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100)); 60 | discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100)); 61 | discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100)); 62 | discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100)); 63 | discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100)); 64 | discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100)); 65 | discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100)); 66 | discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100)); 67 | } 68 | 69 | @Test 70 | public void testUpdate() { 71 | DiscussPost post = discussMapper.selectDiscussPostById(231); 72 | post.setContent("我是新人,使劲灌水"); 73 | discussRepository.save(post); 74 | } 75 | 76 | @Test 77 | public void testDelete() { 78 | discussRepository.deleteById(231); 79 | } 80 | 81 | @Test 82 | public void testSearchByRepository() { 83 | SearchQuery searchQuery = new NativeSearchQueryBuilder() 84 | .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content")) 85 | .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) 86 | .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)) 87 | .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) 88 | .withPageable(PageRequest.of(0, 10)) 89 | .withHighlightFields( 90 | new HighlightBuilder.Field("title").preTags("").postTags(""), 91 | new HighlightBuilder.Field("content").preTags("").postTags("") 92 | ).build(); 93 | 94 | /** 95 | * elasticTemplate.queryForPage(searchQuery, class, SearchResultMapper) 96 | * 底层获取到了高亮显示的值,但是没有返回。 97 | */ 98 | 99 | Page page = discussRepository.search(searchQuery); 100 | System.out.println(page.getTotalElements()); 101 | System.out.println(page.getTotalPages()); 102 | System.out.println(page.getNumber()); 103 | System.out.println(page.getSize()); 104 | for (DiscussPost post : page) { 105 | System.out.println(post); 106 | } 107 | } 108 | 109 | @Test 110 | public void testSearchByTemplate() { 111 | SearchQuery searchQuery = new NativeSearchQueryBuilder() 112 | .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content")) 113 | .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) 114 | .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)) 115 | .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) 116 | .withPageable(PageRequest.of(0, 10)) 117 | .withHighlightFields( 118 | new HighlightBuilder.Field("title").preTags("").postTags(""), 119 | new HighlightBuilder.Field("content").preTags("").postTags("") 120 | ).build(); 121 | 122 | Page page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() { 123 | @Override 124 | public AggregatedPage mapResults(SearchResponse response, Class aClass, Pageable pageable) { 125 | SearchHits hits = response.getHits(); 126 | if (hits.getTotalHits() <= 0) { 127 | return null; 128 | } 129 | 130 | List list = new ArrayList<>(); 131 | for (SearchHit hit : hits) { 132 | DiscussPost post = new DiscussPost(); 133 | 134 | String id = hit.getSourceAsMap().get("id").toString(); 135 | post.setId(Integer.valueOf(id)); 136 | 137 | String userId = hit.getSourceAsMap().get("userId").toString(); 138 | post.setId(Integer.valueOf(userId)); 139 | 140 | String title = hit.getSourceAsMap().get("title").toString(); 141 | post.setTitle(title); 142 | 143 | String content = hit.getSourceAsMap().get("content").toString(); 144 | post.setContent(content); 145 | 146 | String status = hit.getSourceAsMap().get("status").toString(); 147 | post.setStatus(Integer.valueOf(status)); 148 | 149 | String createTime = hit.getSourceAsMap().get("createTime").toString(); 150 | post.setCreateTime(new Date(Long.valueOf(createTime))); 151 | 152 | String commentCount = hit.getSourceAsMap().get("commentCount").toString(); 153 | post.setCommentCount(Integer.valueOf(commentCount)); 154 | 155 | // 处理高亮显示的结果 156 | HighlightField titleField = hit.getHighlightFields().get("title"); 157 | if (titleField != null) { 158 | post.setTitle(titleField.getFragments()[0].toString()); 159 | } 160 | 161 | HighlightField contentField = hit.getHighlightFields().get("content"); 162 | if (contentField != null) { 163 | post.setTitle(contentField.getFragments()[0].toString()); 164 | } 165 | 166 | list.add(post); 167 | } 168 | return new AggregatedPageImpl(list, pageable, hits.getTotalHits(), response.getAggregations(), 169 | response.getScrollId(), hits.getMaxScore()); 170 | } 171 | 172 | @Override 173 | public T mapSearchHit(SearchHit searchHit, Class aClass) { 174 | return null; 175 | } 176 | }); 177 | 178 | System.out.println(page.getTotalElements()); 179 | System.out.println(page.getTotalPages()); 180 | System.out.println(page.getNumber()); 181 | System.out.println(page.getSize()); 182 | for (DiscussPost post : page) { 183 | System.out.println(post); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/KafkaTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerRecord; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.kafka.annotation.KafkaListener; 7 | import org.springframework.kafka.core.KafkaTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Resource; 11 | 12 | /** 13 | * @author shuaiyin.zhang 14 | * @description 15 | * @date 2020/09/13 16 | */ 17 | @SpringBootTest 18 | public class KafkaTest { 19 | 20 | @Resource 21 | KafkaProducer kafkaProducer; 22 | 23 | @Test 24 | public void testKafka() { 25 | kafkaProducer.sentMessage("test", "hello"); 26 | kafkaProducer.sentMessage("test", "world"); 27 | try { 28 | Thread.sleep(1000 * 10); 29 | } catch (InterruptedException e) { 30 | e.printStackTrace(); 31 | } 32 | } 33 | } 34 | 35 | @Component 36 | class KafkaProducer { 37 | @Resource 38 | KafkaTemplate kafkaTemplate; 39 | 40 | public void sentMessage(String topic, String content) { 41 | kafkaTemplate.send(topic, content); 42 | } 43 | } 44 | 45 | @Component 46 | class KafkaConsumer { 47 | @KafkaListener(topics = {"test"}) 48 | public void handleMessage(ConsumerRecord record) { 49 | System.out.println(record.value()); 50 | } 51 | } -------------------------------------------------------------------------------- /src/test/java/com/tassel/MailTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import com.tassel.util.MailClient; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.thymeleaf.TemplateEngine; 7 | import org.thymeleaf.context.Context; 8 | 9 | import javax.annotation.Resource; 10 | 11 | /** 12 | * @author Ep流苏 13 | * @Date: 2020/6/14 18:09 14 | * @Description: 15 | */ 16 | @SpringBootTest 17 | public class MailTest { 18 | 19 | @Resource 20 | private MailClient mailClient; 21 | 22 | @Resource 23 | private TemplateEngine templateEngine; 24 | 25 | @Test 26 | public void testTextMail() { 27 | mailClient.sendMail("594983498@qq.com", "TEST", "Welcome."); 28 | } 29 | 30 | @Test 31 | public void testHtmlMail() { 32 | Context context = new Context(); 33 | context.setVariable("username", "sunday"); 34 | 35 | String content = templateEngine.process("/mail/activation", context); 36 | System.out.println(content); 37 | 38 | mailClient.sendMail("594983498@qq.com", "HTML", content); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/RedisTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.data.redis.core.BoundValueOperations; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | 8 | import javax.annotation.Resource; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * @author shuaiyin.zhang 13 | * @description 14 | * @date 2020/09/11 15 | */ 16 | @SpringBootTest 17 | public class RedisTest { 18 | 19 | @Resource 20 | RedisTemplate redisTemplate; 21 | 22 | @Test 23 | public void testStrings() { 24 | String redisKey = "test:count"; 25 | 26 | redisTemplate.opsForValue().set(redisKey, 1); 27 | System.out.println(redisTemplate.opsForValue().get(redisKey)); 28 | System.out.println(redisTemplate.opsForValue().increment(redisKey)); 29 | System.out.println(redisTemplate.opsForValue().decrement(redisKey)); 30 | } 31 | 32 | @Test 33 | public void testHashes() { 34 | String redisKey = "test:user"; 35 | 36 | redisTemplate.opsForHash().put(redisKey, "id", 1); 37 | redisTemplate.opsForHash().put(redisKey, "username", "zhangsan"); 38 | ; 39 | System.out.println(redisTemplate.opsForHash().get(redisKey, "id")); 40 | ; 41 | System.out.println(redisTemplate.opsForHash().get(redisKey, "username")); 42 | ; 43 | } 44 | 45 | @Test 46 | public void testLists() { 47 | String redisKey = "test:ids"; 48 | 49 | redisTemplate.opsForList().leftPush(redisKey, 101); 50 | redisTemplate.opsForList().leftPush(redisKey, 102); 51 | redisTemplate.opsForList().leftPush(redisKey, 103); 52 | 53 | System.out.println(redisTemplate.opsForList().size(redisKey)); 54 | System.out.println(redisTemplate.opsForList().index(redisKey, 0)); 55 | System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2)); 56 | 57 | System.out.println(redisTemplate.opsForList().leftPop(redisKey)); 58 | System.out.println(redisTemplate.opsForList().leftPop(redisKey)); 59 | System.out.println(redisTemplate.opsForList().leftPop(redisKey)); 60 | } 61 | 62 | @Test 63 | public void testSets() { 64 | String redisKey = "test:teachers"; 65 | 66 | redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮"); 67 | 68 | System.out.println(redisTemplate.opsForSet().size(redisKey)); 69 | System.out.println(redisTemplate.opsForSet().pop(redisKey)); 70 | System.out.println(redisTemplate.opsForSet().members(redisKey)); 71 | } 72 | 73 | @Test 74 | public void testSortedSets() { 75 | String redisKey = "test:students"; 76 | 77 | redisTemplate.opsForZSet().add(redisKey, "唐僧", 80); 78 | redisTemplate.opsForZSet().add(redisKey, "悟空", 90); 79 | redisTemplate.opsForZSet().add(redisKey, "八戒", 50); 80 | redisTemplate.opsForZSet().add(redisKey, "沙僧", 70); 81 | redisTemplate.opsForZSet().add(redisKey, "白龙马", 60); 82 | 83 | System.out.println(redisTemplate.opsForZSet().zCard(redisKey)); 84 | System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒")); 85 | System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒")); 86 | System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2)); 87 | } 88 | 89 | @Test 90 | public void testKeys() { 91 | redisTemplate.delete("test:user"); 92 | 93 | System.out.println(redisTemplate.hasKey("test:user")); 94 | 95 | redisTemplate.expire("test:students", 10, TimeUnit.SECONDS); 96 | } 97 | 98 | @Test 99 | public void testBoundOperations() { 100 | String redisKey = "test:count"; 101 | BoundValueOperations operations = redisTemplate.boundValueOps(redisKey); 102 | operations.increment(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/SensitiveTests.java: -------------------------------------------------------------------------------- 1 | package com.tassel; 2 | 3 | import com.tassel.util.SensitiveFilter; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import javax.annotation.Resource; 8 | 9 | /** 10 | * @author shuaiyin.zhang 11 | * @description 12 | * @date 2020/07/18 13 | */ 14 | @SpringBootTest 15 | public class SensitiveTests { 16 | @Resource 17 | SensitiveFilter sensitiveFilter; 18 | 19 | @Test 20 | public void testSensitiveFilter() { 21 | String text = "这里可以赌博,可以嫖娼,可以开票,可以吸毒,哈哈哈!"; 22 | text = "这里可以☆赌☆博☆,可以☆嫖☆娼☆,可以☆开☆票☆,可以☆吸☆毒☆,哈哈哈!"; 23 | String filter = sensitiveFilter.filter(text); 24 | System.out.println(filter); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/mapper/DiscussPostMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.DiscussPost; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import javax.annotation.Resource; 8 | import java.util.List; 9 | 10 | /** 11 | * @author Ep流苏 12 | * @Date: 2020/6/14 10:11 13 | * @Description: 14 | */ 15 | @SpringBootTest 16 | public class DiscussPostMapperTest { 17 | @Resource 18 | DiscussPostMapper discussPostMapper; 19 | 20 | @Test 21 | public void selectDiscussPostsTest() { 22 | List discussPosts = discussPostMapper.selectDiscussPosts(0, 0, 10); 23 | discussPosts.forEach(discussPost -> System.out.println(discussPost)); 24 | } 25 | 26 | @Test 27 | public void selectDiscussPostRowsTest() { 28 | Integer rows = discussPostMapper.selectDiscussPostRows(0); 29 | System.out.println(rows); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/mapper/LoginTicketMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import com.tassel.entity.LoginTicket; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | import java.util.Date; 9 | 10 | /** 11 | * @Description 12 | * @Author Zhang Shuaiyin 13 | * @Date 2020/07/05 14 | */ 15 | @SpringBootTest 16 | public class LoginTicketMapperTest { 17 | 18 | @Autowired 19 | LoginTicketMapper ticketMapper; 20 | 21 | @Test 22 | public void testInsertLoginTicket() { 23 | LoginTicket loginTicket = new LoginTicket(); 24 | loginTicket.setUserId(101); 25 | loginTicket.setTicket("abc"); 26 | loginTicket.setStatus(0); 27 | loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10)); 28 | ticketMapper.insertLoginTicket(loginTicket); 29 | } 30 | 31 | @Test 32 | public void testSelectLoginTicket() { 33 | LoginTicket abc = ticketMapper.selectByTicket("abc"); 34 | System.out.println(abc); 35 | 36 | ticketMapper.updateStatus("abc", 1); 37 | abc = ticketMapper.selectByTicket("abc"); 38 | System.out.println(abc); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/mapper/UserMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel.mapper; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | import javax.annotation.Resource; 7 | 8 | /** 9 | * @author Ep流苏 10 | * @Date: 2020/6/14 10:28 11 | * @Description: 12 | */ 13 | @SpringBootTest 14 | public class UserMapperTest { 15 | 16 | @Resource 17 | UserMapper userMapper; 18 | 19 | @Test 20 | public void queryUserById(){ 21 | System.out.println(userMapper.queryUserById(149)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/tassel/queue/BlockingQueueTest.java: -------------------------------------------------------------------------------- 1 | package com.tassel.queue; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.ArrayBlockingQueue; 5 | import java.util.concurrent.BlockingQueue; 6 | 7 | /** 8 | * @author shuaiyin.zhang 9 | * @description 10 | * @date 2020/09/13 11 | */ 12 | public class BlockingQueueTest { 13 | 14 | public static void main(String[] args) { 15 | BlockingQueue queue = new ArrayBlockingQueue(10); 16 | new Thread(new Producer(queue)).start(); 17 | new Thread(new Consumer(queue)).start(); 18 | new Thread(new Consumer(queue)).start(); 19 | new Thread(new Consumer(queue)).start(); 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 | class Consumer implements Runnable { 45 | private BlockingQueue queue; 46 | 47 | public Consumer(BlockingQueue queue) { 48 | this.queue = queue; 49 | } 50 | 51 | @Override 52 | public void run() { 53 | try { 54 | while(true) { 55 | Thread.sleep(new Random().nextInt(1000)); 56 | queue.take(); 57 | System.out.println(Thread.currentThread().getName() + "消费: " + queue.size()); 58 | } 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /upload/4ee51b01858d4fe39513918f09b19928.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsy0216/Community/bc2fb1573bd78179f631a9feece1207c11011674/upload/4ee51b01858d4fe39513918f09b19928.jpg -------------------------------------------------------------------------------- /upload/e41d6ebb2d1e499a9b132537ce55da1d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsy0216/Community/bc2fb1573bd78179f631a9feece1207c11011674/upload/e41d6ebb2d1e499a9b132537ce55da1d.jpg --------------------------------------------------------------------------------