├── .gitattributes ├── src └── main │ ├── resources │ ├── application.yml │ ├── static │ │ ├── css │ │ │ ├── login.css │ │ │ ├── letter.css │ │ │ ├── discuss-detail.css │ │ │ └── global.css │ │ ├── img │ │ │ ├── 404.png │ │ │ ├── error.png │ │ │ └── icon.png │ │ └── js │ │ │ ├── register.js │ │ │ ├── discuss.js │ │ │ ├── profile.js │ │ │ ├── global.js │ │ │ ├── index.js │ │ │ ├── letter.js │ │ │ └── bs-custom-file-input.js │ ├── templates │ │ ├── mail │ │ │ ├── forget.html │ │ │ └── activation.html │ │ ├── error │ │ │ ├── 404.html │ │ │ └── 500.html │ │ └── site │ │ │ ├── operate-result.html │ │ │ ├── my-post.html │ │ │ ├── my-comment.html │ │ │ ├── profile.html │ │ │ ├── followee.html │ │ │ ├── follower.html │ │ │ ├── register.html │ │ │ ├── login.html │ │ │ ├── setting.html │ │ │ ├── letter-detail.html │ │ │ ├── letter.html │ │ │ ├── admin │ │ │ └── data.html │ │ │ ├── forget.html │ │ │ └── notice.html │ ├── application-pro.yml │ ├── application-dev.yml │ └── mapper │ │ ├── UserMapper.xml │ │ ├── CommentMapper.xml │ │ ├── DiscussPostMapper.xml │ │ └── MessageMapper.xml │ └── java │ └── com │ └── community │ ├── entity │ ├── LoginTicket.java │ ├── Message.java │ ├── Comment.java │ ├── DiscussPost.java │ ├── User.java │ └── Page.java │ ├── CommunityApplication.java │ ├── annotation │ └── LoginRequired.java │ ├── service │ ├── LikeService.java │ ├── CommentService.java │ ├── FollowService.java │ ├── MessageService.java │ ├── DiscussPostService.java │ ├── UserService.java │ └── impl │ │ ├── MessageServiceImpl.java │ │ ├── DiscussPostServiceImpl.java │ │ ├── LikeServiceImpl.java │ │ ├── CommentServiceImpl.java │ │ ├── FollowServiceImpl.java │ │ └── UserServiceImpl.java │ ├── utils │ ├── UserThreadLocal.java │ ├── CommonUtil.java │ ├── CookieUtil.java │ ├── Constant.java │ ├── MailClient.java │ └── RedisKeyUtil.java │ ├── vo │ └── ResultVo.java │ ├── mapper │ ├── UserMapper.java │ ├── CommentMapper.java │ ├── MessageMapper.java │ └── DiscussPostMapper.java │ ├── config │ ├── RedisConfig.java │ ├── KaptchaConfig.java │ └── WebMvcConfig.java │ ├── controller │ ├── CommentController.java │ ├── advice │ │ └── ExceptionAdvice.java │ ├── LikeController.java │ ├── HomeController.java │ ├── FollowController.java │ ├── MessageController.java │ ├── LoginController.java │ ├── DiscussPostController.java │ └── UserController.java │ └── interceptor │ ├── LoginRequiredInterceptor.java │ └── LoginTicketInterceptor.java ├── .gitignore ├── README.md ├── community.sql └── pom.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=java -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: dev -------------------------------------------------------------------------------- /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/weizujie/community/HEAD/src/main/resources/static/img/404.png -------------------------------------------------------------------------------- /src/main/resources/static/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizujie/community/HEAD/src/main/resources/static/img/error.png -------------------------------------------------------------------------------- /src/main/resources/static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizujie/community/HEAD/src/main/resources/static/img/icon.png -------------------------------------------------------------------------------- /src/main/resources/static/js/register.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $("input").focus(clear_error); 3 | }); 4 | 5 | function clear_error() { 6 | $(this).removeClass("is-invalid"); 7 | } -------------------------------------------------------------------------------- /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/discuss-detail.css: -------------------------------------------------------------------------------- 1 | .content { 2 | font-size: 16px; 3 | line-height: 2em; 4 | } 5 | 6 | .replyform textarea { 7 | width: 100%; 8 | height: 200px; 9 | } 10 | 11 | .floor { 12 | background: #dcdadc; 13 | padding: 4px 12px; 14 | border-radius: 3px; 15 | font-size: 14px; 16 | } 17 | 18 | .input-size { 19 | width: 100%; 20 | height: 35px; 21 | } -------------------------------------------------------------------------------- /src/main/java/com/community/entity/LoginTicket.java: -------------------------------------------------------------------------------- 1 | package com.community.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * 登录凭证(后面会改成 Redis) 9 | */ 10 | @Data 11 | public class LoginTicket { 12 | 13 | 14 | private Integer id; 15 | 16 | private Integer userId; 17 | 18 | private String ticket; 19 | 20 | private Integer status; 21 | 22 | private Date expired; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/forget.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 忘记密码 7 | 8 | 9 |
10 |

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

13 |

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

17 |
18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/community/CommunityApplication.java: -------------------------------------------------------------------------------- 1 | package com.community; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | @MapperScan("com.community.mapper") 9 | public class CommunityApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(CommunityApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail/activation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 激活账号 7 | 8 | 9 |
10 |

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

13 |

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

18 |
19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/community/annotation/LoginRequired.java: -------------------------------------------------------------------------------- 1 | package com.community.annotation; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * 自定义注解,用来描述哪些方法被拦截器拦截(需要配置拦截器) 11 | * Target(ElementType.METHOD) -> 用来描述方法 12 | * Retention(RetentionPolicy.RUNTIME) -> 声明该注解有效的时长(程序运行的时候有效) 13 | */ 14 | @Target(ElementType.METHOD) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface LoginRequired { 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | src/main/resources/application-pro.yml 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ -------------------------------------------------------------------------------- /src/main/resources/static/js/discuss.js: -------------------------------------------------------------------------------- 1 | 2 | function like(btn, entityType, entityId, entityUserId) { 3 | $.post( 4 | "/like", 5 | {"entityType": entityType, "entityId": entityId, "entityUserId": entityUserId}, 6 | function (data) { 7 | data = $.parseJSON(data); 8 | if (data.code === 0) { 9 | $(btn).children("i").text(data.likeCount); 10 | $(btn).children("b").text(data.likeStatus === 1 ? '已赞' : '赞'); 11 | } else { 12 | alert(data.msg); 13 | } 14 | } 15 | ); 16 | } -------------------------------------------------------------------------------- /src/main/java/com/community/service/LikeService.java: -------------------------------------------------------------------------------- 1 | package com.community.service; 2 | 3 | public interface LikeService { 4 | 5 | /** 6 | * 点赞 7 | */ 8 | void like(int userId, int entityType, int entityId, int entityUserId); 9 | 10 | /** 11 | * 查询某实体点赞的数量 12 | */ 13 | long selectEntityLikeCount(int entityType, int entityId); 14 | 15 | /** 16 | * 查询某用户对某实体点赞的状态 17 | */ 18 | int selectEntityLikeStatus(int userId, int entityType, int entityId); 19 | 20 | /** 21 | * 查询某个用户获得的赞 22 | */ 23 | int selectUserLikeCount(int userId); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/community/utils/UserThreadLocal.java: -------------------------------------------------------------------------------- 1 | package com.community.utils; 2 | 3 | import com.community.entity.User; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * 持有用户的信息,用于代替 Session 对象 8 | */ 9 | @Component 10 | public class UserThreadLocal { 11 | 12 | private ThreadLocal users = new ThreadLocal<>(); 13 | 14 | public void setUsers(User user) { 15 | users.set(user); 16 | } 17 | 18 | public User getUser() { 19 | return users.get(); 20 | } 21 | 22 | public void clear() { 23 | users.remove(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/community/utils/CommonUtil.java: -------------------------------------------------------------------------------- 1 | package com.community.utils; 2 | 3 | 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.util.DigestUtils; 6 | 7 | import java.util.UUID; 8 | 9 | public class CommonUtil { 10 | 11 | // 生成随机字符串 12 | public static String generateUUID() { 13 | return UUID.randomUUID().toString().replaceAll("-", ""); 14 | } 15 | 16 | // MD5 + salt 加密 17 | public static String md5(String key) { 18 | // 字符串为空(包括空格)就不加密 19 | if (StringUtils.isBlank(key)) { 20 | return null; 21 | } 22 | return DigestUtils.md5DigestAsHex(key.getBytes()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/community/entity/Message.java: -------------------------------------------------------------------------------- 1 | package com.community.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableName; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | @Data 9 | @TableName("message") 10 | public class Message { 11 | 12 | private Integer id; 13 | // 消息发送用户 id( fromId为1表示系统用户,发送的不是私信而是通知) 14 | private Integer fromId; 15 | // 消息接收用户 id 16 | private Integer toId; 17 | // 会话 id(冗余字段,为了方便查询) 18 | private String conversationId; 19 | // 内容 20 | private String content; 21 | // 状态 0-未读 1-已读 2-删除 22 | private Integer status; 23 | // 创建时间 24 | private Date createTime; 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/community/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package com.community.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableName; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | @Data 9 | @TableName("comment") 10 | public class Comment { 11 | 12 | // 评论 id 13 | private Integer id; 14 | // 用户 id 15 | private Integer userId; 16 | // 实体类型 0-评论 1-回复(给评论的评论) 17 | private Integer entityType; 18 | // 实体 id 19 | private Integer entityId; 20 | // 回复或评论的目标用户 21 | private Integer targetId; 22 | // 正文 23 | private String content; 24 | // 状态 0-正常 25 | private Integer status; 26 | // 创建时间 27 | private Date createTime; 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/community/entity/DiscussPost.java: -------------------------------------------------------------------------------- 1 | package com.community.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableName; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | 9 | @Data 10 | @TableName("discuss_post") 11 | public class DiscussPost { 12 | 13 | // 帖子 id 14 | private Integer id; 15 | // 用户 id 16 | private Integer userId; 17 | // 标题 18 | private String title; 19 | // 内容 20 | private String content; 21 | // 分类 0-普通 1-置顶 22 | private Integer type; 23 | // 状态 0-正常 1-精华 2-拉黑 24 | private Integer status; 25 | // 评论数(冗余的写在这里,正确做法应该是在 comment 表里,但效率低) 26 | private Integer commentCount; 27 | // 分数 用于计算热贴排行 28 | private Double score; 29 | // 创建时间 30 | private Date createTime; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/community/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.community.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableName; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | @Data 9 | @TableName("user") 10 | public class User { 11 | 12 | // 用户 id 13 | private Integer id; 14 | // 用户名 15 | private String username; 16 | // 密码 17 | private String password; 18 | // 加密盐 19 | private String salt; 20 | // 邮箱 21 | private String email; 22 | // 用户类别 0-普通用户 1-超级管理员 2-版主 23 | private Integer type; 24 | // 状态 0-未激活 1-已激活 25 | private Integer status; 26 | // 激活码 27 | private String activationCode; 28 | // 头像 29 | private String headerUrl; 30 | // 创建时间 31 | private Date createTime; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/community/utils/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package com.community.utils; 2 | 3 | import javax.servlet.http.Cookie; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | /** 7 | * Cookie 工具类 8 | * 用来获取 cookie 里的值 9 | */ 10 | public class CookieUtil { 11 | 12 | public static String getValue(HttpServletRequest request, String name) { 13 | 14 | if (request == null || name == null) { 15 | throw new IllegalArgumentException("参数为空!"); 16 | } 17 | 18 | Cookie[] cookies = request.getCookies(); 19 | if (cookies != null) { 20 | for (Cookie cookie : cookies) { 21 | if (cookie.getName().equals(name)) { 22 | return cookie.getValue(); 23 | } 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/community/utils/Constant.java: -------------------------------------------------------------------------------- 1 | package com.community.utils; 2 | 3 | public class Constant { 4 | 5 | /** 6 | * 激活成功 7 | */ 8 | public static int ACTIVATION_SUCCESS = 0; 9 | 10 | /** 11 | * 重复激活 12 | */ 13 | public static int ACTIVATION_REPEAT = 1; 14 | 15 | /** 16 | * 激活失败 17 | */ 18 | public static int ACTIVATION_FAILURE = 2; 19 | 20 | /** 21 | * 默认状态的登录凭证的超市时间(12小时) 22 | */ 23 | public static int DEFAULT_EXPIRED_SECONDS = 3600 * 12; 24 | 25 | /** 26 | * 记住状态下的登录凭证的超时时间(3个月) 27 | */ 28 | public static int REMEMBER_EXPIRED_SECONDS = 3600 * 12 * 100; 29 | 30 | /** 31 | * 实体类型:帖子 32 | */ 33 | public static int ENTITY_TYPE_POST = 1; 34 | 35 | /** 36 | * 实体类型:评论 37 | */ 38 | public static int ENTITY_TYPE_COMMENT = 2; 39 | 40 | /** 41 | * 实体类型:用户 42 | */ 43 | public static int ENTITY_TYPE_USER = 3; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/community/vo/ResultVo.java: -------------------------------------------------------------------------------- 1 | package com.community.vo; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | 5 | import java.util.Map; 6 | 7 | public class ResultVo { 8 | 9 | // 返回统一数据格式 10 | public static String getJsonString(int code, String msg, Map map) { 11 | JSONObject jsonObject = new JSONObject(); 12 | jsonObject.put("code", code); 13 | jsonObject.put("msg", msg); 14 | if (map != null) { 15 | // 遍历 map 有三种方法,这里使用遍历 key 的方法 16 | for (String key : map.keySet()) { 17 | jsonObject.put(key, map.get(key)); 18 | } 19 | } 20 | return jsonObject.toJSONString(); 21 | } 22 | 23 | public static String getJsonString(int code, String msg) { 24 | return getJsonString(code, msg, null); 25 | } 26 | 27 | public static String getJsonString(int code) { 28 | return getJsonString(code, null, null); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.community.service; 2 | 3 | import com.community.entity.Comment; 4 | 5 | import java.util.List; 6 | 7 | public interface CommentService { 8 | 9 | /** 10 | * 根据实体查询评论 11 | * 12 | * @param entityType 实体类型 0-帖子 1-评论 13 | * @param entityId 实体 id 14 | * @param offset 每页起始行行号 15 | * @param limit 一页显示多少条数据 16 | */ 17 | List selectCommentByEntity(int entityType, int entityId, int offset, int limit); 18 | 19 | /** 20 | * 根据实体查询评论数 21 | * 22 | * @param entityType 实体类型 0-帖子 1-评论 23 | * @param entityId 实体 id 24 | */ 25 | Long selectCount(Integer entityType, Integer entityId); 26 | 27 | /** 28 | * 增加评论 29 | */ 30 | Boolean insertComment(Comment comment); 31 | 32 | /** 33 | * 根据用户 id 查询评论数 34 | */ 35 | int selectCountByUserId(int userId); 36 | 37 | 38 | List selectCommentByUserId(int userId, int offset, int limit); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/FollowService.java: -------------------------------------------------------------------------------- 1 | package com.community.service; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | public interface FollowService { 7 | 8 | /** 9 | * 关注 10 | */ 11 | void follow(int userId, int entityType, int entityId); 12 | 13 | /** 14 | * 取消关注 15 | */ 16 | void unfollow(int userId, int entityType, int entityId); 17 | 18 | /** 19 | * 查询某用户关注实体的数量 20 | */ 21 | long selectFolloweeCount(int userId, int entityType); 22 | 23 | /** 24 | * 查询实体的粉丝的数量 25 | */ 26 | long selectFollowerCount(int entityType, int entityId); 27 | 28 | /** 29 | * 查询当前用户是否已关注该实体 30 | */ 31 | boolean hasFollowed(int userId, int entityType, int entityId); 32 | 33 | /** 34 | * 查询某用户关注的用户 35 | */ 36 | List> selectFolloweeList(int userId, int offset, int limit); 37 | 38 | /** 39 | * 查询某用户的粉丝 40 | */ 41 | List> selectFollowerList(int userId, int offset, int limit); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/community/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.community.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.community.entity.User; 5 | 6 | import java.util.Map; 7 | 8 | public interface UserMapper extends BaseMapper { 9 | 10 | /** 11 | * 根据用户 id 查询用户 12 | */ 13 | User selectById(int id); 14 | 15 | /** 16 | * 根据用户名查询用户 17 | */ 18 | User selectByUsername(String username); 19 | 20 | 21 | /** 22 | * 根据邮箱查询用户 23 | */ 24 | User selectByEmail(String email); 25 | 26 | /** 27 | * 添加用户 28 | */ 29 | int insertUser(User user); 30 | 31 | /** 32 | * 注册用户 33 | */ 34 | Map register(User user); 35 | 36 | 37 | /** 38 | * 修改用户状态 39 | */ 40 | int updateStatus(int id, int status); 41 | 42 | /** 43 | * 更新用户头像 44 | */ 45 | int updateHeaderUrl(int id, String headerUrl); 46 | 47 | /** 48 | * 修改密码 49 | */ 50 | int changePassword(int id, String password); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/resources/application-pro.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | datasource: 6 | driver-class-name: com.mysql.cj.jdbc.Driver 7 | url: jdbc:mysql://server:port/community?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai 8 | username: 9 | password: 10 | 11 | thymeleaf: 12 | cache: true 13 | 14 | mail: 15 | host: smtp.163.com # smtp.qq.com 16 | username: 17 | password: 18 | default-encoding: UTF-8 19 | properties: 20 | mail: 21 | smtp: 22 | ssl: 23 | enable: true 24 | redis: 25 | database: 11 26 | host: localhost 27 | port: 6379 28 | 29 | mybatis: 30 | configuration: 31 | map-underscore-to-camel-case: true 32 | use-generated-keys: true 33 | mapper-locations: classpath:mapper/*Mapper.xml 34 | type-aliases-package: com.community.entity 35 | 36 | community: 37 | path: 38 | domain: http://server:8080 39 | 40 | aliyun: 41 | oss: 42 | file: 43 | endpoint: 44 | keyid: 45 | keysecret: 46 | bucketname: 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.community.service; 2 | 3 | import com.community.entity.Message; 4 | 5 | import java.util.List; 6 | 7 | public interface MessageService { 8 | /** 9 | * 查询当前用户的会话列表,针对每个会话返回一条最新的私信 10 | */ 11 | List selectConversations(int userId, int offset, int limit); 12 | 13 | /** 14 | * 查询当前用户的会话数量 15 | */ 16 | int selectConversationCount(int userId); 17 | 18 | /** 19 | * 查询某个会话所包含的私信列表 20 | */ 21 | List selectLetters(String conversationId, int offset, int limit); 22 | 23 | /** 24 | * 查询某个会话所包含的私信数量 25 | */ 26 | int selectLetterCount(String conversationId); 27 | 28 | /** 29 | * 查询未读私信的数量 30 | * conversationId 作为动态拼接条件, 31 | * 如果不拼接则查询所有的未读私信的数量,否则只查询某个用户的未读私信数量 32 | */ 33 | int selectLetterUnreadCount(int userId, String conversationId); 34 | 35 | /** 36 | * 新增消息 37 | */ 38 | int insertMessage(Message message); 39 | 40 | /** 41 | * 修改消息的状态,将所有消息变为已读 42 | */ 43 | int updateStatus(List ids); 44 | } 45 | -------------------------------------------------------------------------------- /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 | } 17 | } 18 | ); 19 | // $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary"); 20 | } else { 21 | // 取消关注 22 | $.post( 23 | "/unfollow", 24 | {"entityType": 3, "entityId": $(btn).prev().val()}, 25 | function (data) { 26 | data = $.parseJSON(data); 27 | if (data.code === 0) { 28 | window.location.reload(); 29 | } 30 | } 31 | ); 32 | // $(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info"); 33 | } 34 | } -------------------------------------------------------------------------------- /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/java/com/community/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.community.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.community.entity.Comment; 5 | 6 | import java.util.List; 7 | 8 | public interface CommentMapper extends BaseMapper { 9 | 10 | /** 11 | * 根据实体查询评论 12 | * 13 | * @param entityType 实体类型 0-帖子 1-评论 14 | * @param entityId 实体 id 15 | * @param offset 每页起始行行号 16 | * @param limit 一页显示多少条数据 17 | */ 18 | List selectCommentByEntity(int entityType, int entityId, int offset, int limit); 19 | 20 | /** 21 | * 根据实体查询评论数 22 | * 23 | * @param entityType 实体类型 0-帖子 1-评论 24 | * @param entityId 实体 id 25 | */ 26 | int selectCountByEntity(int entityType, int entityId); 27 | 28 | /** 29 | * 增加评论 30 | */ 31 | int insertComment(Comment comment); 32 | 33 | /** 34 | * 根据用户 id 查询评论数 35 | */ 36 | int selectCountByUserId(int userId); 37 | 38 | /** 39 | * 根据用户 id 查询评论 40 | */ 41 | List selectCommentByUserId(int userId, int offset, int limit); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 404 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 500 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/com/community/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.community.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.RedisSerializer; 8 | 9 | @Configuration 10 | public class RedisConfig { 11 | 12 | @Bean 13 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) { 14 | RedisTemplate template = new RedisTemplate<>(); 15 | template.setConnectionFactory(factory); 16 | 17 | // 设置 key 序列化方式 18 | template.setKeySerializer(RedisSerializer.string()); 19 | // 设置 value 序列化方法 20 | template.setValueSerializer(RedisSerializer.json()); 21 | // 设置 hash 的 key 序列化方式 22 | template.setHashKeySerializer(RedisSerializer.string()); 23 | // 设置 hash 的value 序列化方式 24 | template.setHashValueSerializer(RedisSerializer.json()); 25 | 26 | template.afterPropertiesSet(); 27 | return template; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/community/mapper/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.community.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.community.entity.Message; 5 | 6 | import java.util.List; 7 | 8 | public interface MessageMapper extends BaseMapper { 9 | 10 | /** 11 | * 查询当前用户的会话列表,针对每个会话返回一条最新的私信 12 | */ 13 | List selectConversations(int userId, int offset, int limit); 14 | 15 | /** 16 | * 查询当前用户的会话数量 17 | */ 18 | int selectConversationCount(int userId); 19 | 20 | /** 21 | * 查询某个会话所包含的私信列表 22 | */ 23 | List selectLetters(String conversationId, int offset, int limit); 24 | 25 | /** 26 | * 查询某个会话所包含的私信数量 27 | */ 28 | int selectLetterCount(String conversationId); 29 | 30 | /** 31 | * 查询未读私信的数量 32 | * conversationId 作为动态拼接条件, 33 | * 如果不拼接则查询所有的未读私信的数量,否则只查询某个用户的未读私信数量 34 | */ 35 | int selectLetterUnreadCount(int userId, String conversationId); 36 | 37 | /** 38 | * 新增消息 39 | */ 40 | int insertMessage(Message message); 41 | 42 | /** 43 | * 修改消息的状态,将所有消息变为已读 44 | */ 45 | int updateStatus(List ids, int status); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/DiscussPostService.java: -------------------------------------------------------------------------------- 1 | package com.community.service; 2 | 3 | 4 | import com.community.entity.DiscussPost; 5 | 6 | import java.util.List; 7 | 8 | public interface DiscussPostService { 9 | /** 10 | * 查询用户帖子列表 11 | * 12 | * @param userId 用户 id 13 | * @param offset 每页起始行行号 14 | * @param limit 一页显示多少条数据 15 | * @return List 16 | */ 17 | List selectDiscussPosts(int userId, int offset, int limit); 18 | 19 | /** 20 | * 查询帖子的行数 21 | * 22 | * @param userId 用户 id 23 | * @return 帖子行数 24 | */ 25 | int selectDiscussPostRows(int userId); 26 | 27 | /** 28 | * 发布帖子 29 | */ 30 | int insertDiscussPost(DiscussPost discussPost); 31 | 32 | /** 33 | * 查询帖子详情 34 | */ 35 | DiscussPost selectDiscussPostById(int id); 36 | 37 | /** 38 | * 更新帖子的评论数量 39 | * 40 | * @param id 帖子 id 41 | * @param commentCount 评论数量 42 | */ 43 | int updateCommentCount(int id, Long commentCount); 44 | 45 | /** 46 | * 查询某用户的帖子数量 47 | * 48 | * @param userId 用户 id 49 | */ 50 | int selectCountByUserId(int userId); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | datasource: 6 | driver-class-name: com.mysql.cj.jdbc.Driver 7 | url: jdbc:mysql://localhost:3306/community?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai 8 | username: root 9 | password: toor 10 | # thymeleaf 11 | thymeleaf: 12 | cache: false 13 | 14 | # email 15 | mail: 16 | host: 17 | username: 18 | password: 19 | default-encoding: UTF-8 20 | properties: 21 | mail: 22 | smtp: 23 | ssl: 24 | enable: true 25 | # redis 26 | redis: 27 | database: 11 28 | host: localhost 29 | port: 6379 30 | 31 | mybatis: 32 | configuration: 33 | # 驼峰转换 34 | map-underscore-to-camel-case: true 35 | # 自动生成 id 36 | use-generated-keys: true 37 | mapper-locations: classpath:mapper/*Mapper.xml 38 | type-aliases-package: com.community.entity 39 | 40 | # community 41 | community: 42 | path: 43 | domain: http://localhost:8080 44 | 45 | 46 | # aliyun OSS 47 | aliyun: 48 | oss: 49 | file: 50 | endpoint: 51 | keyid: 52 | keysecret: 53 | # bucket可以在控制台创建,也可以使用 java 代码创建 54 | bucketname: 55 | 56 | -------------------------------------------------------------------------------- /src/main/resources/static/js/index.js: -------------------------------------------------------------------------------- 1 | // 页面加载完之后初始化这个按钮,增加一个点击事件 2 | $(function () { 3 | $("#publishBtn").click(publish); 4 | }); 5 | 6 | function publish() { 7 | // 显示发帖框 8 | $("#publishModal").modal("hide"); 9 | 10 | // 获取标题和内容 11 | var title = $("#recipient-name").val(); 12 | var content = $("#message-text").val(); 13 | if (title !== "" && content !== "") { 14 | // 发送异步请求 15 | $.post( 16 | "/post/add", // url 17 | {"title": title, "content": content}, // data 18 | function (data) { // 这个 data 是后台传过来的数据(String) 19 | data = $.parseJSON(data); // 转换为 json 对象 20 | // 在提示框中显示返回消息 21 | $("#hintBody").text(data.msg); 22 | // 显示提示框 23 | $("#hintModal").modal("show"); 24 | // 2 秒后隐藏 25 | setTimeout(function () { 26 | $("#hintModal").modal("hide"); 27 | // 刷新页面 28 | if (data.code === 0) { 29 | window.location.reload(); 30 | } 31 | }, 2000); 32 | } 33 | ); 34 | } else { 35 | alert("标题或内容不能为空!"); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /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 | // 显示发私信框 8 | $("#sendModal").modal("hide"); 9 | 10 | // 获取目标用户和内容 11 | var toName = $("#recipient-name").val(); 12 | var content = $("#message-text").val(); 13 | 14 | if (toName !== "" && content !== "") { 15 | // 发送异步请求 16 | $.post( 17 | "/letter/send", 18 | {"toName": toName, "content": content}, 19 | function (data) { 20 | data = $.parseJSON(data); 21 | $("#hintModal").modal("show"); 22 | if (data.code === 0) { 23 | $("#hintBody").text("发送成功!"); 24 | } else { 25 | $("#hintBody").text(data.msg); 26 | } 27 | setTimeout(function () { 28 | $("#hintModal").modal("hide"); 29 | location.reload(); 30 | }, 2000); 31 | } 32 | ); 33 | } else { 34 | alert("目标用户或内容不能为空!"); 35 | } 36 | 37 | } 38 | 39 | function delete_msg() { 40 | // TODO 删除数据 41 | $(this).parents(".media").remove(); 42 | } -------------------------------------------------------------------------------- /src/main/java/com/community/config/KaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package com.community.config; 2 | 3 | import com.google.code.kaptcha.Producer; 4 | import com.google.code.kaptcha.impl.DefaultKaptcha; 5 | import com.google.code.kaptcha.util.Config; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.util.Properties; 10 | 11 | /** 12 | * Kaptcha 配置类,用于生成验证码 13 | */ 14 | @Configuration 15 | public class KaptchaConfig { 16 | 17 | @Bean 18 | public Producer kaptchaProducer() { 19 | 20 | Properties properties = new Properties(); 21 | properties.setProperty("kaptcha.image.width", "100"); 22 | properties.setProperty("kaptcha.image.height", "40"); 23 | properties.setProperty("kaptcha.textproducer.font.size", "32"); 24 | properties.setProperty("kaptcha.textproducer.font.color", "black"); 25 | properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); 26 | properties.setProperty("kaptcha.textproducer.char.length", "4"); 27 | 28 | DefaultKaptcha kaptcha = new DefaultKaptcha(); 29 | Config config = new Config(properties); 30 | kaptcha.setConfig(config); 31 | return kaptcha; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/community/utils/MailClient.java: -------------------------------------------------------------------------------- 1 | package com.community.utils; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.mail.javamail.JavaMailSender; 6 | import org.springframework.mail.javamail.MimeMessageHelper; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.mail.MessagingException; 10 | import javax.mail.internet.MimeMessage; 11 | 12 | /** 13 | * 用于发送邮件的工具类 14 | */ 15 | @Component 16 | public class MailClient { 17 | 18 | @Autowired 19 | private JavaMailSender mailSender; 20 | 21 | @Value("${spring.mail.username}") 22 | private String from; 23 | 24 | public void sendMail(String to, String subject, String content) { 25 | try { 26 | MimeMessage message = mailSender.createMimeMessage(); 27 | MimeMessageHelper helper = new MimeMessageHelper(message); 28 | helper.setFrom(from); 29 | helper.setTo(to); 30 | helper.setSubject(subject); 31 | helper.setText(content, true); 32 | mailSender.send(helper.getMimeMessage()); 33 | } catch (MessagingException e) { 34 | e.printStackTrace(); 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/community/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.community.config; 2 | 3 | import com.community.interceptor.LoginRequiredInterceptor; 4 | import com.community.interceptor.LoginTicketInterceptor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @Configuration 10 | public class WebMvcConfig implements WebMvcConfigurer { 11 | 12 | private final LoginTicketInterceptor loginTicketInterceptor; 13 | private final LoginRequiredInterceptor loginRequiredInterceptor; 14 | 15 | public WebMvcConfig(LoginTicketInterceptor loginTicketInterceptor, LoginRequiredInterceptor loginRequiredInterceptor) { 16 | this.loginTicketInterceptor = loginTicketInterceptor; 17 | this.loginRequiredInterceptor = loginRequiredInterceptor; 18 | } 19 | 20 | @Override 21 | public void addInterceptors(InterceptorRegistry registry) { 22 | registry.addInterceptor(loginTicketInterceptor) 23 | // 除了静态资源外都拦截 24 | .excludePathPatterns("/static/**"); 25 | 26 | registry.addInterceptor(loginRequiredInterceptor) 27 | // 除了静态资源外都拦截 28 | .excludePathPatterns("/static/**"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/community/mapper/DiscussPostMapper.java: -------------------------------------------------------------------------------- 1 | package com.community.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.community.entity.DiscussPost; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.List; 8 | 9 | public interface DiscussPostMapper extends BaseMapper { 10 | 11 | /** 12 | * 查询用户帖子列表 13 | * 14 | * @param userId 用户 id 15 | * @param offset 每页起始行行号 16 | * @param limit 一页显示多少条数据 17 | * @return List 18 | */ 19 | List selectDiscussPosts(int userId, int offset, int limit); 20 | 21 | 22 | /** 23 | * 查询帖子的行数 24 | * Param()注解:用于给参数取别名。如果只有一个参数,并且在 里使用,则必须加别名 25 | * 26 | * @param userId 用户 id 27 | * @return 帖子行数 28 | */ 29 | int selectDiscussPostRows(@Param("userId") int userId); 30 | 31 | /** 32 | * 发布帖子 33 | */ 34 | int insertDiscussPost(DiscussPost discussPost); 35 | 36 | /** 37 | * 查询帖子详情 38 | */ 39 | DiscussPost selectDiscussPostById(int id); 40 | 41 | /** 42 | * 更新帖子的评论数量 43 | * 44 | * @param id 帖子 id 45 | * @param commentCount 评论数量 46 | */ 47 | int updateCommentCount(int id, Long commentCount); 48 | 49 | /** 50 | * 查询某用户帖子数量 51 | * 52 | * @param userId 用户 id 53 | */ 54 | int selectCountByUserId(int userId); 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | import com.community.annotation.LoginRequired; 4 | import com.community.entity.Comment; 5 | import com.community.service.CommentService; 6 | import com.community.utils.UserThreadLocal; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import java.util.Date; 14 | 15 | @Controller 16 | @RequestMapping("/comment") 17 | public class CommentController { 18 | 19 | @Autowired 20 | private CommentService commentService; 21 | 22 | @Autowired 23 | private UserThreadLocal userThreadLocal; 24 | 25 | /** 26 | * 增加评论/回复 27 | * 28 | * @param discussPostId 帖子 id 29 | * @param comment 评论/回复 30 | */ 31 | @LoginRequired 32 | @PostMapping("/add/{discussPostId}") 33 | public String addComment(@PathVariable int discussPostId, Comment comment) { 34 | comment.setUserId(userThreadLocal.getUser().getId()); 35 | comment.setStatus(0); 36 | comment.setTargetId(0); 37 | comment.setCreateTime(new Date()); 38 | 39 | commentService.insertComment(comment); 40 | return "redirect:/post/detail/" + discussPostId; 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/advice/ExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package com.community.controller.advice; 2 | 3 | import com.community.vo.ResultVo; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.io.IOException; 12 | import java.io.PrintWriter; 13 | 14 | /** 15 | * 扫描所有带 controller 注解的类 16 | */ 17 | @ControllerAdvice(annotations = Controller.class) 18 | @Slf4j 19 | public class ExceptionAdvice { 20 | 21 | @ExceptionHandler({Exception.class}) 22 | public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException { 23 | log.error("服务器发生异常!" + e.getMessage()); 24 | for (StackTraceElement element : e.getStackTrace()) { 25 | log.error(element.toString()); 26 | } 27 | 28 | String xRequestedWith = request.getHeader("x-requested-with"); 29 | // 异步请求 30 | if ("XMLHttpRequest".equals(xRequestedWith)) { 31 | response.setContentType("application/plain;charset=utf-8"); 32 | PrintWriter writer = response.getWriter(); 33 | writer.write(ResultVo.getJsonString(-1, "服务器异常!")); 34 | } else { 35 | response.sendRedirect(request.getContextPath() + "/error"); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.community.service; 2 | 3 | import com.community.entity.DiscussPost; 4 | import com.community.entity.LoginTicket; 5 | import com.community.entity.User; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public interface UserService { 11 | 12 | /** 13 | * 根据用户 id 查询用户 14 | */ 15 | User selectById(int id); 16 | 17 | /** 18 | * 根据用户名查询用户 19 | */ 20 | User selectByUsername(String username); 21 | 22 | /** 23 | * 根据邮箱查询用户 24 | */ 25 | User selectByEmail(String email); 26 | 27 | /** 28 | * 添加用户 29 | */ 30 | int insertUser(User user); 31 | 32 | /** 33 | * 注册用户 34 | */ 35 | Map register(User user); 36 | 37 | /** 38 | * 修改用户状态 39 | */ 40 | int updateStatus(int userId, int status); 41 | 42 | /** 43 | * 激活用户 44 | */ 45 | int activation(int userId, String code); 46 | 47 | /** 48 | * 用户登录 49 | */ 50 | Map login(String username, String password, int expired); 51 | 52 | /** 53 | * 用户退出 54 | */ 55 | void logout(String ticket); 56 | 57 | /** 58 | * 根据 ticket 查询用户 59 | */ 60 | LoginTicket selectByTicket(String ticket); 61 | 62 | /** 63 | * 更新用户头像 64 | */ 65 | int updateHeaderUrl(int userId, String avatarUrl); 66 | 67 | /** 68 | * 修改密码 69 | */ 70 | Map changePassword(int id, String oldPassword, String newPassword, String confirmPassword); 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/community/interceptor/LoginRequiredInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.community.interceptor; 2 | 3 | import com.community.annotation.LoginRequired; 4 | import com.community.utils.UserThreadLocal; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.method.HandlerMethod; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.lang.reflect.Method; 13 | 14 | /** 15 | * 用户登录拦截器 16 | * 使用自定义注解,作用于方法上,哪个方法有该注解就拦截哪个方法 17 | * 作用:如果用户没有登录就访问如 /user/profile 等页面就会被拦截,强制跳转到登陆页面 18 | */ 19 | @Component 20 | public class LoginRequiredInterceptor implements HandlerInterceptor { 21 | 22 | @Autowired 23 | private UserThreadLocal userThreadLocal; 24 | 25 | @Override 26 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 27 | // 判断拦截的目标是否为方法 28 | if (handler instanceof HandlerMethod) { 29 | HandlerMethod handlerMethod = (HandlerMethod) handler; 30 | Method method = handlerMethod.getMethod(); 31 | LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); 32 | // 判断用户是否登录 33 | if (loginRequired != null && userThreadLocal.getUser() == null) { 34 | // 如果没有登录,跳转到登陆页面 35 | response.sendRedirect(request.getContextPath() + "/login"); 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/LikeController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | import com.community.annotation.LoginRequired; 4 | import com.community.entity.User; 5 | import com.community.service.LikeService; 6 | import com.community.utils.UserThreadLocal; 7 | import com.community.vo.ResultVo; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Controller 17 | public class LikeController { 18 | 19 | @Autowired 20 | private LikeService likeService; 21 | 22 | @Autowired 23 | private UserThreadLocal userThreadLocal; 24 | 25 | /** 26 | * 点赞(异步请求) 27 | * 28 | * @param entityType 实体类型(用户/帖子/回复) 29 | * @param entityId 实体 id 30 | * @param entityUserId 实体用户 id 31 | */ 32 | @PostMapping("/like") 33 | @ResponseBody 34 | @LoginRequired 35 | public String like(int entityType, int entityId, int entityUserId) { 36 | User curUser = userThreadLocal.getUser(); 37 | // 点赞 38 | likeService.like(curUser.getId(), entityType, entityId, entityUserId); 39 | // 数量 40 | long likeCount = likeService.selectEntityLikeCount(entityType, entityId); 41 | // 状态 42 | int likeStatus = likeService.selectEntityLikeStatus(curUser.getId(), entityType, entityId); 43 | // 返回结果 44 | Map map = new HashMap<>(); 45 | map.put("likeCount", likeCount); 46 | map.put("likeStatus", likeStatus); 47 | 48 | return ResultVo.getJsonString(0, null, map); 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /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 |
18 |
19 |
20 |

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

21 |
22 |

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

26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Community 2 | 3 | > :bulb: 基于SpringBoot + Mybatis + Redis 开发的一个问答社区,实现了基本的登录注册、发帖、评论、点赞、回复等功能。 4 | 5 | ## 项目介绍 6 | 7 | - 主要功能: 8 | 9 | - 使用 ThreadLocal 保存用户状态,通过拦截器拦截请求,根据自定义注解判断用户登录状态 10 | - 使用 Ajax 异步发帖、发送私信、评论 11 | - 使用 Redis 实现点赞、关注功能,优化登录模块——存储登录凭证、缓存用户信息 12 | 13 | ## 运行效果 14 | 15 | ### 首页 16 | 17 | ![首页](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210122180027532.png) 18 | 19 | ### 帖子评论/回复 20 | 21 | ![帖子评论/回复](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210122180557769.png) 22 | 23 | ### 个人主页 24 | 25 | ![个人主页](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210122180108327.png) 26 | 27 | ### 我的帖子 28 | 29 | ![我的帖子](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210128143205727.png) 30 | 31 | ### 我的评论 32 | 33 | ![我的评论](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210128215706715.png) 34 | 35 | ### 发送私信且未读 36 | 37 | ![发送私信且未读](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210122180750435.png) 38 | 39 | ### 私信列表 40 | 41 | ![私信列表](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210122180429898.png) 42 | 43 | ### 关注列表 44 | 45 | ![关注列表](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210123100146107.png) 46 | 47 | ### 粉丝列表 48 | 49 | ![粉丝列表](https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/image-20210123100114699.png) 50 | 51 | ## 知识整理 52 | 53 | ### Interceptor 的作用?在项目中哪些地方用到? 54 | 55 | SpringMVC 中的 Interceptor 拦截请求是通过 HandlerInterceptor 来实现的。主要作用是**拦截用户的请求并进行相应的处理**,比如判断用户是否登录。HandlerInterceptor 56 | 中实现了三个方法: 57 | 58 | - preHandle():在 Controller 中方法调用之前执行,若返回值为 true,则继续执行下一个 handle,否则停止执行 59 | 60 | - postHandle():在 Controller 中方法调用之后,DispatcherServlet 进行视图的渲染之前执行(前提是 preHandle() 返回 true) 61 | 62 | - afterCompletion():该方法将在整个请求完成之后,也就是DispatcherServlet渲染了视图执行(前提是 preHandle() 返回 true) 63 | 64 | 该项目中,每次请求都会检查 request 中的 login_ticket,把找到的 user 信息存在 ThreadLocal 中,在完成请求的处理后自动释放。 65 | -------------------------------------------------------------------------------- /src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | `id` 7 | , `username`, `password`, `salt`, `email`, `type`, `status`, `activation_code`, `header_url`, `create_time` 8 | 9 | 10 | 11 | `username` 12 | , `password`, `salt`, `email`, `type`, `status`, `activation_code`, `header_url`, `create_time` 13 | 14 | 15 | 21 | 22 | 28 | 29 | 35 | 36 | 37 | INSERT INTO user () 38 | VALUES(#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, 39 | #{createTime}) 40 | 41 | 42 | 43 | UPDATE user 44 | SET status = #{status} 45 | WHERE id = #{id} 46 | 47 | 48 | 49 | UPDATE user 50 | SET header_url = #{headerUrl} 51 | WHERE id = #{id} 52 | 53 | 54 | 55 | UPDATE user 56 | SET password = #{password} 57 | WHERE id = #{id} 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/resources/mapper/CommentMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | id 7 | , user_id, entity_type, entity_id, target_id, content,status, create_time 8 | 9 | 10 | 11 | user_id 12 | , entity_type, entity_id, target_id, content,status, create_time 13 | 14 | 15 | 25 | 26 | 33 | 34 | 35 | INSERT INTO comment() 36 | VALUES(#{userId} ,#{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime}) 37 | 38 | 39 | 46 | 47 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/resources/mapper/DiscussPostMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | id 7 | ,user_id, title, content, `type`, status, create_time, comment_count, score 8 | 9 | 10 | 11 | user_id 12 | , title, content, `type`, status, create_time, comment_count, score 13 | 14 | 15 | 26 | 27 | 35 | 36 | 37 | INSERT INTO discuss_post() 38 | VALUES (#{userId}, #{title}, #{content}, #{type}, #{status}, #{createTime}, #{commentCount}, #{score}) 39 | 40 | 41 | 47 | 48 | 49 | UPDATE discuss_post 50 | SET comment_count = #{commentCount} 51 | WHERE id = #{id} 52 | 53 | 54 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/main/java/com/community/utils/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.community.utils; 2 | 3 | public class RedisKeyUtil { 4 | 5 | // 分隔符 6 | private static final String SPLIT = ":"; 7 | // 点赞实体 8 | private static final String PREFIX_ENTITY_LIKE = "like:entity"; 9 | // 点赞用户 10 | private static final String PREFIX_USER_LIKE = "like:user"; 11 | // 关注的目标 12 | private static final String PREFIX_FOLLOWEE = "followee"; 13 | // 被关注目标的粉丝 14 | private static final String PREFIX_FOLLOWER = "follower"; 15 | // 登录验证码 16 | private static final String PREFIX_KAPTCHA = "kaptcha"; 17 | // 登录凭证 18 | private static final String PREFIX_TICKET = "ticket"; 19 | // 用户 20 | private static final String PREFIX_USER = "user"; 21 | 22 | /** 23 | * 某个实体的赞 24 | * like:entity:entityType:entityId -> set(userId) 25 | */ 26 | public static String getEntityLikeKey(int entityType, int entityId) { 27 | return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId; 28 | } 29 | 30 | /** 31 | * 某个用户的赞 32 | * like:user:userId -> int 33 | */ 34 | public static String getUserLikeKey(int userId) { 35 | return PREFIX_USER_LIKE + SPLIT + userId; 36 | } 37 | 38 | /** 39 | * 某个用户关注的实体(某个用户关注了某个实体),存在有序列表 zset 中,以当前时间排序 40 | * followee:userId:entityType -> zset(entityId, now) 41 | */ 42 | public static String getFolloweeKey(int userId, int entityType) { 43 | return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; 44 | } 45 | 46 | /** 47 | * 某个用户拥有的粉丝 48 | * follower:entityType:entityId -> zset(userId, now) 49 | */ 50 | public static String getFollowerKey(int entityType, int entityId) { 51 | return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId; 52 | } 53 | 54 | /** 55 | * 登录验证码 56 | * 57 | * @param owner 验证码的临时凭证(随机字符串) 58 | */ 59 | public static String getKaptchaKey(String owner) { 60 | return PREFIX_KAPTCHA + SPLIT + owner; 61 | } 62 | 63 | /** 64 | * 登录凭证 65 | */ 66 | public static String getTicketKey(String ticket) { 67 | return PREFIX_TICKET + SPLIT + ticket; 68 | } 69 | 70 | /** 71 | * 用户 72 | */ 73 | public static String getUserKey(int userId) { 74 | return PREFIX_USER + SPLIT + userId; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/community/entity/Page.java: -------------------------------------------------------------------------------- 1 | package com.community.entity; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 封装分页相关的信息 9 | */ 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class Page { 13 | 14 | // 当前页码 15 | private Integer current = 1; 16 | // 显示上限 17 | private Integer limit = 10; 18 | // 数据总数(用于计算总页数) 19 | private Integer rows; 20 | // 查询路径(用于复用分页链接) 21 | private String path; 22 | 23 | public Integer getCurrent() { 24 | return current; 25 | } 26 | 27 | public void setCurrent(Integer current) { 28 | if (current >= 1) { 29 | this.current = current; 30 | } 31 | } 32 | 33 | public Integer getLimit() { 34 | return limit; 35 | } 36 | 37 | public void setLimit(Integer limit) { 38 | if (limit >= 1 && limit <= 100) { 39 | this.limit = limit; 40 | } 41 | } 42 | 43 | public Integer getRows() { 44 | return rows; 45 | } 46 | 47 | public void setRows(Integer rows) { 48 | if (rows >= 0) { 49 | this.rows = rows; 50 | } 51 | } 52 | 53 | public String getPath() { 54 | return path; 55 | } 56 | 57 | public void setPath(String path) { 58 | this.path = path; 59 | } 60 | 61 | /** 62 | * 获取当前页的起始行 63 | */ 64 | public Integer getOffset() { 65 | // current * limit - limit 66 | return (current - 1) * limit; 67 | } 68 | 69 | /** 70 | * 获取总页数 71 | */ 72 | public Integer getTotal() { 73 | // rows / limit [+1] 74 | if (rows % limit == 0) { 75 | return rows / limit; 76 | } else { 77 | return rows / limit + 1; 78 | } 79 | } 80 | 81 | /** 82 | * 获取起始页码 83 | */ 84 | public Integer getStart() { 85 | Integer from = current - 2; 86 | // from < 1 ? 1 : from 87 | return Math.max(from, 1); 88 | } 89 | 90 | /** 91 | * 获取结束页码 92 | */ 93 | public Integer getEnd() { 94 | Integer end = current + 2; 95 | Integer total = getTotal(); 96 | // end > total ? total : end 97 | return Math.min(end, total); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/impl/MessageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.community.service.impl; 2 | 3 | import com.community.entity.Message; 4 | import com.community.mapper.MessageMapper; 5 | import com.community.service.MessageService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.util.HtmlUtils; 10 | 11 | import java.util.List; 12 | 13 | @Slf4j 14 | @Service 15 | public class MessageServiceImpl implements MessageService { 16 | 17 | @Autowired 18 | private MessageMapper messageMapper; 19 | 20 | /** 21 | * 查询当前用户的会话列表,针对每个会话返回一条最新的私信 22 | */ 23 | @Override 24 | public List selectConversations(int userId, int offset, int limit) { 25 | return messageMapper.selectConversations(userId, offset, limit); 26 | } 27 | 28 | /** 29 | * 查询当前用户的会话数量 30 | */ 31 | @Override 32 | public int selectConversationCount(int userId) { 33 | return messageMapper.selectConversationCount(userId); 34 | } 35 | 36 | /** 37 | * 查询某个会话所包含的私信列表 38 | */ 39 | @Override 40 | public List selectLetters(String conversationId, int offset, int limit) { 41 | return messageMapper.selectLetters(conversationId, offset, limit); 42 | } 43 | 44 | /** 45 | * 查询某个会话所包含的私信数量 46 | */ 47 | @Override 48 | public int selectLetterCount(String conversationId) { 49 | return messageMapper.selectLetterCount(conversationId); 50 | } 51 | 52 | /** 53 | * 查询未读私信的数量 54 | * conversationId 作为动态拼接条件, 55 | * 如果不拼接则查询所有的未读私信的数量,否则只查询某个用户的未读私信数量 56 | */ 57 | @Override 58 | public int selectLetterUnreadCount(int userId, String conversationId) { 59 | return messageMapper.selectLetterUnreadCount(userId, conversationId); 60 | } 61 | 62 | /** 63 | * 新增消息 64 | */ 65 | @Override 66 | public int insertMessage(Message message) { 67 | // HTML 转义 68 | message.setContent(HtmlUtils.htmlEscape(message.getContent())); 69 | return messageMapper.insertMessage(message); 70 | } 71 | 72 | /** 73 | * 修改消息的状态,将所有消息变为已读 74 | */ 75 | @Override 76 | public int updateStatus(List ids) { 77 | return messageMapper.updateStatus(ids, 1); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | 4 | import com.community.entity.DiscussPost; 5 | import com.community.entity.Page; 6 | import com.community.entity.User; 7 | import com.community.service.DiscussPostService; 8 | import com.community.service.LikeService; 9 | import com.community.service.UserService; 10 | import com.community.utils.CommonUtil; 11 | import com.community.utils.Constant; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | @Controller 23 | public class HomeController { 24 | 25 | @Autowired 26 | private UserService userService; 27 | 28 | @Autowired 29 | private DiscussPostService discussPostService; 30 | 31 | @Autowired 32 | private LikeService likeService; 33 | 34 | /** 35 | * 社区首页,展示贴子列表 36 | * 方法调用前,SpringMVC 会自动实例化 Model 和 Page,并将 Page 注入 Model, 37 | * 所以,在 thymeleaf 中可以直接访问 Page 对象中的数据,不需要再 model.addAttribute() 方法。 38 | * ------- 39 | * userId 为 0 表示: select * from user 40 | * userId 不为 0 表示:select * from user where user_id = #{userId} 41 | * 该做法是为了在用户个人主页上可以查询到某个用户发布的帖子 42 | */ 43 | @GetMapping({"/index", "/"}) 44 | public String index(Model model, Page page) { 45 | // 设置分页信息 46 | page.setRows(discussPostService.selectDiscussPostRows(0)); 47 | page.setPath("/index"); 48 | 49 | // 查询所有帖子 50 | List discussPosts = discussPostService.selectDiscussPosts(0, page.getOffset(), page.getLimit()); 51 | List> list = new ArrayList<>(); 52 | for (DiscussPost post : discussPosts) { 53 | Map map = new HashMap<>(); 54 | map.put("post", post); 55 | User user = userService.selectById(post.getUserId()); 56 | map.put("user", user); 57 | 58 | // 点赞数量 59 | long likeCount = likeService.selectEntityLikeCount(Constant.ENTITY_TYPE_POST, post.getId()); 60 | map.put("likeCount", likeCount); 61 | 62 | list.add(map); 63 | } 64 | model.addAttribute("discussPosts", list); 65 | return "index"; 66 | 67 | } 68 | 69 | @GetMapping("/error") 70 | public String toErrorPage() { 71 | return "error/500"; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/community/interceptor/LoginTicketInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.community.interceptor; 2 | 3 | import com.community.entity.LoginTicket; 4 | import com.community.entity.User; 5 | import com.community.service.UserService; 6 | import com.community.utils.CookieUtil; 7 | import com.community.utils.UserThreadLocal; 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 | @Component 18 | public class LoginTicketInterceptor implements HandlerInterceptor { 19 | 20 | @Autowired 21 | private UserService userService; 22 | 23 | @Autowired 24 | private UserThreadLocal userThreadLocal; 25 | 26 | /** 27 | * 在 controller 之前执行 28 | */ 29 | @Override 30 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 31 | // 从 cookie 中获取凭证 32 | String ticket = CookieUtil.getValue(request, "ticket"); 33 | // 判断 cookie 中是否有数据 34 | if (ticket != null) { 35 | // 查询凭证 36 | LoginTicket loginTicket = userService.selectByTicket(ticket); 37 | // 检查凭证是否有效 38 | if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { 39 | // 根据凭证查询用户 40 | User user = userService.selectById(loginTicket.getUserId()); 41 | // 存入 ThreadLocal 中 42 | userThreadLocal.setUsers(user); 43 | } 44 | } 45 | // 证明用户已登录,予以放行 46 | return true; 47 | } 48 | 49 | /** 50 | * 在 controller 之后,模板引擎之前执行 51 | */ 52 | @Override 53 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 54 | // 获取登录的用户,存到 modelAndView 里,在模板引擎中使用 55 | User user = userThreadLocal.getUser(); 56 | if (user != null && modelAndView != null) { 57 | modelAndView.addObject("loginUser", user); 58 | } 59 | } 60 | 61 | /** 62 | * 在模板引擎之后执行 63 | */ 64 | @Override 65 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 66 | // 清理登录用户凭证 67 | userThreadLocal.clear(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/impl/DiscussPostServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.community.service.impl; 2 | 3 | 4 | import com.community.entity.DiscussPost; 5 | import com.community.mapper.DiscussPostMapper; 6 | import com.community.service.DiscussPostService; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.web.util.HtmlUtils; 11 | 12 | import java.util.List; 13 | 14 | @Slf4j 15 | @Service 16 | public class DiscussPostServiceImpl implements DiscussPostService { 17 | 18 | @Autowired 19 | private DiscussPostMapper discussPostMapper; 20 | 21 | /** 22 | * 查询用户帖子列表 23 | * 24 | * @param userId 用户 id 25 | * @param offset 每页起始行行号 26 | * @param limit 一页显示多少条数据 27 | * @return List 28 | */ 29 | @Override 30 | public List selectDiscussPosts(int userId, int offset, int limit) { 31 | return discussPostMapper.selectDiscussPosts(userId, offset, limit); 32 | } 33 | 34 | /** 35 | * 查询帖子的行数 36 | * 37 | * @param userId 用户 id 38 | * @return 帖子行数 39 | */ 40 | @Override 41 | public int selectDiscussPostRows(int userId) { 42 | return discussPostMapper.selectDiscussPostRows(userId); 43 | } 44 | 45 | /** 46 | * 发布帖子 47 | */ 48 | @Override 49 | public int insertDiscussPost(DiscussPost discussPost) { 50 | if (discussPost == null) { 51 | throw new IllegalArgumentException("参数不能为空!"); 52 | } 53 | // 转义 HTML 标签 54 | discussPost.setTitle(HtmlUtils.htmlEscape(discussPost.getTitle())); 55 | discussPost.setContent(HtmlUtils.htmlEscape(discussPost.getContent())); 56 | 57 | return discussPostMapper.insertDiscussPost(discussPost); 58 | } 59 | 60 | /** 61 | * 查询帖子详情 62 | */ 63 | @Override 64 | public DiscussPost selectDiscussPostById(int id) { 65 | return discussPostMapper.selectDiscussPostById(id); 66 | } 67 | 68 | /** 69 | * 更新帖子的评论数量 70 | * 71 | * @param id 帖子 id 72 | * @param commentCount 评论数量 73 | */ 74 | @Override 75 | public int updateCommentCount(int id, Long commentCount) { 76 | return discussPostMapper.updateCommentCount(id, commentCount); 77 | } 78 | 79 | /** 80 | * 查询某用户帖子数量 81 | * 82 | * @param userId 用户 id 83 | */ 84 | @Override 85 | public int selectCountByUserId(int userId) { 86 | return discussPostMapper.selectCountByUserId(userId); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /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, serif; 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('#') 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 200px; 65 | } 66 | 67 | .main .container { 68 | background: #fff; 69 | padding: 20px; 70 | } 71 | 72 | i { 73 | font-style: normal; 74 | } 75 | 76 | u { 77 | text-decoration: none; 78 | } 79 | 80 | b { 81 | font-weight: normal; 82 | } 83 | 84 | a { 85 | color: #000; 86 | } 87 | 88 | a:hover { 89 | text-decoration: none; 90 | } 91 | 92 | .font-size-12 { 93 | font-size: 12px; 94 | } 95 | 96 | .font-size-14 { 97 | font-size: 14px; 98 | } 99 | 100 | .font-size-16 { 101 | font-size: 16px; 102 | } 103 | 104 | .font-size-18 { 105 | font-size: 18px; 106 | } 107 | 108 | .font-size-20 { 109 | font-size: 20px; 110 | } 111 | 112 | .font-size-22 { 113 | font-size: 20px; 114 | } 115 | 116 | .font-size-24 { 117 | font-size: 20px; 118 | } 119 | 120 | .hidden { 121 | display: none; 122 | } 123 | 124 | .rt-0 { 125 | right: 0; 126 | top: 0; 127 | } 128 | 129 | .square { 130 | display: inline-block; 131 | width: 7px; 132 | height: 7px; 133 | background: #ff6547; 134 | margin-bottom: 2px; 135 | margin-right: 3px; 136 | } 137 | 138 | .bg-gray { 139 | background: #eff0f2; 140 | } 141 | 142 | .user-header { 143 | width: 50px; 144 | height: 50px; 145 | } 146 | 147 | em { 148 | font-style: normal; 149 | color: red; 150 | } 151 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/my-post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 个人主页 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 32 | 返回个人主页> 33 |
34 | 35 |
36 |
发布的帖子()
37 | 49 | 50 | 51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/resources/mapper/MessageMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | id 7 | , from_id, to_id, conversation_id, content, status, create_time 8 | 9 | 10 | 11 | from_id 12 | , to_id, conversation_id, content, status, create_time 13 | 14 | 15 | 29 | 30 | 42 | 43 | 53 | 54 | 61 | 62 | 71 | 72 | 73 | INSERT INTO message() 74 | VALUES(#{fromId}, #{toId}, #{conversationId}, #{content}, #{status}, #{createTime}) 75 | 76 | 77 | 78 | UPDATE message 79 | SET status = #{status} 80 | WHERE id IN 81 | 82 | #{id} 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/my-comment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 个人主页 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 32 | 返回个人主页> 33 |
34 | 35 |
36 |
评论的帖子()
37 |
    38 |
  • 39 |
    40 | 42 |
    43 |
    44 |
    45 | 评论于 46 |
    47 |
  • 48 |
49 | 50 | 51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/impl/LikeServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.community.service.impl; 2 | 3 | import com.community.service.LikeService; 4 | import com.community.utils.RedisKeyUtil; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.dao.DataAccessException; 8 | import org.springframework.data.redis.core.RedisOperations; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.data.redis.core.SessionCallback; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Slf4j 14 | @Service 15 | public class LikeServiceImpl implements LikeService { 16 | 17 | @Autowired 18 | private RedisTemplate redisTemplate; 19 | 20 | /** 21 | * 点赞 22 | */ 23 | @Override 24 | public void like(int userId, int entityType, int entityId, int entityUserId) { 25 | // 加入事务 26 | redisTemplate.execute(new SessionCallback() { 27 | @Override 28 | public Object execute(RedisOperations operations) throws DataAccessException { 29 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 30 | String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId); 31 | 32 | // 查询操作要在事务开启之前执行,否则在事务中不会立即执行。 33 | Boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId); 34 | // 开启事务 35 | operations.multi(); 36 | if (isMember) { 37 | operations.opsForSet().remove(entityLikeKey, userId); 38 | operations.opsForValue().decrement(userLikeKey); 39 | } else { 40 | operations.opsForSet().add(entityLikeKey, userId); 41 | operations.opsForValue().increment(userLikeKey); 42 | } 43 | // 提交事务 44 | return operations.exec(); 45 | } 46 | }); 47 | } 48 | 49 | /** 50 | * 查询某实体点赞的数量 51 | */ 52 | @Override 53 | public long selectEntityLikeCount(int entityType, int entityId) { 54 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 55 | return redisTemplate.opsForSet().size(entityLikeKey); 56 | } 57 | 58 | /** 59 | * 查询某用户对某实体点赞的状态 60 | * 返回的类型不用 boolean 的原因: boolean 只有两种状态:已点赞和未点赞,将来如果要开发“点踩”功能,两种状态表达不出来 61 | */ 62 | @Override 63 | public int selectEntityLikeStatus(int userId, int entityType, int entityId) { 64 | String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); 65 | return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0; 66 | } 67 | 68 | /** 69 | * 查询某个用户获得的赞 70 | */ 71 | @Override 72 | public int selectUserLikeCount(int userId) { 73 | String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); 74 | Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey); 75 | return count == null ? 0 : count.intValue(); 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 个人主页 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 32 |
33 | 34 |
35 | 用户头像 37 |
38 |
39 | 40 | 41 | 46 |
47 |
48 | 注册于 50 |
51 |
52 | 关注了 54 | 关注者 56 | 获得了 个赞 57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/followee.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 关注 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 | 33 | 返回个人主页> 34 |
35 | 36 | 37 |
    38 |
  • 39 | 40 | 用户头像 42 | 43 |
    44 |
    45 | 46 | 关注于 48 |
    49 |
    50 | 51 | 56 |
    57 |
    58 |
  • 59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/follower.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 关注 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 | 34 | 返回个人主页> 35 |
36 | 37 | 38 |
    39 |
  • 40 | 41 | 用户头像 43 | 44 |
    45 |
    46 | 47 | 关注于 48 | 49 | 50 |
    51 |
    52 | 53 | 58 |
    59 |
    60 |
  • 61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/impl/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.community.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; 5 | import com.community.entity.Comment; 6 | import com.community.mapper.CommentMapper; 7 | import com.community.mapper.DiscussPostMapper; 8 | import com.community.service.CommentService; 9 | import com.community.utils.Constant; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Isolation; 14 | import org.springframework.transaction.annotation.Transactional; 15 | import org.springframework.web.util.HtmlUtils; 16 | 17 | import java.util.List; 18 | import java.util.Objects; 19 | 20 | @Slf4j 21 | @Service 22 | public class CommentServiceImpl implements CommentService { 23 | 24 | @Autowired 25 | private CommentMapper commentMapper; 26 | 27 | @Autowired 28 | private DiscussPostMapper discussPostMapper; 29 | 30 | /** 31 | * 根据实体查询评论 32 | * 33 | * @param entityType 实体类型 0-帖子 1-评论 34 | * @param entityId 实体 id 35 | * @param offset 每页起始行行号 36 | * @param limit 一页显示多少条数据 37 | */ 38 | @Override 39 | public List selectCommentByEntity(int entityType, int entityId, int offset, int limit) { 40 | return commentMapper.selectCommentByEntity(entityType, entityId, offset, limit); 41 | } 42 | 43 | /** 44 | * 根据实体查询评论数 45 | * 46 | * @param entityType 实体类型 0-帖子 1-评论 47 | * @param entityId 实体 id 48 | */ 49 | @Override 50 | public Long selectCount(Integer entityType, Integer entityId) { 51 | LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(Comment.class) 52 | .eq(Comment::getEntityType, entityType) 53 | .eq(Comment::getEntityId, entityId) 54 | .eq(Comment::getStatus, 0); 55 | return commentMapper.selectCount(wrapper); 56 | } 57 | 58 | /** 59 | * 增加评论 60 | * 在 comment 表里新增评论,然后在 discuss_post 表里更新评论数量,需要事务来管理,新增评论成功必须更新评论数量,要么全部执行,要么全部不执行 61 | * isolation = Isolation.READ_COMMITTED:事务的隔离级别。读取已提交 62 | * propagation = Propagation.REQUIRED:事务的传播。支持当前事务,如果当前没有事务,就新建一个事务 63 | */ 64 | @Override 65 | @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED) 66 | public Boolean insertComment(Comment comment) { 67 | if (Objects.isNull(comment)) { 68 | throw new IllegalArgumentException("参数不能为空!"); 69 | } 70 | 71 | // HTML 字符转义 72 | comment.setContent(HtmlUtils.htmlEscape(comment.getContent())); 73 | 74 | // 新增评论 75 | int row = commentMapper.insert(comment); 76 | if (row < 1) { 77 | throw new IllegalArgumentException("新增评论失败"); 78 | } 79 | 80 | // 更新帖子评论数量 81 | if (comment.getEntityType() == Constant.ENTITY_TYPE_POST) { 82 | Long count = selectCount(Constant.ENTITY_TYPE_POST, comment.getEntityId()); 83 | discussPostMapper.updateCommentCount(comment.getEntityId(), count); 84 | } 85 | 86 | return Boolean.TRUE; 87 | } 88 | 89 | /** 90 | * 根据用户 id 查询评论数量 91 | */ 92 | @Override 93 | public int selectCountByUserId(int userId) { 94 | return commentMapper.selectCountByUserId(userId); 95 | } 96 | 97 | /** 98 | * 根据用户 id 查询评论 99 | */ 100 | @Override 101 | public List selectCommentByUserId(int userId, int offset, int limit) { 102 | return commentMapper.selectCommentByUserId(userId, offset, limit); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/FollowController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | import com.community.annotation.LoginRequired; 4 | import com.community.entity.Page; 5 | import com.community.entity.User; 6 | import com.community.service.FollowService; 7 | import com.community.service.UserService; 8 | import com.community.utils.Constant; 9 | import com.community.utils.UserThreadLocal; 10 | import com.community.vo.ResultVo; 11 | import org.springframework.beans.factory.annotation.Autowired; 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 java.util.List; 20 | import java.util.Map; 21 | 22 | @Controller 23 | public class FollowController { 24 | 25 | @Autowired 26 | private FollowService followService; 27 | 28 | @Autowired 29 | private UserThreadLocal userThreadLocal; 30 | 31 | @Autowired 32 | private UserService userService; 33 | 34 | /** 35 | * 取消关注 36 | */ 37 | @PostMapping("/follow") 38 | @ResponseBody 39 | @LoginRequired 40 | public String follow(int entityType, int entityId) { 41 | User curUser = userThreadLocal.getUser(); 42 | followService.follow(curUser.getId(), entityType, entityId); 43 | return ResultVo.getJsonString(0, "已关注"); 44 | } 45 | 46 | /** 47 | * 取消关注 48 | */ 49 | @PostMapping("/unfollow") 50 | @ResponseBody 51 | @LoginRequired 52 | public String unfollow(int entityType, int entityId) { 53 | User curUser = userThreadLocal.getUser(); 54 | followService.unfollow(curUser.getId(), entityType, entityId); 55 | return ResultVo.getJsonString(0, "已取消关注"); 56 | } 57 | 58 | 59 | @GetMapping("/followees/{userId}") 60 | public String getFollowees(@PathVariable int userId, Page page, Model model) { 61 | User user = userService.selectById(userId); 62 | if (user == null) { 63 | throw new RuntimeException("该用户不存在!"); 64 | } 65 | model.addAttribute("user", user); 66 | 67 | // 分页条件 68 | page.setLimit(5); 69 | page.setPath("/followees/" + userId); 70 | page.setRows((int) followService.selectFolloweeCount(userId, Constant.ENTITY_TYPE_USER)); 71 | 72 | List> followeeList = followService.selectFolloweeList(userId, page.getOffset(), page.getLimit()); 73 | if (followeeList != null) { 74 | for (Map map : followeeList) { 75 | User u = (User) map.get("user"); 76 | map.put("hasFollowed", hasFollowed(u.getId())); 77 | } 78 | } 79 | model.addAttribute("users", followeeList); 80 | return "site/followee"; 81 | } 82 | 83 | @GetMapping("/followers/{userId}") 84 | public String getFollowers(@PathVariable int userId, Page page, Model model) { 85 | User user = userService.selectById(userId); 86 | if (user == null) { 87 | throw new RuntimeException("该用户不存在!"); 88 | } 89 | model.addAttribute("user", user); 90 | 91 | // 分页条件 92 | page.setLimit(5); 93 | page.setPath("/followers/" + userId); 94 | page.setRows((int) followService.selectFollowerCount(Constant.ENTITY_TYPE_USER, userId)); 95 | 96 | List> followerList = followService.selectFollowerList(userId, page.getOffset(), page.getLimit()); 97 | if (followerList != null) { 98 | for (Map map : followerList) { 99 | User u = (User) map.get("user"); 100 | map.put("hasFollowed", hasFollowed(u.getId())); 101 | } 102 | } 103 | model.addAttribute("users", followerList); 104 | return "site/follower"; 105 | } 106 | 107 | private boolean hasFollowed(int userId) { 108 | if (userThreadLocal.getUser() == null) { 109 | return false; 110 | } 111 | return followService.hasFollowed(userThreadLocal.getUser().getId(), Constant.ENTITY_TYPE_USER, userId); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /community.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 80022 7 | Source Host : localhost:3306 8 | Source Schema : community 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80022 12 | File Encoding : 65001 13 | 14 | Date: 23/04/2021 09:59:32 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comment 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comment`; 24 | CREATE TABLE `comment` ( 25 | `id` int(0) NOT NULL AUTO_INCREMENT, 26 | `user_id` int(0) NULL DEFAULT NULL, 27 | `entity_type` int(0) NULL DEFAULT NULL, 28 | `entity_id` int(0) NULL DEFAULT NULL, 29 | `target_id` int(0) NULL DEFAULT NULL, 30 | `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, 31 | `status` int(0) NULL DEFAULT NULL, 32 | `create_time` timestamp(0) NULL DEFAULT NULL, 33 | PRIMARY KEY (`id`) USING BTREE, 34 | INDEX `index_user_id`(`user_id`) USING BTREE, 35 | INDEX `index_entity_id`(`entity_id`) USING BTREE 36 | ) ENGINE = InnoDB AUTO_INCREMENT = 266 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 37 | 38 | -- ---------------------------- 39 | -- Table structure for discuss_post 40 | -- ---------------------------- 41 | DROP TABLE IF EXISTS `discuss_post`; 42 | CREATE TABLE `discuss_post` ( 43 | `id` int(0) NOT NULL AUTO_INCREMENT, 44 | `user_id` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id', 45 | `title` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题', 46 | `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '内容', 47 | `type` int(0) NULL DEFAULT NULL COMMENT '0-普通; 1-置顶;', 48 | `status` int(0) NULL DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;', 49 | `create_time` timestamp(0) NULL DEFAULT NULL COMMENT '创建时间', 50 | `comment_count` int(0) NULL DEFAULT NULL COMMENT '评论数', 51 | `score` double NULL DEFAULT NULL, 52 | PRIMARY KEY (`id`) USING BTREE, 53 | INDEX `index_user_id`(`user_id`) USING BTREE 54 | ) ENGINE = InnoDB AUTO_INCREMENT = 297 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 55 | 56 | -- ---------------------------- 57 | -- Table structure for message 58 | -- ---------------------------- 59 | DROP TABLE IF EXISTS `message`; 60 | CREATE TABLE `message` ( 61 | `id` int(0) NOT NULL AUTO_INCREMENT, 62 | `from_id` int(0) NULL DEFAULT NULL COMMENT '消息的发送用户', 63 | `to_id` int(0) NULL DEFAULT NULL COMMENT '消息的接收用户', 64 | `conversation_id` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '会话 id', 65 | `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '消息内容', 66 | `status` int(0) NULL DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;', 67 | `create_time` timestamp(0) NULL DEFAULT NULL COMMENT '创建时间', 68 | PRIMARY KEY (`id`) USING BTREE, 69 | INDEX `index_from_id`(`from_id`) USING BTREE, 70 | INDEX `index_to_id`(`to_id`) USING BTREE, 71 | INDEX `index_conversation_id`(`conversation_id`) USING BTREE 72 | ) ENGINE = InnoDB AUTO_INCREMENT = 371 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 73 | 74 | -- ---------------------------- 75 | -- Table structure for user 76 | -- ---------------------------- 77 | DROP TABLE IF EXISTS `user`; 78 | CREATE TABLE `user` ( 79 | `id` int(0) NOT NULL AUTO_INCREMENT, 80 | `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 81 | `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 82 | `salt` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 83 | `email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 84 | `type` int(0) NULL DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;', 85 | `status` int(0) NULL DEFAULT NULL COMMENT '0-未激活; 1-已激活;', 86 | `activation_code` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 87 | `header_url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, 88 | `create_time` timestamp(0) NULL DEFAULT NULL, 89 | PRIMARY KEY (`id`) USING BTREE, 90 | INDEX `index_username`(`username`(20)) USING BTREE, 91 | INDEX `index_email`(`email`(20)) USING BTREE 92 | ) ENGINE = InnoDB AUTO_INCREMENT = 166 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 93 | 94 | SET FOREIGN_KEY_CHECKS = 1; 95 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 注册 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |

注  册

21 |
22 |
23 | 24 |
25 | 30 |
31 |
32 |
33 |
34 | 35 |
36 | 41 |
42 |
43 |
44 |
45 | 46 |
47 | 51 |
52 | 两次输入的密码不一致! 53 |
54 |
55 |
56 |
57 | 58 |
59 | 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 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.6.RELEASE 9 | 10 | 11 | 12 | jar 13 | com.community 14 | community 15 | 0.0.1-SNAPSHOT 16 | community 17 | Community for Spring Boot 18 | 19 | 20 | 1.8 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | 34 | 35 | 36 | 37 | mysql 38 | mysql-connector-java 39 | 40 | 41 | 42 | 43 | com.baomidou 44 | mybatis-plus-boot-starter 45 | 3.5.1 46 | 47 | 48 | 49 | 50 | org.projectlombok 51 | lombok 52 | 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-thymeleaf 58 | 2.2.6.RELEASE 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-mail 65 | 2.2.6.RELEASE 66 | 67 | 68 | 69 | 70 | org.apache.commons 71 | commons-lang3 72 | 3.11 73 | 74 | 75 | 76 | 77 | com.github.penggle 78 | kaptcha 79 | 2.3.2 80 | 81 | 82 | 83 | 84 | com.alibaba 85 | fastjson 86 | 1.2.75 87 | 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-starter-data-redis 93 | 2.2.6.RELEASE 94 | 95 | 96 | 97 | 98 | com.aliyun.oss 99 | aliyun-sdk-oss 100 | 3.1.0 101 | 102 | 103 | 104 | 105 | joda-time 106 | joda-time 107 | 2.10.1 108 | 109 | 110 | 111 | 112 | app 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-maven-plugin 117 | 118 | 119 | 120 | org.projectlombok 121 | lombok 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 登录 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |

登  录

21 |
22 |
23 | 24 |
25 | 31 |
32 |
33 |
34 |
35 | 36 |
37 | 43 |
44 | 密码长度不能小于8位! 45 |
46 |
47 |
48 |
49 | 50 |
51 | 55 |
56 | 验证码不正确! 57 |
58 |
59 |
60 | 62 |
63 |
64 |
65 |
66 |
67 | 69 | 70 | 忘记密码? 71 |
72 |
73 |
74 |
75 |
76 | 77 |
78 |
79 |
80 |
81 |
82 | 83 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/setting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 账号设置 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 | 21 |
上传头像
22 |
23 |
24 | 25 |
26 |
27 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 |
修改密码
44 |
45 |
46 | 47 |
48 | 53 |
54 |
55 |
56 |
57 | 58 |
59 | 64 |
65 |
66 |
67 |
68 | 69 |
70 | 77 |
78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 |
92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/impl/FollowServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.community.service.impl; 2 | 3 | import com.community.entity.User; 4 | import com.community.mapper.UserMapper; 5 | import com.community.service.FollowService; 6 | import com.community.utils.Constant; 7 | import com.community.utils.RedisKeyUtil; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.dao.DataAccessException; 11 | import org.springframework.data.redis.core.RedisOperations; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.data.redis.core.SessionCallback; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Date; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | @Slf4j 24 | @Service 25 | public class FollowServiceImpl implements FollowService { 26 | 27 | @Autowired 28 | private UserMapper userMapper; 29 | 30 | @Autowired 31 | private RedisTemplate redisTemplate; 32 | 33 | /** 34 | * 关注 35 | */ 36 | @Override 37 | public void follow(int userId, int entityType, int entityId) { 38 | redisTemplate.execute(new SessionCallback() { 39 | @Override 40 | public Object execute(RedisOperations operations) throws DataAccessException { 41 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 42 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 43 | // 开启事务 44 | operations.multi(); 45 | 46 | operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis()); 47 | operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis()); 48 | 49 | 50 | return operations.exec(); 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * 取消关注 57 | */ 58 | @Override 59 | public void unfollow(int userId, int entityType, int entityId) { 60 | redisTemplate.execute(new SessionCallback() { 61 | @Override 62 | public Object execute(RedisOperations operations) throws DataAccessException { 63 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 64 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 65 | // 开启事务 66 | operations.multi(); 67 | 68 | operations.opsForZSet().remove(followeeKey, entityId); 69 | operations.opsForZSet().remove(followerKey, userId); 70 | 71 | // 执行事务 72 | return operations.exec(); 73 | } 74 | }); 75 | } 76 | 77 | /** 78 | * 查询某用户关注实体的数量 79 | */ 80 | @Override 81 | public long selectFolloweeCount(int userId, int entityType) { 82 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 83 | return redisTemplate.opsForZSet().zCard(followeeKey); 84 | } 85 | 86 | /** 87 | * 查询实体的粉丝的数量 88 | */ 89 | @Override 90 | public long selectFollowerCount(int entityType, int entityId) { 91 | String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); 92 | return redisTemplate.opsForZSet().zCard(followerKey); 93 | } 94 | 95 | /** 96 | * 查询当前用户是否已关注该实体 97 | */ 98 | @Override 99 | public boolean hasFollowed(int userId, int entityType, int entityId) { 100 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); 101 | return redisTemplate.opsForZSet().score(followeeKey, entityId) != null; 102 | } 103 | 104 | /** 105 | * 查询某用户关注的用户 106 | */ 107 | @Override 108 | public List> selectFolloweeList(int userId, int offset, int limit) { 109 | String followeeKey = RedisKeyUtil.getFolloweeKey(userId, Constant.ENTITY_TYPE_USER); 110 | Set targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1); 111 | 112 | if (targetIds == null) { 113 | return null; 114 | } 115 | List> list = new ArrayList<>(); 116 | for (Integer targetId : targetIds) { 117 | Map map = new HashMap<>(); 118 | User user = userMapper.selectById(targetId); 119 | map.put("user", user); 120 | Double score = redisTemplate.opsForZSet().score(followeeKey, targetId); 121 | map.put("followTime", new Date(score.longValue())); 122 | list.add(map); 123 | } 124 | return list; 125 | } 126 | 127 | /** 128 | * 查询某用户的粉丝 129 | */ 130 | @Override 131 | public List> selectFollowerList(int userId, int offset, int limit) { 132 | String followerKey = RedisKeyUtil.getFollowerKey(Constant.ENTITY_TYPE_USER, userId); 133 | Set targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1); 134 | 135 | if (targetIds == null) { 136 | return null; 137 | } 138 | 139 | List> list = new ArrayList<>(); 140 | for (Integer targetId : targetIds) { 141 | Map map = new HashMap<>(); 142 | User user = userMapper.selectById(targetId); 143 | map.put("user", user); 144 | Double score = redisTemplate.opsForZSet().score(followerKey, targetId); 145 | map.put("followTime", new Date(score.longValue())); 146 | list.add(map); 147 | } 148 | return list; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/resources/static/js/bs-custom-file-input.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * bsCustomFileInput v1.3.4 (https://github.com/Johann-S/bs-custom-file-input) 3 | * Copyright 2018 - 2020 Johann-S 4 | * Licensed under MIT (https://github.com/Johann-S/bs-custom-file-input/blob/master/LICENSE) 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global = global || self, global.bsCustomFileInput = factory()); 10 | }(this, (function () { 'use strict'; 11 | 12 | var Selector = { 13 | CUSTOMFILE: '.custom-file input[type="file"]', 14 | CUSTOMFILELABEL: '.custom-file-label', 15 | FORM: 'form', 16 | INPUT: 'input' 17 | }; 18 | 19 | var textNodeType = 3; 20 | 21 | var getDefaultText = function getDefaultText(input) { 22 | var defaultText = ''; 23 | var label = input.parentNode.querySelector(Selector.CUSTOMFILELABEL); 24 | 25 | if (label) { 26 | defaultText = label.textContent; 27 | } 28 | 29 | return defaultText; 30 | }; 31 | 32 | var findFirstChildNode = function findFirstChildNode(element) { 33 | if (element.childNodes.length > 0) { 34 | var childNodes = [].slice.call(element.childNodes); 35 | 36 | for (var i = 0; i < childNodes.length; i++) { 37 | var node = childNodes[i]; 38 | 39 | if (node.nodeType !== textNodeType) { 40 | return node; 41 | } 42 | } 43 | } 44 | 45 | return element; 46 | }; 47 | 48 | var restoreDefaultText = function restoreDefaultText(input) { 49 | var defaultText = input.bsCustomFileInput.defaultText; 50 | var label = input.parentNode.querySelector(Selector.CUSTOMFILELABEL); 51 | 52 | if (label) { 53 | var element = findFirstChildNode(label); 54 | element.textContent = defaultText; 55 | } 56 | }; 57 | 58 | var fileApi = !!window.File; 59 | var FAKE_PATH = 'fakepath'; 60 | var FAKE_PATH_SEPARATOR = '\\'; 61 | 62 | var getSelectedFiles = function getSelectedFiles(input) { 63 | if (input.hasAttribute('multiple') && fileApi) { 64 | return [].slice.call(input.files).map(function (file) { 65 | return file.name; 66 | }).join(', '); 67 | } 68 | 69 | if (input.value.indexOf(FAKE_PATH) !== -1) { 70 | var splittedValue = input.value.split(FAKE_PATH_SEPARATOR); 71 | return splittedValue[splittedValue.length - 1]; 72 | } 73 | 74 | return input.value; 75 | }; 76 | 77 | function handleInputChange() { 78 | var label = this.parentNode.querySelector(Selector.CUSTOMFILELABEL); 79 | 80 | if (label) { 81 | var element = findFirstChildNode(label); 82 | var inputValue = getSelectedFiles(this); 83 | 84 | if (inputValue.length) { 85 | element.textContent = inputValue; 86 | } else { 87 | restoreDefaultText(this); 88 | } 89 | } 90 | } 91 | 92 | function handleFormReset() { 93 | var customFileList = [].slice.call(this.querySelectorAll(Selector.INPUT)).filter(function (input) { 94 | return !!input.bsCustomFileInput; 95 | }); 96 | 97 | for (var i = 0, len = customFileList.length; i < len; i++) { 98 | restoreDefaultText(customFileList[i]); 99 | } 100 | } 101 | 102 | var customProperty = 'bsCustomFileInput'; 103 | var Event = { 104 | FORMRESET: 'reset', 105 | INPUTCHANGE: 'change' 106 | }; 107 | var bsCustomFileInput = { 108 | init: function init(inputSelector, formSelector) { 109 | if (inputSelector === void 0) { 110 | inputSelector = Selector.CUSTOMFILE; 111 | } 112 | 113 | if (formSelector === void 0) { 114 | formSelector = Selector.FORM; 115 | } 116 | 117 | var customFileInputList = [].slice.call(document.querySelectorAll(inputSelector)); 118 | var formList = [].slice.call(document.querySelectorAll(formSelector)); 119 | 120 | for (var i = 0, len = customFileInputList.length; i < len; i++) { 121 | var input = customFileInputList[i]; 122 | Object.defineProperty(input, customProperty, { 123 | value: { 124 | defaultText: getDefaultText(input) 125 | }, 126 | writable: true 127 | }); 128 | handleInputChange.call(input); 129 | input.addEventListener(Event.INPUTCHANGE, handleInputChange); 130 | } 131 | 132 | for (var _i = 0, _len = formList.length; _i < _len; _i++) { 133 | formList[_i].addEventListener(Event.FORMRESET, handleFormReset); 134 | 135 | Object.defineProperty(formList[_i], customProperty, { 136 | value: true, 137 | writable: true 138 | }); 139 | } 140 | }, 141 | destroy: function destroy() { 142 | var formList = [].slice.call(document.querySelectorAll(Selector.FORM)).filter(function (form) { 143 | return !!form.bsCustomFileInput; 144 | }); 145 | var customFileInputList = [].slice.call(document.querySelectorAll(Selector.INPUT)).filter(function (input) { 146 | return !!input.bsCustomFileInput; 147 | }); 148 | 149 | for (var i = 0, len = customFileInputList.length; i < len; i++) { 150 | var input = customFileInputList[i]; 151 | restoreDefaultText(input); 152 | input[customProperty] = undefined; 153 | input.removeEventListener(Event.INPUTCHANGE, handleInputChange); 154 | } 155 | 156 | for (var _i2 = 0, _len2 = formList.length; _i2 < _len2; _i2++) { 157 | formList[_i2].removeEventListener(Event.FORMRESET, handleFormReset); 158 | 159 | formList[_i2][customProperty] = undefined; 160 | } 161 | } 162 | }; 163 | 164 | return bsCustomFileInput; 165 | 166 | }))); 167 | //# sourceMappingURL=bs-custom-file-input.js.map 168 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/letter-detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 私信详情 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 |
来自 的私信
23 |
24 |
25 | 27 | 30 |
31 |
32 | 33 | 63 | 64 | 77 | 78 | 79 |
    80 |
  • 81 | 82 | 用户头像 84 | 85 | 95 |
  • 96 |
97 | 98 | 99 |
100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/MessageController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | import com.community.annotation.LoginRequired; 4 | import com.community.entity.Message; 5 | import com.community.entity.Page; 6 | import com.community.entity.User; 7 | import com.community.service.MessageService; 8 | import com.community.service.UserService; 9 | import com.community.utils.UserThreadLocal; 10 | import com.community.vo.ResultVo; 11 | import org.springframework.beans.factory.annotation.Autowired; 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 java.util.*; 20 | 21 | @Controller 22 | public class MessageController { 23 | 24 | @Autowired 25 | private MessageService messageService; 26 | 27 | @Autowired 28 | private UserService userService; 29 | 30 | @Autowired 31 | private UserThreadLocal userThreadLocal; 32 | 33 | 34 | /** 35 | * 私信列表 36 | */ 37 | @LoginRequired 38 | @GetMapping("/letter/list") 39 | public String getLetterList(Model model, Page page) { 40 | User curUser = userThreadLocal.getUser(); 41 | // 分页信息 42 | page.setLimit(5); 43 | page.setPath("/letter/list"); 44 | page.setRows(messageService.selectConversationCount(curUser.getId())); 45 | 46 | // 会话列表 47 | List conversationList = messageService.selectConversations(curUser.getId(), page.getOffset(), page.getLimit()); 48 | 49 | List> conversations = new ArrayList<>(); 50 | if (conversationList != null) { 51 | for (Message message : conversationList) { 52 | Map map = new HashMap<>(); 53 | map.put("conversation", message); 54 | map.put("letterCount", messageService.selectLetterCount(message.getConversationId())); 55 | map.put("unreadCount", messageService.selectLetterUnreadCount(curUser.getId(), message.getConversationId())); 56 | int targetId = curUser.getId() == message.getFromId() ? message.getToId() : message.getFromId(); 57 | map.put("target", userService.selectById(targetId)); 58 | 59 | conversations.add(map); 60 | } 61 | } 62 | model.addAttribute("conversations", conversations); 63 | 64 | // 未读消息数量 65 | int letterUnreadCount = messageService.selectLetterUnreadCount(curUser.getId(), null); 66 | model.addAttribute("letterUnreadCount", letterUnreadCount); 67 | return "site/letter"; 68 | } 69 | 70 | 71 | @LoginRequired 72 | @GetMapping("/letter/detail/{conversationId}") 73 | public String getLetterDetail(@PathVariable String conversationId, Page page, Model model) { 74 | // 分页信息 75 | page.setLimit(5); 76 | page.setPath("/letter/detail/" + conversationId); 77 | page.setRows(messageService.selectLetterCount(conversationId)); 78 | 79 | // 私信列表 80 | List letterList = messageService.selectLetters(conversationId, page.getOffset(), page.getLimit()); 81 | List> letters = new ArrayList<>(); 82 | if (letterList != null) { 83 | for (Message letter : letterList) { 84 | Map map = new HashMap<>(); 85 | map.put("letter", letter); 86 | map.put("fromUser", userService.selectById(letter.getFromId())); 87 | letters.add(map); 88 | } 89 | } 90 | model.addAttribute("letters", letters); 91 | 92 | // 私信目标 93 | User target = getLetterTarget(conversationId); 94 | model.addAttribute("target", target); 95 | 96 | // 设置消息为已读 97 | List letterIds = getLetterIds(letterList); 98 | if (!letterIds.isEmpty()) { 99 | messageService.updateStatus(letterIds); 100 | } 101 | 102 | return "site/letter-detail"; 103 | } 104 | 105 | private User getLetterTarget(String conversationId) { 106 | String[] ids = conversationId.split("_"); 107 | int id0 = Integer.parseInt(ids[0]); 108 | int id1 = Integer.parseInt(ids[1]); 109 | if (userThreadLocal.getUser().getId() == id0) { 110 | return userService.selectById(id1); 111 | } else { 112 | return userService.selectById(id0); 113 | } 114 | } 115 | 116 | private List getLetterIds(List letterList) { 117 | List ids = new ArrayList<>(); 118 | if (letterList != null) { 119 | for (Message message : letterList) { 120 | if (userThreadLocal.getUser().getId().equals(message.getToId()) && message.getStatus() == 0) { 121 | ids.add(message.getId()); 122 | } 123 | } 124 | } 125 | return ids; 126 | } 127 | 128 | @PostMapping("/letter/send") 129 | @ResponseBody 130 | @LoginRequired 131 | public String sendLetter(String toName, String content) { 132 | User target = userService.selectByUsername(toName); 133 | if (target == null) { 134 | return ResultVo.getJsonString(-1, "用户目标不存在!"); 135 | } 136 | 137 | Message message = new Message(); 138 | message.setFromId(userThreadLocal.getUser().getId()); 139 | message.setToId(target.getId()); 140 | if (message.getFromId() < message.getToId()) { 141 | message.setConversationId(message.getFromId() + "_" + message.getToId()); 142 | } else { 143 | message.setConversationId(message.getToId() + "_" + message.getFromId()); 144 | } 145 | message.setContent(content); 146 | // 设置私信默认未读状态 147 | message.setStatus(0); 148 | message.setCreateTime(new Date()); 149 | messageService.insertMessage(message); 150 | return ResultVo.getJsonString(0); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/resources/templates/site/letter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 私信列表 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 33 | 36 |
37 | 38 | 67 | 68 | 81 | 82 | 83 |
    84 |
  • 85 | 87 | 88 | 用户头像 90 | 91 |
    92 |
    93 | 94 | 96 |
    97 |
    98 | 100 | 104 |
    105 |
    106 |
  • 107 |
108 | 109 | 110 |
111 |
112 | 113 | 114 |
115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | 4 | import com.community.annotation.LoginRequired; 5 | import com.community.entity.User; 6 | import com.community.service.UserService; 7 | import com.community.utils.CommonUtil; 8 | import com.community.utils.Constant; 9 | import com.community.utils.RedisKeyUtil; 10 | import com.google.code.kaptcha.Producer; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.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.imageio.ImageIO; 23 | import javax.servlet.ServletOutputStream; 24 | import javax.servlet.http.Cookie; 25 | import javax.servlet.http.HttpServletResponse; 26 | import java.awt.image.BufferedImage; 27 | import java.io.IOException; 28 | import java.util.Map; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | @Slf4j 32 | @Controller 33 | public class LoginController { 34 | 35 | @Autowired 36 | private UserService userService; 37 | 38 | @Autowired 39 | private Producer kaptchaProducer; 40 | 41 | @Autowired 42 | private RedisTemplate redisTemplate; 43 | 44 | 45 | /** 46 | * 跳转到用户登录页面 47 | */ 48 | @GetMapping("/login") 49 | public String toLogin() { 50 | return "site/login"; 51 | } 52 | 53 | /** 54 | * 跳转到用户注册界面 55 | */ 56 | @GetMapping("/register") 57 | public String toRegister() { 58 | return "site/register"; 59 | } 60 | 61 | /** 62 | * 用户注册 63 | */ 64 | @PostMapping("/register") 65 | public String register(Model model, User user) { 66 | Map map = userService.register(user); 67 | // map 为空则注册成功 68 | if (map == null || map.isEmpty()) { 69 | model.addAttribute("msg", "注册成功,请到邮箱激活该账号!"); 70 | model.addAttribute("target", "/index"); 71 | return "site/operate-result"; 72 | } else { 73 | model.addAttribute("UsernameMessage", map.get("UsernameMessage")); 74 | model.addAttribute("PasswordMessage", map.get("PasswordMessage")); 75 | model.addAttribute("EmailMessage", map.get("EmailMessage")); 76 | return "site/register"; 77 | } 78 | } 79 | 80 | /** 81 | * 邮箱激活账号 82 | */ 83 | @GetMapping("/activation/{userId}/{code}") 84 | public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) { 85 | // 根据用户 id 查询该用户的状态 86 | int result = userService.activation(userId, code); 87 | if (result == Constant.ACTIVATION_SUCCESS) { 88 | model.addAttribute("msg", "激活成功,你的账号可以正常使用!"); 89 | model.addAttribute("target", "/login"); 90 | } else if (result == Constant.ACTIVATION_REPEAT) { 91 | model.addAttribute("msg", "无效操作,该账号已激活过了!"); 92 | model.addAttribute("target", "/index"); 93 | } else { 94 | model.addAttribute("msg", "激活失败,该账号激活码无效!"); 95 | model.addAttribute("target", "/index"); 96 | } 97 | return "site/operate-result"; 98 | } 99 | 100 | /** 101 | * 获取验证码 102 | */ 103 | @GetMapping("/kaptcha") 104 | public void getKaptcha(HttpServletResponse response) { 105 | // 生成验证码 106 | String text = kaptchaProducer.createText(); 107 | BufferedImage image = kaptchaProducer.createImage(text); 108 | 109 | // 验证码的归属者(随机字符串) 110 | String kaptchaOwner = CommonUtil.generateUUID(); 111 | Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner); 112 | cookie.setMaxAge(60); 113 | cookie.setPath("/"); 114 | response.addCookie(cookie); 115 | // 将验证码存入 redis 116 | String kaptchaKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); 117 | redisTemplate.opsForValue().set(kaptchaKey, text, 60, TimeUnit.SECONDS); 118 | 119 | // 将图片输出给浏览器 120 | response.setContentType("image/png"); 121 | try { 122 | // 该流不用关闭,Spring 会帮我们关 123 | ServletOutputStream outputStream = response.getOutputStream(); 124 | ImageIO.write(image, "png", outputStream); 125 | } catch (IOException e) { 126 | e.printStackTrace(); 127 | } 128 | } 129 | 130 | /** 131 | * 用户登录 132 | */ 133 | @PostMapping("/login") 134 | public String login(String username, String password, String code, boolean rememberMe, 135 | Model model, HttpServletResponse response, @CookieValue("kaptchaOwner") String kaptchaOwner) { 136 | // 判断验证码 137 | String kaptcha = null; 138 | if (StringUtils.isNoneBlank(kaptchaOwner)) { 139 | // 从 redis 取验证码 140 | String kaptchaKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); 141 | kaptcha = (String) redisTemplate.opsForValue().get(kaptchaKey); 142 | } 143 | 144 | if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) { 145 | model.addAttribute("CodeMessage", "验证码错误!"); 146 | return "site/login"; 147 | } 148 | 149 | // 检查账号和密码 150 | int expired = rememberMe ? Constant.REMEMBER_EXPIRED_SECONDS : Constant.DEFAULT_EXPIRED_SECONDS; 151 | Map result = userService.login(username, password, expired); 152 | if (result.containsKey("ticket")) { 153 | Cookie cookie = new Cookie("ticket", result.get("ticket").toString()); 154 | cookie.setPath("/"); 155 | cookie.setMaxAge(expired); 156 | response.addCookie(cookie); 157 | return "redirect:/index"; 158 | } else { 159 | model.addAttribute("UsernameMessage", result.get("UsernameMessage")); 160 | model.addAttribute("PasswordMessage", result.get("PasswordMessage")); 161 | return "site/login"; 162 | } 163 | } 164 | 165 | /** 166 | * 用户退出 167 | */ 168 | @LoginRequired 169 | @GetMapping("/logout") 170 | public String logout(@CookieValue("ticket") String ticket) { 171 | userService.logout(ticket); 172 | return "redirect:/login"; 173 | } 174 | 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/DiscussPostController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | import com.community.annotation.LoginRequired; 4 | import com.community.entity.Comment; 5 | import com.community.entity.DiscussPost; 6 | import com.community.entity.Page; 7 | import com.community.entity.User; 8 | import com.community.service.CommentService; 9 | import com.community.service.DiscussPostService; 10 | import com.community.service.LikeService; 11 | import com.community.service.UserService; 12 | import com.community.utils.Constant; 13 | import com.community.utils.UserThreadLocal; 14 | import com.community.vo.ResultVo; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.ui.Model; 18 | import org.springframework.web.bind.annotation.*; 19 | 20 | import java.util.*; 21 | 22 | @Controller 23 | @RequestMapping("/post") 24 | public class DiscussPostController { 25 | 26 | @Autowired 27 | private DiscussPostService discussPostService; 28 | 29 | @Autowired 30 | private UserThreadLocal userThreadLocal; 31 | 32 | @Autowired 33 | private UserService userService; 34 | 35 | @Autowired 36 | private CommentService commentService; 37 | 38 | @Autowired 39 | private LikeService likeService; 40 | 41 | /** 42 | * 发布帖子 43 | * 44 | * @param title 标题 45 | * @param content 正文 46 | */ 47 | @PostMapping("/add") 48 | @ResponseBody 49 | @LoginRequired 50 | public String addDiscussPost(String title, String content) { 51 | // 判断用户是否登录 52 | User loginUser = userThreadLocal.getUser(); 53 | if (loginUser == null) { 54 | return ResultVo.getJsonString(-1, "请登录后再操作!"); 55 | } 56 | 57 | // 发布帖子 58 | DiscussPost post = new DiscussPost(); 59 | post.setUserId(loginUser.getId()); 60 | post.setTitle(title); 61 | post.setStatus(0); 62 | post.setType(0); 63 | post.setScore(0.0); 64 | post.setCommentCount(0); 65 | post.setContent(content); 66 | post.setCreateTime(new Date()); 67 | discussPostService.insertDiscussPost(post); 68 | return ResultVo.getJsonString(0, "发布成功!"); 69 | } 70 | 71 | /** 72 | * 查询帖子详情 73 | * 帖子的回复分为评论:1.对帖子进行评论(comment) 2.对评论进行评论(reply) 74 | * 首先展示所有的 comment,再针对每个 comment 展示 reply 75 | */ 76 | @GetMapping("/detail/{id}") 77 | public String getDiscussPost(@PathVariable int id, Model model, Page page) { 78 | // 帖子 79 | DiscussPost post = discussPostService.selectDiscussPostById(id); 80 | model.addAttribute("post", post); 81 | 82 | // 用户 83 | User user = userService.selectById(post.getUserId()); 84 | model.addAttribute("user", user); 85 | 86 | // 点赞数量 87 | long likeCount = likeService.selectEntityLikeCount(Constant.ENTITY_TYPE_POST, id); 88 | model.addAttribute("likeCount", likeCount); 89 | 90 | // 点赞状态 91 | int likeStatus = userThreadLocal.getUser() == null ? 0 : likeService.selectEntityLikeStatus(userThreadLocal.getUser().getId(), Constant.ENTITY_TYPE_POST, id); 92 | model.addAttribute("likeStatus", likeStatus); 93 | 94 | // 评论(分页) 95 | page.setLimit(5); 96 | page.setPath("/post/detail/" + id); 97 | page.setRows(post.getCommentCount()); 98 | 99 | // 评论列表 100 | List commentList = commentService.selectCommentByEntity(Constant.ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit()); 101 | 102 | // 评论Vo列表 103 | List> commitVoList = new ArrayList<>(); 104 | if (commentList != null) { 105 | for (Comment comment : commentList) { 106 | // 评论Vo 107 | Map commentVo = new HashMap<>(); 108 | 109 | // 评论 110 | commentVo.put("comment", comment); 111 | 112 | // 用户 113 | commentVo.put("user", userService.selectById(comment.getUserId())); 114 | 115 | // 点赞数量 116 | likeCount = likeService.selectEntityLikeCount(Constant.ENTITY_TYPE_COMMENT, comment.getId()); 117 | commentVo.put("likeCount", likeCount); 118 | 119 | // 点赞状态 120 | likeStatus = userThreadLocal.getUser() == null ? 0 : likeService.selectEntityLikeStatus(userThreadLocal.getUser().getId(), Constant.ENTITY_TYPE_COMMENT, comment.getId()); 121 | commentVo.put("likeStatus", likeStatus); 122 | 123 | // 回复列表(不作分页) 124 | List replyList = commentService.selectCommentByEntity(Constant.ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE); 125 | 126 | // 回复Vo列表 127 | List> replyVoList = new ArrayList<>(); 128 | if (replyList != null) { 129 | for (Comment reply : replyList) { 130 | // 回复Vo 131 | Map replyVo = new HashMap<>(); 132 | 133 | // 回复 134 | replyVo.put("reply", reply); 135 | 136 | // 用户 137 | replyVo.put("user", userService.selectById(reply.getUserId())); 138 | 139 | // 回复的目标 140 | User targetUser = reply.getTargetId() == 0 ? null : userService.selectById(reply.getTargetId()); 141 | replyVo.put("target", targetUser); 142 | 143 | // 点赞数量 144 | likeCount = likeService.selectEntityLikeCount(Constant.ENTITY_TYPE_COMMENT, reply.getId()); 145 | replyVo.put("likeCount", likeCount); 146 | 147 | // 点赞状态 148 | likeStatus = userThreadLocal.getUser() == null ? 0 : likeService.selectEntityLikeStatus(userThreadLocal.getUser().getId(), Constant.ENTITY_TYPE_COMMENT, reply.getId()); 149 | replyVo.put("likeStatus", likeStatus); 150 | 151 | replyVoList.add(replyVo); 152 | } 153 | } 154 | 155 | commentVo.put("replys", replyVoList); 156 | 157 | // 回复数量 158 | Long replyCount = commentService.selectCount(Constant.ENTITY_TYPE_COMMENT, comment.getId()); 159 | commentVo.put("replyCount", replyCount); 160 | 161 | commitVoList.add(commentVo); 162 | } 163 | } 164 | 165 | model.addAttribute("comments", commitVoList); 166 | return "site/discuss-detail"; 167 | } 168 | } 169 | 170 | 171 | -------------------------------------------------------------------------------- /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/notice.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 | 75 |
76 | 77 | 78 | 128 |
129 |
130 | 131 | 132 |
133 |
134 |
135 | 136 |
137 | 138 |
139 | 140 |
141 |
142 |
143 | 166 |
167 |
168 |
169 |
170 | 186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /src/main/java/com/community/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.community.controller; 2 | 3 | import com.aliyun.oss.OSS; 4 | import com.aliyun.oss.OSSClientBuilder; 5 | import com.community.annotation.LoginRequired; 6 | import com.community.entity.Comment; 7 | import com.community.entity.DiscussPost; 8 | import com.community.entity.Page; 9 | import com.community.entity.User; 10 | import com.community.service.CommentService; 11 | import com.community.service.DiscussPostService; 12 | import com.community.service.FollowService; 13 | import com.community.service.LikeService; 14 | import com.community.service.UserService; 15 | import com.community.utils.Constant; 16 | import com.community.utils.UserThreadLocal; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.apache.commons.lang3.StringUtils; 19 | import org.joda.time.DateTime; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.beans.factory.annotation.Value; 22 | import org.springframework.stereotype.Controller; 23 | import org.springframework.ui.Model; 24 | import org.springframework.web.bind.annotation.CookieValue; 25 | import org.springframework.web.bind.annotation.GetMapping; 26 | import org.springframework.web.bind.annotation.PathVariable; 27 | import org.springframework.web.bind.annotation.PostMapping; 28 | import org.springframework.web.bind.annotation.RequestMapping; 29 | import org.springframework.web.multipart.MultipartFile; 30 | 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.util.ArrayList; 34 | import java.util.HashMap; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.UUID; 38 | 39 | @Slf4j 40 | @Controller 41 | @RequestMapping("/user") 42 | public class UserController { 43 | 44 | @Value("${aliyun.oss.file.endpoint}") 45 | private String endPoint; 46 | 47 | @Value("${aliyun.oss.file.keyid}") 48 | private String keyId; 49 | 50 | @Value("${aliyun.oss.file.keysecret}") 51 | private String keySecret; 52 | 53 | @Value("${aliyun.oss.file.bucketname}") 54 | private String bucketName; 55 | 56 | @Autowired 57 | private UserService userService; 58 | 59 | @Autowired 60 | private UserThreadLocal userThreadLocal; 61 | 62 | @Autowired 63 | private LikeService likeService; 64 | 65 | @Autowired 66 | private FollowService followService; 67 | 68 | @Autowired 69 | private DiscussPostService discussPostService; 70 | 71 | @Autowired 72 | private CommentService commentService; 73 | 74 | /** 75 | * 跳转到用户个人页面 76 | */ 77 | @LoginRequired 78 | @GetMapping("/profile/{userId}") 79 | public String toProfile(@PathVariable int userId, Model model) { 80 | User user = userService.selectById(userId); 81 | if (user == null) { 82 | throw new RuntimeException("该用户不存在!"); 83 | } 84 | // 用户 85 | model.addAttribute("user", user); 86 | // 点赞数量 87 | int likeCount = likeService.selectUserLikeCount(userId); 88 | model.addAttribute("likeCount", likeCount); 89 | // 关注数量 90 | long followeeCount = followService.selectFolloweeCount(userId, Constant.ENTITY_TYPE_USER); 91 | model.addAttribute("followeeCount", followeeCount); 92 | // 粉丝数量 93 | long followerCount = followService.selectFollowerCount(Constant.ENTITY_TYPE_USER, userId); 94 | model.addAttribute("followerCount", followerCount); 95 | // 当前用户是否已关注当前页面上的用户 96 | boolean hasFollowed = false; 97 | if (userThreadLocal.getUser() != null) { 98 | hasFollowed = followService.hasFollowed(userThreadLocal.getUser().getId(), Constant.ENTITY_TYPE_USER, userId); 99 | } 100 | model.addAttribute("hasFollowed", hasFollowed); 101 | return "site/profile"; 102 | } 103 | 104 | /** 105 | * 跳转到用户设置页面 106 | */ 107 | @LoginRequired 108 | @GetMapping("/setting") 109 | public String toSetting() { 110 | return "site/setting"; 111 | } 112 | 113 | /** 114 | * 跳转到我的帖子页面 115 | */ 116 | @LoginRequired 117 | @GetMapping("/mypost") 118 | public String toMyPost(Model model, Page page) { 119 | // 获取当前登录用户 120 | User curUser = userThreadLocal.getUser(); 121 | model.addAttribute("user", curUser); 122 | 123 | // 设置分页信息 124 | page.setLimit(5); 125 | page.setRows(discussPostService.selectDiscussPostRows(curUser.getId())); 126 | page.setPath("/user/mypost"); 127 | 128 | 129 | // 查询某用户发布的帖子 130 | List discussPosts = discussPostService.selectDiscussPosts(curUser.getId(), page.getOffset(), page.getLimit()); 131 | List> list = new ArrayList<>(); 132 | if (discussPosts != null) { 133 | for (DiscussPost post : discussPosts) { 134 | Map map = new HashMap<>(); 135 | map.put("post", post); 136 | // 点赞数量 137 | long likeCount = likeService.selectEntityLikeCount(Constant.ENTITY_TYPE_POST, post.getId()); 138 | map.put("likeCount", likeCount); 139 | 140 | list.add(map); 141 | } 142 | } 143 | // 帖子数量 144 | int postCount = discussPostService.selectCountByUserId(curUser.getId()); 145 | model.addAttribute("postCount", postCount); 146 | model.addAttribute("discussPosts", list); 147 | 148 | return "site/my-post"; 149 | } 150 | 151 | /** 152 | * 跳转到我的评论页面 153 | */ 154 | @LoginRequired 155 | @GetMapping("/mycomment") 156 | public String toMyReply(Model model, Page page) { 157 | // 获取当前登录用户 158 | User curUser = userThreadLocal.getUser(); 159 | model.addAttribute("user", curUser); 160 | 161 | // 设置分页信息 162 | page.setLimit(5); 163 | page.setRows(commentService.selectCountByUserId(curUser.getId())); 164 | page.setPath("/user/mycomment"); 165 | 166 | // 获取用户所有评论 (而不是回复,所以在 sql 里加一个条件 entity_type = 1) 167 | List comments = commentService.selectCommentByUserId(curUser.getId(), page.getOffset(), page.getLimit()); 168 | List> list = new ArrayList<>(); 169 | if (comments != null) { 170 | for (Comment comment : comments) { 171 | Map map = new HashMap<>(); 172 | map.put("comment", comment); 173 | 174 | // 根据实体 id 查询对应的帖子标题 175 | String discussPostTitle = discussPostService.selectDiscussPostById(comment.getEntityId()).getTitle(); 176 | map.put("discussPostTitle", discussPostTitle); 177 | 178 | list.add(map); 179 | } 180 | } 181 | 182 | // 回复的数量 183 | int commentCount = commentService.selectCountByUserId(curUser.getId()); 184 | model.addAttribute("commentCount", commentCount); 185 | 186 | model.addAttribute("comments", list); 187 | return "site/my-comment"; 188 | } 189 | 190 | /** 191 | * 更新头像 192 | */ 193 | @LoginRequired 194 | @PostMapping("/upload") 195 | public String uploadAvatar(MultipartFile headerUrl, Model model) { 196 | 197 | if (headerUrl == null) { 198 | model.addAttribute("error", "请选择图片!"); 199 | return "site/setting"; 200 | } 201 | 202 | String filename = headerUrl.getOriginalFilename(); 203 | String suffix = filename.substring(filename.lastIndexOf(".")); 204 | 205 | if (StringUtils.isBlank(suffix)) { 206 | model.addAttribute("error", "文件格式不正确!"); 207 | return "site/setting"; 208 | } 209 | 210 | try { 211 | // 创建 oss 实例 212 | OSS ossClient = new OSSClientBuilder().build(endPoint, keyId, keySecret); 213 | // 上传文件输入流 214 | InputStream inputStream = headerUrl.getInputStream(); 215 | // 获取文件的名称 216 | String fileName = headerUrl.getOriginalFilename(); 217 | // 在文件名称添加随机的唯一的值 218 | String uuid = UUID.randomUUID().toString().replace("-", ""); 219 | fileName = uuid + "_" + fileName; 220 | // 把文件按照日期分类 2020/12/28/xxx.jpg 221 | String datePath = new DateTime().toString("yyyy/MM/dd"); 222 | fileName = datePath + "/" + fileName; 223 | // 调用 oss 方法实现文件上传 224 | // 第一个参数:bucket 名称; 第二个参数:上传到 oss 的路径和名称;第三个参数:上传文件的输入流 225 | ossClient.putObject(bucketName, fileName, inputStream); 226 | // 更新当前用户头像的访问路径 227 | User user = userThreadLocal.getUser(); 228 | // http://localhost:8080/user/xx.jpg 229 | String url = "https://" + bucketName + "." + endPoint + "/" + fileName; 230 | userService.updateHeaderUrl(user.getId(), url); 231 | // 关闭 ossClient 232 | ossClient.shutdown(); 233 | return "redirect:/index"; 234 | } catch (IOException e) { 235 | log.error(e.getMessage()); 236 | throw new RuntimeException("文件上传失败,服务器发生异常!"); 237 | } 238 | } 239 | 240 | /** 241 | * 密码修改 242 | */ 243 | @LoginRequired 244 | @PostMapping("/password") 245 | public String changePassword(Model model, String oldPassword, String newPassword, String confirmPassword, @CookieValue String ticket) { 246 | // 当前登录用户 247 | User curUser = userThreadLocal.getUser(); 248 | Map map = userService.changePassword(curUser.getId(), oldPassword, newPassword, confirmPassword); 249 | // map 为空则修改成功 250 | if (map.isEmpty()) { 251 | userService.logout(ticket); 252 | return "redirect:/login"; 253 | } 254 | model.addAttribute("OldPasswordMessage", map.get("OldPasswordMessage")); 255 | model.addAttribute("NewPasswordMessage", map.get("NewPasswordMessage")); 256 | model.addAttribute("ConfirmPasswordMessage", map.get("ConfirmPasswordMessage")); 257 | return "site/setting"; 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/com/community/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.community.service.impl; 2 | 3 | import com.community.entity.LoginTicket; 4 | import com.community.entity.User; 5 | import com.community.mapper.UserMapper; 6 | import com.community.service.UserService; 7 | import com.community.utils.CommonUtil; 8 | import com.community.utils.Constant; 9 | import com.community.utils.MailClient; 10 | import com.community.utils.RedisKeyUtil; 11 | import com.community.utils.UserThreadLocal; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | import org.springframework.stereotype.Service; 18 | import org.thymeleaf.TemplateEngine; 19 | import org.thymeleaf.context.Context; 20 | 21 | import java.util.Date; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | @Slf4j 27 | @Service 28 | public class UserServiceImpl implements UserService { 29 | 30 | @Autowired 31 | private UserMapper userMapper; 32 | 33 | @Autowired 34 | private MailClient mailClient; 35 | 36 | @Autowired 37 | private TemplateEngine templateEngine; 38 | 39 | @Autowired 40 | private UserThreadLocal userThreadLocal; 41 | 42 | @Autowired 43 | private RedisTemplate redisTemplate; 44 | 45 | @Value("${community.path.domain}") 46 | private String domain; 47 | 48 | // 1. 先从缓存中查询 49 | private User getCache(int userId) { 50 | String userKey = RedisKeyUtil.getUserKey(userId); 51 | return (User) redisTemplate.opsForValue().get(userKey); 52 | } 53 | 54 | // 2. 取不到时初始化缓存数据 55 | private User initCache(int userId) { 56 | User user = userMapper.selectById(userId); 57 | String userKey = RedisKeyUtil.getUserKey(userId); 58 | redisTemplate.opsForValue().set(userKey, user, 3600, TimeUnit.SECONDS); 59 | return user; 60 | } 61 | 62 | // 3. 数据变更时清除缓存数据 63 | private void clearCache(int userId) { 64 | String userKey = RedisKeyUtil.getUserKey(userId); 65 | redisTemplate.delete(userKey); 66 | } 67 | 68 | /** 69 | * 根据用户 id 查询用户 70 | */ 71 | @Override 72 | public User selectById(int id) { 73 | User user = getCache(id); 74 | if (user == null) { 75 | user = initCache(id); 76 | } 77 | return user; 78 | } 79 | 80 | /** 81 | * 根据用户名查询用户 82 | */ 83 | @Override 84 | public User selectByUsername(String username) { 85 | return userMapper.selectByUsername(username); 86 | } 87 | 88 | /** 89 | * 根据邮箱查询用户 90 | */ 91 | @Override 92 | public User selectByEmail(String email) { 93 | return userMapper.selectByEmail(email); 94 | } 95 | 96 | /** 97 | * 添加用户 98 | */ 99 | @Override 100 | public int insertUser(User user) { 101 | return userMapper.insertUser(user); 102 | } 103 | 104 | /** 105 | * 修改用户状态 106 | */ 107 | @Override 108 | public int updateStatus(int id, int status) { 109 | return userMapper.updateStatus(id, status); 110 | } 111 | 112 | /** 113 | * 注册用户 114 | */ 115 | @Override 116 | public Map register(User user) { 117 | Map map = new HashMap<>(); 118 | 119 | // 空值判断 120 | if (user == null) { 121 | throw new IllegalArgumentException("参数不能为空!"); 122 | } 123 | 124 | if (StringUtils.isBlank(user.getUsername())) { 125 | map.put("UsernameMessage", "账号不能为空!"); 126 | return map; 127 | } 128 | 129 | if (StringUtils.isBlank(user.getPassword())) { 130 | map.put("PasswordMessage", "密码不能为空!"); 131 | return map; 132 | } 133 | 134 | if (StringUtils.isBlank(user.getEmail())) { 135 | map.put("EmailMessage", "邮箱不能为空!"); 136 | return map; 137 | } 138 | 139 | // 验证账号 140 | User dbUser = userMapper.selectByUsername(user.getUsername()); 141 | if (dbUser != null) { 142 | map.put("UsernameMessage", "该账号已存在!"); 143 | return map; 144 | } 145 | 146 | // 验证邮箱 147 | dbUser = userMapper.selectByEmail(user.getEmail()); 148 | if (dbUser != null) { 149 | map.put("EmailMessage", "该邮箱已被注册!"); 150 | return map; 151 | } 152 | 153 | // 注册用户 154 | user.setSalt(CommonUtil.generateUUID().substring(0, 5)); 155 | user.setPassword(CommonUtil.md5(user.getPassword() + user.getSalt())); 156 | user.setType(0); 157 | user.setStatus(0); 158 | user.setActivationCode(CommonUtil.generateUUID()); 159 | user.setHeaderUrl("https://weizujie.oss-cn-shenzhen.aliyuncs.com/img/avatar.png"); 160 | user.setCreateTime(new Date()); 161 | userMapper.insertUser(user); 162 | 163 | // 发送激活邮件 164 | Context context = new Context(); 165 | context.setVariable("email", user.getEmail()); 166 | // http://localhost:8080/activation/153/ajdaejfsiufhsfbsef 167 | String url = domain + "/activation/" + user.getId() + "/" + user.getActivationCode(); 168 | context.setVariable("url", url); 169 | String content = templateEngine.process("mail/activation", context); 170 | mailClient.sendMail(user.getEmail(), "激活账号", content); 171 | 172 | return map; 173 | } 174 | 175 | /** 176 | * 激活用户 177 | */ 178 | @Override 179 | public int activation(int id, String code) { 180 | User dbUser = userMapper.selectById(id); 181 | if (dbUser.getStatus() == 1) { 182 | return Constant.ACTIVATION_REPEAT; 183 | } else if (dbUser.getActivationCode().equals(code)) { 184 | userMapper.updateStatus(id, 1); 185 | clearCache(id); 186 | return Constant.ACTIVATION_SUCCESS; 187 | } else { 188 | return Constant.ACTIVATION_FAILURE; 189 | } 190 | } 191 | 192 | /** 193 | * 用户登录 194 | * 195 | * @param username 登录账号 196 | * @param password 明文密码。数据库里存的是加密后的密码 197 | * @param expired 凭证过期时间 198 | */ 199 | @Override 200 | public Map login(String username, String password, int expired) { 201 | Map map = new HashMap<>(); 202 | 203 | // 空值判断 204 | if (StringUtils.isBlank(username)) { 205 | map.put("UsernameMessage", "账号不能为空!"); 206 | } 207 | 208 | if (StringUtils.isBlank(password)) { 209 | map.put("PasswordMessage", "密码不能为空!"); 210 | } 211 | 212 | // 账号验证 213 | User dbUser = userMapper.selectByUsername(username); 214 | if (dbUser == null) { 215 | map.put("UsernameMessage", "该账号不存在!"); 216 | return map; 217 | } 218 | if (dbUser.getStatus() == 0) { 219 | map.put("UsernameMessage", "该账号未激活!"); 220 | return map; 221 | } 222 | // 密码验证 223 | password = CommonUtil.md5(password + dbUser.getSalt()); 224 | if (!dbUser.getPassword().equals(password)) { 225 | map.put("PasswordMessage", "密码错误!"); 226 | return map; 227 | } 228 | 229 | // 生成登录凭证 230 | LoginTicket loginTicket = new LoginTicket(); 231 | loginTicket.setUserId(dbUser.getId()); 232 | loginTicket.setTicket(CommonUtil.generateUUID()); 233 | loginTicket.setStatus(0); 234 | loginTicket.setExpired(new Date(System.currentTimeMillis() + expired * 1000L)); 235 | // loginTicketMapper.insertLoginTicket(loginTicket); 236 | // 将登录凭证存入 redis 237 | String ticketKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket()); 238 | redisTemplate.opsForValue().set(ticketKey, loginTicket); 239 | 240 | map.put("ticket", loginTicket.getTicket()); 241 | 242 | return map; 243 | } 244 | 245 | /** 246 | * 用户退出 247 | */ 248 | @Override 249 | public void logout(String ticket) { 250 | // return loginTicketMapper.updateStatus(ticket, 1); 251 | String ticketKey = RedisKeyUtil.getTicketKey(ticket); 252 | LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(ticketKey); 253 | loginTicket.setStatus(1); 254 | redisTemplate.opsForValue().set(ticketKey, loginTicket); 255 | } 256 | 257 | /** 258 | * 根据 ticket 查询用户 259 | */ 260 | @Override 261 | public LoginTicket selectByTicket(String ticket) { 262 | String ticketKey = RedisKeyUtil.getTicketKey(ticket); 263 | return (LoginTicket) redisTemplate.opsForValue().get(ticketKey); 264 | } 265 | 266 | /** 267 | * 更新用户头像 268 | */ 269 | @Override 270 | public int updateHeaderUrl(int id, String headerUrl) { 271 | // return userMapper.updateHeaderUrl(id, headerUrl); 272 | int rows = userMapper.updateHeaderUrl(id, headerUrl); 273 | clearCache(id); 274 | return rows; 275 | } 276 | 277 | /** 278 | * 修改密码 279 | */ 280 | @Override 281 | public Map changePassword(int id, String oldPassword, String newPassword, String confirmPassword) { 282 | Map map = new HashMap<>(); 283 | // 获取当前登录用户 284 | User curUser = userThreadLocal.getUser(); 285 | // 密码判断 286 | if (StringUtils.isBlank(oldPassword)) { 287 | map.put("OldPasswordMessage", "原密码不能为空!"); 288 | return map; 289 | } 290 | if (StringUtils.isBlank(newPassword)) { 291 | map.put("NewPasswordMessage", "新密码不能为空!"); 292 | return map; 293 | } 294 | if (!newPassword.equals(confirmPassword)) { 295 | map.put("ConfirmPasswordMessage", "两次密码输入不一致!"); 296 | return map; 297 | } 298 | // 判断用户输入的原密码(明文)是否正确 299 | String inputPassword = CommonUtil.md5(oldPassword + curUser.getSalt()); 300 | if (!inputPassword.equals(curUser.getPassword())) { 301 | map.put("OldPasswordMessage", "原密码错误!"); 302 | return map; 303 | } 304 | // 修改当前登录用户密码 305 | newPassword = CommonUtil.md5(newPassword + curUser.getSalt()); 306 | userMapper.changePassword(curUser.getId(), newPassword); 307 | return map; 308 | } 309 | } 310 | --------------------------------------------------------------------------------