1. 标签展示
1.1 question.html展示标签
通过","切割成数组,并循环
<hr class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<span class="label label-info question-tag" th:each="tag : ${question.tag.split(',')}">
<span class="glyphicon glyphicon-tags" ></span>
<span class="label label-info" th:text="${tag}"></span>
</span>
</div>
1.2 标签样式question-tag
2. 相关问题展示
2.1 编写sql
QuestionMapper.xml
文件
这样可以模糊查询通过标签,查出有相同标签的文章
<select id="selectRelated" parameterType="life.guohui.community.model.Question" resultMap="BaseResultMap">
SELECT * from question WHERE id != #{id} and tag regexp #{tag}
</select>
2.2 Service层进行封装
把,
替换为|
用于查询
public List<QuestionDTO> selectRelated(QuestionDTO queryDTO) {
if(StringUtils.isBlank(queryDTO.getTag())){
return new ArrayList<>();
}
String[] tags = StringUtils.split(queryDTO.getTag(), ",");
String regexpTag = Arrays.stream(tags).collect(Collectors.joining("|"));
Question question = new Question();
question.setId(queryDTO.getId());
question.setTag(regexpTag);
List<Question> questions = questionMapper.selectRelated(question);
List<QuestionDTO> questionDTOS = questions.stream().map(q -> {
QuestionDTO questionDTO = new QuestionDTO();
BeanUtils.copyProperties(q, questionDTO);
return questionDTO;
}).collect(Collectors.toList());
return questionDTOS;
}
2.3 Controller使用
2.4 页面展示
3. 指定标签展示
3.1 创建TagDTO
-
categoryName
:上层标签 -
tags
:下层标签
3.2 缓存标签类
- 用来存放标签的内容,所有的标签都通过它的
get()
方法获取 -
filterInvalid(String tags)
:用来校验传来的标签中标签库里是否存在
package life.guohui.community.cache;
public class TagCache {
public static List<TagDTO> get(){
ArrayList<TagDTO> tagDTOS = new ArrayList<>();
TagDTO program = new TagDTO();
program.setCategoryName("开发语言");
program.setTags(Arrays.asList("javascript", "php", "css", "html", "html5", "java", "node.js", "python", "c++", "c", "golang", "objective-c", "typescript", "shell", "swift", "c#", "sass", "ruby", "bash", "less", "asp.net", "lua", "scala", "coffeescript", "actionscript", "rust", "erlang", "perl"));
tagDTOS.add(program);
TagDTO framework = new TagDTO();
framework.setCategoryName("平台框架");
framework.setTags(Arrays.asList("laravel", "spring", "express", "django", "flask", "yii", "ruby-on-rails", "tornado", "koa", "struts"));
tagDTOS.add(framework);
TagDTO server = new TagDTO();
server.setCategoryName("服务器");
server.setTags(Arrays.asList("linux", "nginx", "docker", "apache", "ubuntu", "centos", "缓存 tomcat", "负载均衡", "unix", "hadoop", "windows-server"));
tagDTOS.add(server);
TagDTO db = new TagDTO();
db.setCategoryName("数据库");
db.setTags(Arrays.asList("mysql", "redis", "mongodb", "sql", "oracle", "nosql memcached", "sqlserver", "postgresql", "sqlite"));
tagDTOS.add(db);
TagDTO tool = new TagDTO();
tool.setCategoryName("开发工具");
tool.setTags(Arrays.asList("git", "github", "visual-studio-code", "vim", "sublime-text", "xcode intellij-idea", "eclipse", "maven", "ide", "svn", "visual-studio", "atom emacs", "textmate", "hg"));
tagDTOS.add(tool);
return tagDTOS;
}
public static String filterInvalid(String tags){
String[] split = StringUtils.split(tags, ",");
List<TagDTO> tagDTOS = get();
List<String> tagList = tagDTOS.stream().flatMap(tag -> tag.getTags().stream()).collect(Collectors.toList());
String invalid = Arrays.stream(split).filter(t -> !tagList.contains(t)).collect(Collectors.joining(","));
return invalid;
}
}
3.3 在PublishController中把标签存到Model中
3.4 在发布问题功能中做校验
String invalid = TagCache.filterInvalid(tag);
if(StringUtils.isNotBlank(invalid)){
model.addAttribute("error", "输入非法标签:"+invalid);
return "publish";
}
3.5 publish.html处理
function selectTag(e){
var value = e.getAttribute("data-tag");
var previous = $("#tag").val();
if(previous.indexOf(value) == -1){
if(previous){
$("#tag").val(previous+','+value);
}else {
$("#tag").val(value);
}
}
}
function showSelectTag() {
$("#select-tag").show();
}
第二个参数为index,利用它的.first
判断是不是第一个元素,来控制选中高亮及标签显示
th:each="selectCategory,selectCategoryStat : ${tags}"
<div id="select-tag" class="publish-tag-tab">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" th:each="selectCategory,selectCategoryStat : ${tags}"
th:class="${selectCategoryStat.first ? 'active':''}">
<a th:href="${'#'+selectCategory.categoryName}" aria-controls="home" role="tab"
data-toggle="tab" th:text="${selectCategory.categoryName}"></a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" th:id="${selectCategory.categoryName}"
th:class="${selectCategoryStat.first ? 'active tab-pane':'tab-pane'}"
th:each="selectCategory: ${tags}">
<span>
<span class="label label-info" th:each="selectTag : ${selectCategory.tags}">
<span class="glyphicon glyphicon-tags" onclick="selectTag(this)"
th:data-tag="${selectTag}"
th:text="${' '+selectTag}">
</span>
</span>
</span>
</div>
</div>
</div>
4. 回复通知
4.1 表的设计
create table notification
(
id bigint auto_increment primary key,
notifier bigint not null,-- 评论当前问题的人的Id
receiver bigint not null,-- 接受者Id,当前问题的发起者Id
outerid bigint not null,-- 当前问题的Id
type int not null,-- 回复了问题(1),还是回复了评论(2)
gmt_create bigint not null,
status int default 0 not null,-- 已读Or未读
notifier_name varchar(100),-- 评论当前问题的人名
outer_title varchar(256)-- 当前问题的标题
);
逆向工程生成对应的实体类和mapper
4.2 创建NotificationTypeEnum
- 区分是回复了问题,还是回复了评论。
-
nameOfType(int type)
方法返回数字对应的信息
package life.guohui.community.enums;
public enum NotificationTypeEnum {
REPLY_QUESTION(1,"回复了问题"),
REPLY_COMMENT(2,"回复了评论")
;
private int type;
private String name;
NotificationTypeEnum(int status, String name) {
this.type = status;
this.name = name;
}
public int getType() {
return type;
}
public String getName() {
return name;
}
public static String nameOfType(int type) {
for (NotificationTypeEnum notificationTypeEnum : NotificationTypeEnum.values()) {
if (notificationTypeEnum.getType() == type) {
return notificationTypeEnum.getName();
}
}
return "";
}
}
4.3 创建NotificationStatusEnum
用来区分回复是已读还是未读
4.4 创建NotificationDTO
用来在页面上展示对应的数据
package life.guohui.community.dto;
@Data
public class NotificationDTO {
private Long id;
private Long gmtCreate;
private Integer status;
private Long notifier;//评论者
private Long outerid;//问题Id
private String notifierName;//评论者的名字
private String outerTitle;//问题提出者的问题标题
private String typeName;
private Integer type;
}
4.5 评论后会去哪?增加回复
a. 点击评论后发送Ajax请求/comment
到CommentController
执行insert
方法
b. 添加一个评论时,我们就要同时创建一个通知
增加一个创建通知的方法,该方法的参数:
- 评论的实体类对象
Comment
- 问题的提出者Id:
receiver
- 当前评论者的名字:
notifierName
,从Controller
层中传过来,从session
中获取user
- 当前问题的标题:
outerTitle
- 回复的类型:
notificationType
,回复的是问题Or评论 - 当前问题的Id:
outerId
private void createNotify(Comment comment, Long receiver, String notifierName, String outerTitle, NotificationTypeEnum notificationType,Long outerId) {
Notification notification = new Notification();
notification.setGmtCreate(System.currentTimeMillis());
notification.setType(notificationType.getType());
notification.setOuterid(outerId);//发布问题的id
notification.setNotifier(comment.getCommentator());//评论人的人的id
notification.setStatus(NotificationStatusEnum.UNREAD.getStatus());
notification.setReceiver(receiver);//当前问题发布人的Id
notification.setNotifierName(notifierName);//评论的人的名字
notification.setOuterTitle(outerTitle);//发布问题的人的问题标题
notificationMapper.insert(notification);
}
c. 在添加评论的方法中使用创建通知方法
@Transactional
public void insert(Comment comment, User commentator) {
if(comment.getParentId() == null || comment.getParentId() == 0){
throw new CustomizeException(CustomizeErrorCode.TARGET_PARAM_NOT_FOUND);
}
if(comment.getType() == null || !CommentTypeEnum.isExist(comment.getType())){
throw new CustomizeException(CustomizeErrorCode.TYPE_PARAM_WRONG);
}
if(comment.getType() == CommentTypeEnum.COMMENT.getType()){
//回复评论
Comment dbComment = commentMapper.selectByPrimaryKey(comment.getParentId());
if(dbComment == null){
throw new CustomizeException(CustomizeErrorCode.COMMENT_NOT_FOUND);
}
Question question = questionMapper.selectByPrimaryKey(dbComment.getParentId());
if(question == null){
throw new CustomizeException(CustomizeErrorCode.QUESTION_NOT_FOUND);
}
commentMapper.insert(comment);
//增加评论数
Comment parentComment = new Comment();
parentComment.setId(comment.getParentId());
parentComment.setCommentCount(1);
commentMapper.incCommentCount(parentComment);
//创建通知
createNotify(comment, dbComment.getCommentator(), commentator.getName(), question.getTitle(), NotificationTypeEnum.REPLY_COMMENT,question.getId());
}else {
//回复问题
Question question = questionMapper.selectByPrimaryKey(comment.getParentId());
if(question == null){
throw new CustomizeException(CustomizeErrorCode.QUESTION_NOT_FOUND);
}
comment.setCommentCount(0);
commentMapper.insert(comment);
question.setCommentCount(1);
questionMapper.incCommentCount(question);
//创建通知
createNotify(comment,question.getCreator(),commentator.getName(),question.getTitle(),NotificationTypeEnum.REPLY_QUESTION,question.getId());
}
}
4.6 展示回复
a. 给分页DTO添加泛型
可以存入不同类型的集合数据
package life.guohui.community.dto;
@Data
public class PaginationDTO<T> {
private List<T> data;
private boolean showPrevious;
private boolean showFirstPage;
private boolean showNext;
private boolean showEndPage;
private Integer page;
private Integer totalPage;
private List<Integer> pages = new ArrayList<>();
public void setPagination(Integer totalPage, Integer page) {
this.totalPage = totalPage;
this.page = page;
pages.add(page);
for(int i = 1;i <=3; i++){
if(page-i>0){
pages.add(0,page-i);
}
if(page+i <= totalPage){
pages.add(page+i);
}
}
//是否展示上一页
if(page == 1){
showPrevious = false;
}else {
showPrevious = true;
}
//是否展示下一页
if(page == totalPage){
showNext = false;
}else{
showNext = true;
}
//是否展示第一页
if(pages.contains(1)){
showFirstPage = false;
}else{
showFirstPage = true;
}
//是否展示最后一页
if(pages.contains(totalPage)){
showEndPage = false;
}else{
showEndPage = true;
}
}
}
b. 创建NotificationService
- 查出当前用户的所有的回复
- 将查出的所有
Notification
对象,赋值给NotificationDTO
对象
public PaginationDTO list(Long id, Integer page, Integer size) {
PaginationDTO<NotificationDTO> paginationDTO = new PaginationDTO();
Integer totalPage;
//拿到总数
NotificationExample notificationExample = new NotificationExample();
notificationExample.createCriteria().andReceiverEqualTo(id);
Integer totalCount = (int)notificationMapper.countByExample(notificationExample);
if(totalCount % size == 0){totalPage = totalCount/size;}else{totalPage = totalCount/size + 1;}
if(page<1){ page = 1; }
if(page>totalPage){ page = totalPage; }
paginationDTO.setPagination(totalPage,page);
Integer offset = size * (page - 1);
NotificationExample example1 = new NotificationExample();
example1.createCriteria().andReceiverEqualTo(id);
List<Notification> notifications = notificationMapper.selectByExampleWithRowbounds(example1, new RowBounds(offset, size));
if(notifications.size() == 0){
return paginationDTO;
}
List<NotificationDTO> notificationDTOS = new ArrayList<>();
for (Notification notification : notifications) {
NotificationDTO notificationDTO = new NotificationDTO();
BeanUtils.copyProperties(notification,notificationDTO);
notificationDTO.setTypeName(NotificationTypeEnum.nameOfType(notification.getType()));
notificationDTOS.add(notificationDTO);
}
paginationDTO.setData(notificationDTOS);
return paginationDTO;
}
c. 要查出当前用户未读评论的数量
public Long unreadCount(Long userId) {
NotificationExample notificationExample = new NotificationExample();
notificationExample.createCriteria().andReceiverEqualTo(userId).andStatusEqualTo(NotificationStatusEnum.UNREAD.getStatus());
return notificationMapper.countByExample(notificationExample);
}
d. 在ProfileController中存入Model
e. 页面的渲染
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" th:if="${section == 'replies'}">
<div class="media" th:each="notification : ${pagination.data}">
<div class="media-body">
<p class="media-heading" >
<span th:text="${notification.notifierName+' '+ notification.typeName+' '}" ></span>
<a th:href="@{'/notification/'+${notification.id}}" th:text="${notification.outerTitle}"></a>
<span class="label label-danger" th:if="${notification.status == 0}">未读</span>
</p>
</div>
</div>
</div>
未读数量
<a href="/profile/questions"
th:class="${section == 'questions'}? 'active list-group-item' : 'list-group-item'">我的问题</a>
<a href="/profile/replies"
th:class="${section == 'replies'}? 'active list-group-item' : 'list-group-item'">
最新回复
<span class="badge" th:text="${unreadCount}"></span>
</a>
4.7 问题的标题是链接
点击后跳转到该问题的question.html
页面
a. Controller中增加read(id,user)
方法
id
为回复对象的Id
,该方法返回这个notificationDTO
对象
b. Service中
- 通过
Id
查出这个回复的信息,修改它的status为已读。 - 把
Notification
的值赋给NotificationDTO
并返回。
public NotificationDTO read(Long id, User user) {
Notification notification = notificationMapper.selectByPrimaryKey(id);
if(notification == null){
throw new CustomizeException(CustomizeErrorCode.NOTIFICATION_NOT_FOUND);
}
//接收者和当前用户是不是同一个人,防止从网址中输入其他接收者的id
if(!Objects.equals(notification.getReceiver(),user.getId())){
throw new CustomizeException(CustomizeErrorCode.READ_NOTIFICATION_FAIL);
}
//更新已读
notification.setStatus(NotificationStatusEnum.READ.getStatus());
notificationMapper.updateByPrimaryKey(notification);
NotificationDTO notificationDTO = new NotificationDTO();
BeanUtils.copyProperties(notification,notificationDTO);
notificationDTO.setTypeName(NotificationTypeEnum.nameOfType(notification.getType()));
return notificationDTO;
}
网友评论