1. 过滤敏感词
1. 创建一个储存要过滤的敏感词的文本文件
- 首先创建一个文本文件储存要过滤的敏感词
在下面的工具类中我们会读取这个文本文件,这里提前给出
@PostConstruct // 这个注解表示当容器实例化这个bean(服务启动的时候)之后在调用构造器之后这个方法会自动的调用
public void init(){
try(
// 读取写有“敏感词”的文件,getClass表示从程序编译之后的target/classes读配置文件,读之后是字节流
// java7语法,在这里的句子最后会自动执行close语句
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
// 字节流 -> 字符流 -> 缓冲流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
// 从文件中一行一行读
while ((keyword = reader.readLine()) != null){
// 添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败: " + e.getMessage());
}
}
2. 开发过滤敏感词的工具类
- 开发过滤敏感词组件
为了方便以后复用,我们把过滤敏感词写成一个工具类SensitiveFilter。
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
// 当检测到敏感词后我们要把敏感词替换成什么符号
private static final String REPLACEMENT = "***";
// 根节点
private TrieNode rootNode = new TrieNode();
@PostConstruct // 这个注解表示当容器实例化这个bean(服务启动的时候)之后在调用构造器之后这个方法会自动的调用
public void init(){
try(
// 读取写有“敏感词”的文件,getClass表示从程序编译之后的target/classes读配置文件,读之后是字节流
// java7语法,在这里的句子最后会自动执行close语句
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
// 字节流 -> 字符流 -> 缓冲流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
// 从文件中一行一行读
while ((keyword = reader.readLine()) != null){
// 添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败: " + e.getMessage());
}
}
// 将一个敏感词添加到前缀树中
private void addKeyword(String keyword){
// 首先默认指向根
TrieNode tempNode = rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if(subNode == null){
// subNode为空,初始化子节点;subNode不为空,直接用就可以了
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
// 指针指向子节点,进入下一轮循环
tempNode = subNode;
}
// 最后要设置结束标识
tempNode.setKeywordEnd(true);
}
/**
* 过滤敏感词
* @param text 待过滤的文本
* @return 过滤后的文本
*/
public String filter(String text){
if(StringUtils.isBlank(text)){
// 待过滤的文本为空,直接返回null
return null;
}
// 指针1,指向树
TrieNode tempNode = rootNode;
// 指针2,指向正在检测的字符串段的首
int begin = 0;
// 指针3,指向正在检测的字符串段的尾
int position = 0;
// 储存过滤后的文本
StringBuilder sb = new StringBuilder();
while (begin < text.length()){
char c = text.charAt(position);
// 跳过符号,比如 “开票”是敏感词 #开#票# 这个字符串中间的 '#' 应该跳过
if(isSymbol(c)){
// 是特殊字符
// 若指针1处于根节点,将此符号计入结果,指针2、3向右走一步
if(tempNode == rootNode){
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
// 符号处理完,进入下一轮循环
continue;
}
// 执行到这里说明字符不是特殊符号
// 检查下级节点
tempNode = tempNode.getSubNode(c);
if(tempNode == null){
// 以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if(tempNode.isKeywordEnd()){
// 发现敏感词,将begin~position字符串替换掉,存 REPLACEMENT (里面是***)
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
// 重新指向根节点
tempNode = rootNode;
} else {
// 检查下一个字符
position++;
}
}
return sb.toString();
}
// 判断是否为特殊符号,是则返回true,不是则返回false
private boolean isSymbol(Character c){
// CharUtils.isAsciiAlphanumeric(c)方法:a、b、1、2···返回true,特殊字符返回false
// 0x2E80 ~ 0x9FFF 是东亚的文字范围,东亚文字范围我们不认为是符号
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
// 前缀树
private class TrieNode{
// 关键词结束标识
private boolean isKeywordEnd = false;
// 当前节点的子节点(key是下级字符、value是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node){
subNodes.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c){
return subNodes.get(c);
}
}
}
上面就是过滤敏感词工具类的全部代码,接下来我们来解释一下开发步骤
开发过滤敏感词组件分为三步:
- 定义前缀树(Tree)
我们将定义前缀树写为SensitiveFilter工具类的内部类
// 前缀树
private class TrieNode{
// 关键词结束标识
private boolean isKeywordEnd = false;
// 当前节点的子节点(key是下级字符、value是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node){
subNodes.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c){
return subNodes.get(c);
}
}
- 根据敏感词,初始化前缀树
将敏感词添加到前缀树中
// 将一个敏感词添加到前缀树中
private void addKeyword(String keyword){
// 首先默认指向根
TrieNode tempNode = rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if(subNode == null){
// subNode为空,初始化子节点;subNode不为空,直接用就可以了
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
// 指针指向子节点,进入下一轮循环
tempNode = subNode;
}
// 最后要设置结束标识
tempNode.setKeywordEnd(true);
}
- 编写过滤敏感词的方法
如何过滤文本中的敏感词:
特殊符号怎么处理:
敏感词前缀树初始化完毕之后,过滤文本中的敏感词的算法应该如下:
定义三个指针:
- 指针1指向Tree树
- 指针2指向待过滤字符串段的头
- 指针3指向待过滤字符串段的尾
/**
* 过滤敏感词
* @param text 待过滤的文本
* @return 过滤后的文本
*/
public String filter(String text){
if(StringUtils.isBlank(text)){
// 待过滤的文本为空,直接返回null
return null;
}
// 指针1,指向树
TrieNode tempNode = rootNode;
// 指针2,指向正在检测的字符串段的首
int begin = 0;
// 指针3,指向正在检测的字符串段的尾
int position = 0;
// 储存过滤后的文本
StringBuilder sb = new StringBuilder();
while (begin < text.length()){
char c = text.charAt(position);
// 跳过符号,比如 “开票”是敏感词 #开#票# 这个字符串中间的 '#' 应该跳过
if(isSymbol(c)){
// 是特殊字符
// 若指针1处于根节点,将此符号计入结果,指针2、3向右走一步
if(tempNode == rootNode){
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
// 符号处理完,进入下一轮循环
continue;
}
// 执行到这里说明字符不是特殊符号
// 检查下级节点
tempNode = tempNode.getSubNode(c);
if(tempNode == null){
// 以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if(tempNode.isKeywordEnd()){
// 发现敏感词,将begin~position字符串替换掉,存 REPLACEMENT (里面是***)
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
// 重新指向根节点
tempNode = rootNode;
} else {
// 检查下一个字符
position++;
}
}
return sb.toString();
}
// 判断是否为特殊符号,是则返回true,不是则返回false
private boolean isSymbol(Character c){
// CharUtils.isAsciiAlphanumeric(c)方法:a、b、1、2···返回true,特殊字符返回false
// 0x2E80 ~ 0x9FFF 是东亚的文字范围,东亚文字范围我们不认为是符号
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
最后:建议在测试类中测试一下
经测试,过滤敏感词的工具类开发完成,这个工具会在接下来的发布帖子的功能中用到。
2. 发布帖子
异步请求:当前网页不刷新,还要访问服务器,服务器会返回一些结果,通过这些结果提炼出来的数据对网页做一个局部的刷新(提示、样式等),实现异步请求的技术是AJAX。
演示一下使用 JQuery 发送异步请求的示例
引一个FastJson的包,用这个包里面的 api 处理json字符串的转换,效率更高一点,性能更好一点。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
首先在 CommunityUtil 工具类中加几个处理json字符串的方法,因为服务器给浏览器返回json字符串,
// 返回JSON字符串
// 参数:编码、提示信息、Map(Map里面封装业务数据)
public static String getJSONString(int code, String msg, Map<String, Object> map){
/*
封装成json对象,然后把json对象转换成字符串就得到了一个json格式的字符串
*/
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if(map != null){
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
// 重载
public static String getJSONString(int code, String msg) {
return getJSONString(code, msg, null);
}
public static String getJSONString(int code){
return getJSONString(code, null, null);
}
写一个html页面 ajax-demo.html,在
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AJAX</title>
</head>
<body>
<p>
<input type="button" value="发送" onclick="send();">
</p>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script>
function send() {
$.post(
"/community/alpha/ajax",
{"name":"张三","age":23},
function (data){
console.log(typeof (data));
console.log(data);
// 将字符串转换成json对象
data = $.parseJSON(data);
console.log(typeof (data));
console.log(data.code);
console.log(data.msg);
}
);
}
</script>
</body>
</html>
引入 JQuery:在html页面body内加上下面这句话
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
测试:
接下来我们来开发发布帖子的功能
- 数据访问层(dao)
增加插入帖子的方法
int insertDiscussPost(DiscussPost discussPost);
/*
声明插入帖子的功能
*/
然后是对应的mapper配置文件
<sql id="insertFields" >
user_id, title, content, type, status, create_time, comment_count, score
</sql>
<insert id="insertDiscussPost" parameterType="com.nowcoder.community.entity.DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
- 业务层(service)
@Service
public class DiscussPostService {
@Autowired
private DiscussPostMapper discussPostMapper;
@Autowired
private SensitiveFilter sensitiveFilter;
public List<DiscussPost> findDiscussPosts(int userId, int offest, int limit){
return discussPostMapper.selectDiscussPosts(userId, offest, limit);
}
public int findDiscussPostRows(int userId){
return discussPostMapper.selectDiscussPostRows(userId);
}
// 插入帖子
public int addDiscussPost(DiscussPost post){
if(post == null){
throw new IllegalArgumentException("参数不能为空!");
}
// 转义HTML标记,对帖子中的标签做一个处理(为了防止帖子中的标签对源码产生不好的影响)
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getContent()));
// 过滤敏感词
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
}
- 视图层(Controller和页面)
DiscussPostController:
@Component
@RequestMapping("/discuss")
public class DiscussPostController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content){
User user = hostHolder.getUser();
if(user == null){
// 如果没有登录,直接返回错误信息
// 403 代表没有权限
return CommunityUtil.getJSONString(403, "你还没有登录!");
}
// 可以执行到这里说明已经登录了
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
// 帖子类型和状态、得分等默认就是0,不用设置
discussPostService.addDiscussPost(post);
// 报错的情况将来统一处理
return CommunityUtil.getJSONString(0, "发布成功!"); // 0 表示是一个正确的状态
}
}
index.html:
index.js:
主页的发布帖子功能里面有js,我们修改一下js
测试
接着在数据库中看一下标签是否被转义以及敏感词是否被过滤
3. 帖子详情
在首页的帖子列表页面上,可以随便选一个帖子,然后点它的标题,我们就可以打开一个显示帖子详细信息的页面,然后把帖子的详细内容显示完整,这就是帖子详情的功能。
- 数据访问层(dao)
dao接口中增加一个查询帖子的功能:
DiscussPost selectDiscussPostById(int id);
/*
根据帖子id查询帖子的详细内容
*/
然后编写它对应的mapper配置文件
<sql id="selectFields" >
id, user_id, title, content, type, status, create_time, comment_count, score
</sql>
<select id="selectDiscussPostById" resultType="com.nowcoder.community.entity.DiscussPost" parameterType="Integer">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
- 业务层(service)
在DiscussPostService中增加一个查询帖子的方法
public DiscussPost findDiscussPostById(int id){
return discussPostMapper.selectDiscussPostById(id);
}
- 表现层(controller和html)
DiscussPostController:
@Autowired
private UserService userService;
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
// 由id查询帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 由于discussPost显示的只有用户的id,我们显示在页面上的肯定是用户的username而不是id,所以我们还需要查一下username
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
return "/site/discuss-detail";
}
index.html:
我们要在首页的每一个帖子上的标题上加一个链接,链接能够访问到上面DiscussPostController中的查询帖子的方法。
discuss-detail.html:
接着我们要处理详情页面数据的展现
测试
4. 事务管理
事务管理常见知识
第一类丢失更新
第二类丢失更新
脏读
不可重复读
幻读
不同的隔离级别可以解决的问题
Spring中的事务管理
Spring中的事务管理有两种方式:
-
声明式事务
-
通过xml配置,声明某方法的事务特征
或
-
通过注解,声明某方法的事务特征
-
-
编程式事务
- 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
两者选一种即可,建议选第一种,因为比较简单,当然如果业务比较复杂,仅仅只是想管理一小部分,使用第二种。
写一个demo演示Spring中的事务管理
1. 声明式事务管理事务
这里我们通过加注解声明某方法的事务特征
@Service
public class AlphaService {
@Autowired
private AlphaDao alphaDao;
@Autowired
private UserMapper userMapper;
@Autowired
private DiscussPostMapper discussPostMapper;
/*
isolation 事务的隔离级别
propagation 事务
的传播机制:
REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.
REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),不存在外部事务就会和REQUIRED一样
*/
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1(){
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId()); // 虽然上面我们没有给user设置id,但是执行过数据库操作之后,数据库给user的id
post.setTitle("Hello");
post.setContent("新人报道!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
// 报错
Integer.valueOf("abc"); // 将 "abc" 这个字符串转换为整数,肯定转不了,报错
return "ok";
}
}
测试:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TransactionTests {
@Autowired
private AlphaService alphaService;
@Test
public void testSave1() {
Object obj = alphaService.save1();
System.out.println(obj);
}
}
2. 编程式事务管理
通过 TransactionTemplate 管理事务
@Service
public class AlphaService {
@Autowired
private AlphaDao alphaDao;
@Autowired
private UserMapper userMapper;
@Autowired
private DiscussPostMapper discussPostMapper;
/*
这个类是Spring自动创建的,我们无须配置,直接注入即可
*/
@Autowired
private TransactionTemplate transactionTemplate;
public Object save2(){
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); // 设置隔离级别
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置传播机制
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
// 新增用户
User user = new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("beta@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
});
}
}
测试:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TransactionTests {
@Autowired
private AlphaService alphaService;
@Test
public void testSave2() {
Object obj = alphaService.save2();
System.out.println(obj);
}
}
5. 显示评论
数据访问层(dao)
- 根据实体查询一页评论数据
- 根据实体查询评论的数量
数据库中的评论表
CREATE TABLE `comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`entity_type` int(11) DEFAULT NULL, # 回复的类型,1代表回复帖子,2代表回复评论
`entity_id` int(11) DEFAULT NULL,
`target_id` int(11) DEFAULT NULL,
`content` text,
`status` int(11) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_user_id` (`user_id`),
KEY `index_entity_id` (`entity_id`)
) ENGINE=InnoDB AUTO_INCREMENT=232 DEFAULT CHARSET=utf8;
/*
entity_type 代表评论的类型:比如 评论帖子、评论别人的评论等
entity_id 代表评论的帖子的 id,比如评论的帖子的 id
target_id 代表指向目标评论的人的 id,比如评论别人的帖子,target_id 表示评论的人的 id
content 代表评论的内容
status 代表评论的状态,0 代表正常,1 代表删除或者不可用
*/
comment表对应的实体类
public class Comment {
private int id;
private int userId;
private int entityType;
private int entityId;
private int targetId;
private String content;
private int status;
private Date createTime;
// 为了以免影响阅读体验,get、set、toString方法没有粘上来,但其实是有的
}
dao接口:
@Mapper
public interface CommentMapper {
/*
分页查询评论
参数:1.评论的类型 2.评论的是哪个评论的id 3.起始页 4.每页限制条数
*/
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
/*
查询评论的总条数
参数:1.评论的类型 2.评论的是哪个评论的id
*/
int selectCountByEntity(int entityType, int entityId);
}
dao接口对应的mapper配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.CommentMapper">
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentsByEntity" resultType="com.nowcoder.community.entity.Comment" >
select <include refid="selectFields"></include>
from comment
where status = 0 <!--status=0表示这个数据是有效的-->
and entity_type = #{entityType}
and entity_id = #{entityId}
order by create_time
limit #{offset}, #{limit}
</select>
<select id="selectCountByEntity" resultType="Integer" >
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
</mapper>
业务层(Service)
- 处理查询评论的业务
- 处理查询评论数量的业务
CommentService:
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper;
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
}
public int findCommentCount(int entityType, int entityId) {
return commentMapper.selectCountByEntity(entityType, entityId);
}
}
表现层(Controller和themeleaf模板)
- 显示帖子详情数据时,同时显示该帖子所有的评论数据
CommunityConstant 常量接口中设置两个常量分别表示评论的类型:
/**
* 回复的实体类型:帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 回复的实体类型:评论
*/
int ENTITY_TYPE_COMMENT = 2;
对 DiscussPostController 中的getDiscussPost方法补充一些代码去在页面上展示具体的评论
@Component
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
@Autowired
private CommentService commentService;
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content){
User user = hostHolder.getUser();
if(user == null){
// 如果没有登录,直接返回错误信息
// 403 代表没有权限
return CommunityUtil.getJSONString(403, "你还没有登录!");
}
// 可以执行到这里说明已经登录了
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
// 帖子类型和状态、得分等默认就是0,不用设置
discussPostService.addDiscussPost(post);
// 报错的情况将来统一处理
return CommunityUtil.getJSONString(0, "发布成功!"); // 0 表示是一个正确的状态
}
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){
// 由id查询帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 由于discussPost显示的只有用户的id,我们显示在页面上的肯定是用户的username而不是id,所以我们还需要查一下username
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
/*
评论:给帖子的评论
回复:给评论的评论,因为不止帖子有评论,评论也可能有评论
*/
page.setLimit(5); // 设置每页显示的条数
page.setPath("/discuss/detail/" + discussPostId); // 设置查询的controller方法路径
page.setRows(post.getCommentCount()); // 一共有多少条评论的数据
// 评论列表,因为评论里面有user_id、target_id 我们还需要查user表查到username
List<Comment> commentList = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit()
);
// 评论VO列表 (VO 意思是在页面上显示的对象 View Object )
List<Map<String, Object>> commentVoList = new ArrayList<>();
if(commentList != null){
for (Comment comment : commentList) {
// 每个评论的VO
Map<String, Object> commentVo = new HashMap<>();
// 评论
commentVo.put("comment", comment);
// 作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 回复列表
List<Comment> replyList = commentService.findCommentsByEntity(
ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE // 这里评论的回复就不分页了
);
// 回复Vo列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
// 评论的每一个回复Vo
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 回复目标,等于0表示只是普通的评论,
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
}
首先让 DiscussPostController 实现常量接口 CommunityConstant
然后是方法的详细内容
接下来我们就需要处理模板了:
先处理一下首页 index.html 帖子的评论数量和分页逻辑复用:
接下来是处理帖子的详细页面 discuss-detail.html :
cvoStat 状态的隐含对象:循环变量名后面加上 Stat
cvoStat.count 循环到了第几次
测试
测试成功