• (尚硅谷)JavaWeb新版教程08-QQZone项目的实现


    1、熟悉 QQZone 业务需求

    1. 用户登录

    2. 登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词,如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表

    3. 查看日志详情:

      • 日志本身的信息(作者头像、昵称、日志标题、日志内容、日志的日期)
      • 回复列表(回复者的头像、昵称、回复内容、回复日期)
      • 主人回复信息
    4. 删除日志

    5. 删除特定回复

    6. 删除特定主人回复

    7. 添加日志、添加回复、添加主人回复

    8. 点击左侧好友链接,进入好友的空间

    注意:

    • 文中的静态页面已经提供好的,如果没有去尚硅谷公众号下载,直接使用其样式和静态页面即可。

    2、数据库设计

    2.1 抽取实体

    用户登录信息(一键快速登录)、用户详情信息(后面自己再修改手机号和邮箱等等详细信息) 、 日志 、 回贴 、 主人回复

    2.2 分析其中的属性

    • 用户登录信息:账号、密码、头像、昵称
    • 用户详情信息:真实姓名、星座、血型、邮箱、手机号…
    • 日志:标题、内容、日期、作者
    • 回复:内容、日期、作者、日志
    • 主人回复:内容、日期、作者、回复

    2.3 分析实体之间的关系

    • 用户登录信息 : 用户详情信息 1:1 PK(一对一关系)
    • 用户 : 日志 1:N(一对N)
    • 日志 : 回复 1:N(一对N)
    • 回复 : 主人回复 1:1 UK(一对一)
    • 用户 : 好友 M : N(多对多,一个人可以有多个好友,一个人也可以成为多个人的好友)
      在这里插入图片描述
      注意(上图其实是 QQ 空间项目的 ER 设计图):
    • 实体用矩形表示;
    • 实体的属性用椭圆形表示;
    • 实体与实体之间的关系用菱形来表示。

    2.4 数据库的范式

    1. 第一范式:列不可再分。比如说收货地址,上海市xxx街道xxx路xxxx小区,每一个所属地都可以分成一列,这样在同一个地区的,比如都是xxx街道的,他们的数据库存储xxx路就可以用一个数字来代替,这样存储数字比存储汉字节省空间。
    2. 第二范式:一张表只表达一层含义(只描述一件事情),确保表中的每一列都和主键相关。
    3. 第三范式:表中的每一列和主键都是直接依赖关系,而不是间接依赖。
      参考链接:数据库-----三大范式–详解

    一般数据库设计中遵循的规则:
    数据库设计的范式和数据库的查询性能很多时候是相悖的,我们需要根据实际的业务情况做一个选择:

    • 查询频次不高的情况下,我们更倾向于提高数据库的设计范式,从而提高存储效率;
    • 查询频次较高的情形,我们更倾向于牺牲数据库的规范度,降低数据库设计的范式,允许特定的冗余,从而提高查询的性能;

    比如这个 QQ空间项目,明明主人回复中的作者,我们可以根据 日志-回复-主人回复 之间的关联关系查到作者信息,为什么我们还要在主人回复中设置作者这一项呢(这样做不满足第三范式)?

    • 如果我们没有添加作者这一列,想在主人回复中查询作者,那么我们需要多表连接查询,查询三次查到作者
    • 如果我们添加作者这一列,那么我们查询的时候进行单表查询就可以了,查询效率更高。
      在这里插入图片描述
      解释:
    • 主键尽量使用没有实际业务意义的自增列,这样以后在场景改变的时候需要合并数据库的时候不会发生主键冲突;
      5个实体有6张表,因为多对多关联会产生中间第三张表 t_friend 表,表示 t_user_basic 这张表和自己产生关联。

    3、根据数据库的表新建 pojo 类(ORM编程思想)

    ORM 编程思想:(object relational mapping),有点万事万物皆对象那种意思

    • 一个数据表对应一个 java 类
    • 表中的一条记录对应 java 类的一个对象
    • 表中的一个字段对应 java 类的一个属性

    注意:

    • java 类中的属性名字尽量和数据库的列名相同,如果确实不相同的话,后面写 sql 语句的时候,记得起列的别名。

    3.1 UserBasic 类:

    public class UserBasic {
        private Integer id ;
        private String loginId ;
        private String nickName ;
        private String pwd ;
        private String headImg ;
    //自定义属性的所属类,代表级联关系
        private UserDetail userDetail ;     //1:1,一个用户对应一个用户详情
        private List<Topic> topicList ;     //1:N,一个用户对应一个日志列表
        private List<UserBasic> friendList ;//M:N,一个用户对应一个朋友列表
    
        public UserBasic(){}
        public UserBasic(Integer id) {this.id = id;}
        public Integer getId() {return id;}
        public void setId(Integer id) {this.id = id;}
        public String getLoginId() {return loginId;}
        public void setLoginId(String loginId) {this.loginId = loginId;}
        public String getNickName() {return nickName;}
        public void setNickName(String nickName) {this.nickName = nickName;}
    	public String getPwd() {return pwd;}
        public void setPwd(String pwd) {this.pwd = pwd;}
        public String getHeadImg() {return headImg;}
        public void setHeadImg(String headImg) {this.headImg = headImg;}
        public UserDetail getUserDetail() {return userDetail;}
        public void setUserDetail(UserDetail userDetail) {this.userDetail = userDetail;}
        public List<Topic> getTopicList() {return topicList;}
        public void setTopicList(List<Topic> topicList) {this.topicList = topicList;}
        public List<UserBasic> getFriendList() {return friendList;}
        public void setFriendList(List<UserBasic> friendList) {this.friendList = friendList;}
    }
    
    • 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
    • 28
    • 29
    • 30

    解释:

    • 构造器先定义一个空参构造器,后面需要哪几个参数的构造器再回来补;
    • 下面每个类我就不放 get、set 方法了,记得构造所有属性的 get、set 方法。

    3.2 UserDetail 类:

    public class UserDetail {
        private Integer id ;
        private String realName ;
        private String tel ;
        private String email ;
        private LocalDateTime birth ;
        private String star ;
    
        public UserDetail(){}
        public Integer getId() {
            return id;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    日期的继承关系:
    父类:java.util.Date 年月日时分秒毫秒
    子类:java.sql.Date 年月日
    子类:java.sql.Time 时分秒

    3.3 Topic 类:

    public class Topic {
        private Integer id ;
        private String title ;
        private String content ;
        private LocalDateTime topicDate ;
        
        private UserBasic author ;          //M:1
        private List<Reply> replyList ;     //1:N
    
        public Topic(){}
        public Topic(Integer id) {
            this.id = id;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    解释:

    • 这里是因为 MySQL 8.0 中默认的 data 类型是 LocalDateTime,所以这里要和老师定义的 Data 类型不一致,否则后面会报错,下面所有涉及到日期类型的都替换成 LocalDateTime

    3.4 Reply 类:

    public class Reply {
        private Integer id ;
        private String content ;
        private LocalDateTime replyDate ;
        
        private UserBasic author ;  //M:1
        private Topic topic ;       //M:1
        private HostReply hostReply ;   //1:1
    
        public Reply() {}
        public Reply(Integer id) {this.id = id;}
        public Reply(String content, LocalDateTime replyDate, UserBasic author, Topic topic) {
            this.content = content;
            this.replyDate = replyDate;
            this.author = author;
            this.topic = topic;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.5 HostReply 类:

    public class HostReply {
        private Integer id ;
        private String content ;
        private LocalDateTime hostReplyDate ;
        
        private UserBasic author ; //M:1
        private Reply reply ;   //1:1
    
        public HostReply(){}
        public HostReply(Integer id) {
            this.id = id;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    4、DAO 层 - service 层 - controller 控制器

    接下来,我们大致要完成的事情是:

    1. 建立 DAO 接口:指定某一类对应的接口应该完成什么功能,然后建立这些接口的实现类;
    2. 建立业务层:每一个基本类的 DAO 实现之后呢,需要考虑实现业务功能,需要建立 service 接口,然后实现这些接口的功能;这里需要创建配置文件,配置 DAO 层和业务层交互的 bean 节点,以及 DAO 层和 service 层之间的依赖关系(以后 spring 不用自己手动配置了)
    3. 建立 controller 控制器:业务层建好之后,controller 负责调用 service 层封装好的各种业务功能实现对同一个类的各种 业务流程控制。

    实际上开发中,我们都是根据想实现的功能需求来一步一步完善 DAO层、service 层、controller 控制器以及配置文件的。

    5、用户登录功能

    首先,我们程序运行之后,首先看到的界面是一个登陆界面,界面如下:
    在这里插入图片描述
    输入用户名和密码之后,需要验证是否登陆成功,也就是要去数据库里面查询是否有对应的账号和密码。

    5.1 登陆功能

    login.html 页面设计成一个表单,被中央控制器 DispatcherServlet 拦截之后,根据配置文件的配置跳转到 UserController 控制器:

    <form th:action="@{/user.do}" method="get">
    
    • 1

    配置文件:

    <bean id="user" class="com.atguigu.qqzone.controller.UserController">
        <property name="userBasicService" ref="userBasicService"/>
        <property name="topicService" ref="topicService"/>
    </bean>
    
    • 1
    • 2
    • 3
    • 4

    UserController 类中的登陆方法:

    private UserBasicService userBasicService ;
    private TopicService topicService ;
    
    public String login(String loginId , String pwd , HttpSession session){
        //1.登录验证
        UserBasic userBasic = userBasicService.login(loginId, pwd);
        if(userBasic!=null){
            //1-1 获取相关的好友信息
            List<UserBasic> friendList = userBasicService.getFriendList(userBasic);
            //1-2 获取相关的日志列表信息(但是,日志只有id,没有其他信息)
            List<Topic> topicList = topicService.getTopicList(userBasic);
    
            userBasic.setFriendList(friendList);
            userBasic.setTopicList(topicList);
    
            //userBasic这个key保存的是登陆者的信息
            //friend这个key保存的是当前进入的是谁的空间,将来点进好友空间这个key要有改动
            session.setAttribute("userBasic",userBasic);
            session.setAttribute("friend",userBasic);
            return "index";
        }else{
            session.setAttribute("login",123);
            return "login";
        }
    }
    
    • 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

    解释:

    • 首先调用 userBasicService 层中的登陆方法进行登陆验证;
    • 登陆成功之后要通过 userBasicService 层获取相关好友列表,以及通过 topicService 层获取日志列表,这些都是要展示在主页上的;
    • 获取到 friendList 和 topicList 之后要调用 userBasic 类的 set 方法设置到 userBasic 对应的属性上去,也就是进行类的关联,其实也就是表的连接;
    • 最后,将获取到的 userBasic 类设置到 session 作用域中,方便之后调用,如果想获取 好友列表和日志列表通过 userBasic 来调用就可以了。

    5.2 获取好友列表

    所有的实现方法均需要提前在接口中定义规范,这里实现类中再重写这些方法,这里省略接口步骤。

    UserBasicServiceImpl 实现:

    public class UserBasicServiceImpl implements UserBasicService {
    
        private UserBasicDAO userBasicDAO = null;
    
        @Override
        public UserBasic login(String loginId, String pwd) {
            UserBasic userBasic = userBasicDAO.getUserBasic(loginId, pwd);
            return userBasic;
        }
    
        @Override
        public List<UserBasic> getFriendList(UserBasic userBasic) {
            List<UserBasic> userBasicList = userBasicDAO.getUserBasicList(userBasic);
            List<UserBasic> friendList = new ArrayList<>(userBasicList.size());
            for (int i = 0; i < userBasicList.size(); i++) {
                UserBasic friend = userBasicList.get(i);
                friend = getUserBasicById(friend.getId());
                friendList.add(friend);
            }
            return friendList;
        }
    
        @Override
        public UserBasic getUserBasicById(Integer id) {
            return userBasicDAO.getUserBasicById(id);
        }
    }
    
    • 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

    配置文件:

    <bean id="userBasicService" class="com.atguigu.qqzone.service.impl.UserBasicServiceImpl">
        <property name="userBasicDAO" ref="userBasicDAO"/>
    </bean>
    
    • 1
    • 2
    • 3

    UserBasicDAOImpl 实现:

    public class UserBasicDAOImpl extends BaseDAO<UserBasic> implements UserBasicDAO {
        @Override
        public UserBasic getUserBasic(String loginId, String pwd) {
            return super.load("select * from t_user_basic where loginId = ? and pwd = ? " , loginId , pwd);
        }
    
        @Override
        public List<UserBasic> getUserBasicList(UserBasic userBasic) {
            String sql = "SELECT fid as 'id' FROM t_friend WHERE uid = ?";
            return super.executeQuery(sql,userBasic.getId());
        }
    
        @Override
        public UserBasic getUserBasicById(Integer id) {
            return load("select * from t_user_basic where id = ? " , id);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    配置文件:

    <bean id="userBasicDAO" class="com.atguigu.qqzone.dao.impl.UserBasicDAOImpl"/>
    
    • 1

    解释:

    • UserBasicService 层调用 DAO 层的 getUserBasic 方法从数据库查到了一组 UserBasic 信息,返回给 UserController 层;
    • 如果查到了 UserBasic 信息,也就是它不为空的话,那么我们需要调用UserBasicService 层的 getFriendList 方法获取好友列表,UserBasicService 层调用的是 DAO 层的 getUserBasicList 方法,注意这里从 t_friend 表中获取到的是一系列 fid 值,这个 fid 值对应的是 UserBasic 中的某 id 值;
      在这里插入图片描述
    • 拿到一系列 id 值之后我们要去 t_user_basic 中找这些 id 值对应的是哪些用户,即 UserBasicService 层遍历每一个 fid 值,调用 DAO 层的 getUserBasicById 方法获取到真正对应的所有好友的 UserBasic 信息。

    5.3 获取日志列表

    TopicServiceImpl 实现:

    public class TopicServiceImpl implements TopicService {
        private TopicDAO topicDAO ;
    
        @Override
        public List<Topic> getTopicList(UserBasic userBasic) {
            return topicDAO.getTopicList(userBasic);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    TopicDAOImpl 实现:

    public class TopicDAOImpl extends BaseDAO<Topic> implements TopicDAO {
        @Override
        public List<Topic> getTopicList(UserBasic userBasic) {
            return super.executeQuery("select * from t_topic where author = ? " , userBasic.getId());
        }
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    配置文件:

    <bean id="topicDAO" class="com.atguigu.qqzone.dao.impl.TopicDAOImpl"/>
    
    <bean id="topicService" class="com.atguigu.qqzone.service.impl.TopicServiceImpl">
        <property name="topicDAO" ref="topicDAO"/>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解释:

    • UserController 控制器 - topicService 层 - topicDAO 层的 getTopicList 方法,层层调用,功能分离。

    5.4 登录功能的错误排查

    1. URL没修改,用的还是 fruitdb,将 ConnUtil 工具类中的 jdbc:mysql://localhost:3306/fruitdb?useUnicode=true&characterEncoding=utf-8&useSSL=false 地址修改为 jdbc:mysql://localhost:3306/qqzonedb?useUnicode=true&characterEncoding=utf-8&useSSL=false

    2. 给 fid 起别名,TopicDAOImpl 实现类中的 getUserBasicList 方法要封装成一个 UserBasic 的 list,这个类里面没有 fid 这个属性,所以要起一个别名;

    3. 并且在 BaseDAO 中的获取别名的方法修改为 rsmd.getColumnLabel(), 而不是 rsmd.getColumnName() (获取列的列名);

    4. Can not set com.atguigu.qqzone.pojo.UserBasic field com.atguigu.qqzone.pojo.Topic.author to java.lang.Integer 错误,这个错误是什么原因呢?

    • 我们之前将从数据库获取到的数据集,获取数据库列名然后将数据集中的某一列设置到这个运行时类的某个属性上,这里报错是我们需要的是一个 UserBasic 类的属性,但是我们获取到的是 Integer 属性的数据,不能把 Integer 属性的数据强制设置上去;

    • 如下图所示,topic 表中最后一列存放的是 author 的 id 值,我们其实获取到的 Integer 值是作者的 id 值,那么我们需要将这个 id 值根据构造器方法封装成一个 UserBasic 类的值赋值上去;
      在这里插入图片描述

    • 获取当前字段的类型名称,判断如果是自定义类型,获取这个自定义类型的 Class 对象,然后获取这个类的带 Integer 类型的构造器(这里因为这个项目只有 Integer 类的某一数据列,其他项目不一定),即需要用反射调用这个自定义类的带一个参数的构造方法,创建出这个自定义类的实例对象,然后将实例对象赋值给这个属性。

    下面是 BaseDAO 中对应的 setValue 方法的修改:

    private void setValue(Object obj, String property, Object propertyValue) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        Class clazz = obj.getClass();
        Field field = clazz.getDeclaredField(property);
        if (field != null) {
            String typeName = field.getType().getName();
            if (isMyType(typeName)) {
                Class typeNameClass = Class.forName(typeName);
                Constructor constructor = typeNameClass.getDeclaredConstructor(Integer.class);
                propertyValue = constructor.newInstance(propertyValue);
            }
            field.setAccessible(true);
            field.set(obj, propertyValue);
        }
    }
    
    private static boolean isNotMyType(String typeName) {
        return "java.lang.Integer".equals(typeName) || 
    	    "java.lang.String".equals(typeName) || 
    	    "java.util.Date".equals(typeName) || 
    	    "java.sql.Date".equals(typeName) || 
    	    "java.time.LocalDateTime".equals(typeName);
    }
    
    private static boolean isMyType(String typeName) {
        return !isNotMyType(typeName);
    }
    
    • 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

    注意:

    • 这里如果你是 MySQL 8.0 版本的方法,之前设置 pojo 类的时候将时间格式都设置成了 LocalDateTime 类,所以这里判断是否不是自定义类中要多加一个类型判断,即 "java.time.LocalDateTime".equals(typeName);

    6、显示主界面

    1. 用户登录之后跳转到 index.html 页面,其中页面左侧显示好友列表;上端显示欢迎词,如果不是自己的空间,显示超链接:返回自己的空间;中间显示日志列表。

    index.html 的各个模块都需要 Thymeleaf 动态渲染之后进行覆盖:

    <div id="div0">
        <div id="div_top"><iframe height="118px" th:src="@{/page.do?operate=page&page=frames/top}" width="100%" frameborder="no"></iframe></div>
        <div id="div_left"><iframe th:src="@{/page.do?operate=page&page=frames/left}" width="100%" frameborder="no"  onload="this.style.height = window.frames[1].document.body.scrollHeight+'px';"></iframe></div>
    <div id="div_main"><iframe th:src="@{/page.do?operate=page&page=frames/main}" scrolling="no" width="100%" frameborder="no"  onload="this.style.height = Math.max(window.frames[1].document.body.scrollHeight,window.frames[2].document.body.scrollHeight)+'px';"></iframe></div>
        <div id="div_bottom">
            <p class="center" >版权所有&reg;,欢迎盗版</p>
        </div>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    解释:

    • 如果在 index.html 页面直接去请求的静态页面资源(.html界面),那么并没有执行 super.processTemplate(),也就是 thymeleaf 没有起作用;
    • 这里所有的模块要想动态显示界面,需要设置访问路径为 th:src="@{/page.do?operate=page&page=frames/left}",目的是执行 super.processTemplate() 方法,让 thymeleaf 生效,数据动态的显示在界面上。
    • 解决方法是添加一个 PageController 控制器,添加page方法。

    这里的 PageController 是通用的,所有的静态页面上的动态数据均需要经过 PageController 的作用:

    public class PageController {
        public String page(String page) {
            return page;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解释:

    • /page.do?operate=page&page=login 通过 servletPath 解析后从 bean 容器中找到 pageController
    • operate=page 找到 pageController 中的 page 方法;
    • 请求的 page=login 代表获取到的 page 参数就是 login 参数在中央控制器反射调用方法时进行注入;
    • pageController 中的 page 方法直接就将 login 这个字符串返回,字符串 "login" 返回给 DispatcherServlet;
    • 由于没有前缀默认执行的 processTemplate() 方法,login 被 thymeleaf 渲染之后实际上找到的是动态的 /login.html 页面。

    配置文件:

    <bean id="page" class="com.atguigu.myssm.myspringmvc.PageController"/>
    
    • 1

    6.1 显示左侧好友列表

    left.html 页面显示,遍历好友列表 session.userBasic.friendList 并且动态渲染到界面上,界面上显示好友名称即 friend.nickName

    <div id="div_friendList" >
        我的好友<br/>
        <ul>
            <li th:if="${#lists.isEmpty(session.userBasic.friendList)}">一个好友也没有</li>
            <li th:unless="${#lists.isEmpty(session.userBasic.friendList)}" th:each="friend : ${session.userBasic.friendList}" th:text="${friend.nickName}">乔峰</li>
        </ul>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    6.2 显示中间日志列表

    main.html 页面,判断 session.friend.topicList 是否为空,如果不为空则遍历将每一项显示到界面上 th:each="topic : ${session.friend.topicList}",在界面上显示日志ID topic.id、日志标题 topic.title、日志日期 topic.topicDate、日志操作(未完成)

    <div id="div_topic_list">
         <div id="div_to_add">
             <p class="right8">发表新日志</p>
         </div>
         <table id="tbl_topic_list">
             <tr>
                 <th>ID</th>
                 <th>标题</th>
                 <th>日期</th>
                 <th>操作</th>
             </tr>
             <tr th:if="${#lists.isEmpty(session.friend.topicList)}">
                 <th colspan="4">暂无日志列表</th>
             </tr>
             <tr th:unless="${#lists.isEmpty(session.friend.topicList)}" th:each="topic : ${session.friend.topicList}">
                 <td th:text="${topic.id}">2</td>
                 <td class="left"><a href="detail.html" th:text="${topic.title}">我乔峰要走,你们谁可阻拦</a></td>
                 <td th:text="${topic.topicDate}">2021-09-01 12:30:55</td>
                 <td>删除</td>
             </tr>
         </table>
     </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    6.3 显示顶端欢迎界面

    1. 首先我们要做一个判断,判断是否是自己的界面,如果是,显示欢迎进入xxx的空间 ${session.friend.nickName},如果不是,我们要右边显示一块超链接是返回自己的空间;
    2. 这里我们如何判断是自己空间还是朋友的空间呢?判断进入的是否是自己的空间的依据是: userBasic 和 friend 这两个 key 中保存的 UserBasic 是否一致,${session.userBasic.id!=session.friend.id}
    <div id="top_title" >欢迎来到QQZone!</div>
    <div id="top_link_div" >
        <span th:text="|欢迎进入${session.friend.nickName}的空间!|">欢迎进入Jim的空间!</span>
        <!--
        判断进入的是否是自己的空间的依据是:
        userBasic和friend这两个key中保存的UserBasic是否一致
        -->
        <span th:if="${session.userBasic.id!=session.friend.id}">
            <a th:href="@{|/user.do?operate=friend&id=${session.userBasic.id}|}" target="_top">返回自己的空间!</a>
        </span>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    6.4 总结

    • 第一次发送请求

      • /page.do?operate=page&page=login 通过 servletPath 解析后从 bean 容器中找到 pageController;
      • operate=page 找到 pageController 中的 page 方法;
      • 请求的 page=login 代表获取到的参数就是 String login 参数在中央控制器反射调用方法时进行注入;
      • pageController 中的 page 方法直接就将 login 这个字符串返回;
      • 由于没有前缀默认执行的 processTemplate() 方法,login 被 thymeleaf 渲染之后实际上找到的是动态的 /login.html 页面。
    • 将 login.html 页面响应给客户端
      客户端看到一个登陆页面,我们点击登陆之后,发送了第二次请求。

    • 第二次请求(登陆验证)

      • /user.do?operate=login,找到配置文件中 id 为 user 的,它对应的类为 UserController 类;
      • 又因为 operate=login,调用其中的 login 方法;
      • login 方法首先是会调用 userBasicService 组件返回一个 userBasic 对象;
      • 再一次调用 userBasicService 组件获取所有的好友列表 friendList,并通过 userBasic 的 set 方法将它设置进这个对象的属性中;
      • 然后调用 topicService 获取所有的日志列表 topicList,同样 set 进 userBasic 中;
      • 然后将 userBasic 保存到 session 作用域;
      • 跳转到 index 页面,其中搭载着 top、left、main 三个 src 路径,响应给客户端。
    • 客户端看到这三个 src 之后给服务器发送三次请求,分别请求这三个页面:
      后面的三次请求的 /page.do?operate=page&page=frames/left 后端的处理的具体过程和第一次发送请求的处理过程一样。

    6.5 左侧好友名称变超链接

    1. 点击 left.html 左侧的好友,跳转到对应好友的空间,显示好友的日志列表;
    2. 首先要在 left.html 中设置一个超链接,这里我们遍历的是 session.userBasic.friendList 这个列表然后将这个信息作为 friend 出现,在遍历的内部可以直接根据 friend 这个参数来调用它的一些属性,比如 friend.id
    <ul>
        <li th:if="${#lists.isEmpty(session.userBasic.friendList)}">一个好友也没有</li>
        <li th:unless="${#lists.isEmpty(session.userBasic.friendList)}" th:each="friend : ${session.userBasic.friendList}">
            <a th:href="@{|/user.do?operate=friend&id=${friend.id}|}" th:text="${friend.nickName}" target="_top">张三</a>
        </li>
    </ul>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 在 UserController 中新建一个 friend 方法,用来获取对应好友 id 的 userBasic 属性为 currFriend 对象,并且获取它的日志列表 set 进 currFriend 中;
    2. 将这个 currFriend 放进 session 作用域 friend 对应的 value 中;

    UserController 类中获取好友的基本信息,以及好友的日志列表方法:

    public String friend(Integer id, HttpSession session){
        //1.根据id获取指定的用户信息
        UserBasic currFriend = userBasicService.getUserBasicById(id);
        List<Topic> topicList = topicService.getTopicList(currFriend);
        currFriend.setTopicList(topicList);
        session.setAttribute("friend",currFriend);
        return "index";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    问题:

    • 但是我们一点击好友,在左侧(left)中显示整个 index 页面,修改 target 显示到整个页面上层,在超链接标签 <a> 里面加 target="_top" 属性,上面返回自己空间的超链接也需要设置这个属性。

    7、日志详情页面

    7.1 获取所有 topic 信息

    1. 已知 topic 的 id,需要根据 topic 的 id 获取特定 topic,首先将显示的 topic.title 这一行设置一个超链接:
    <tr th:unless="${#lists.isEmpty(session.friend.topicList)}" th:each="topic : ${session.friend.topicList}">
       <td th:text="${topic.id}">2</td>
       <td class="left"><a th:href="@{|/topic.do?operate=topicDetail&id=${topic.id}|}" th:text="${topic.title}">我乔峰要走,你们谁可阻拦</a></td>
       <td th:text="${topic.topicDate}">2021-09-01 12:30:55</td>
       <td><input type="button" value="删除"/></td>
    </tr>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 新建一个 TopicController ,实现 topicDetail 方法,并且是根据 id 值来获取 Topic 日志;

    TopicController 控制器的实现:

    public class TopicController {
        private TopicService topicService ;
    
        public String topicDetail(Integer id , HttpSession session){
            Topic topic = topicService.getTopicById(id);
            session.setAttribute("topic",topic);
            return "frames/detail";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意:

    • 这里要 return "frames/detail"; ,保证找渲染完之后找资源的时候去 frames 文件夹下去找 detail.html 资源。
    1. 要在 topicService 中添加一个 getTopicById 方法,并在实现类中实现它,并且保存到 session 作用域中 key 为 topic 的 value 中;

    TopicService 层实现:

    @Override
    public Topic getTopicById(Integer id) {
        Topic topic = topicDAO.getTopic(id);
        return topic ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    TopicDAO 实现:

    @Override
    public Topic getTopic(Integer id) {
        return load("select * from t_topic where id = ? ", id);
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 同时有一个 topic 的 key,需要在配置文件中进行配置,配置一个 topic 的 bean,并且添加它和 topicService 的依赖关系;
    <bean id="topic" class="com.atguigu.qqzone.controller.TopicController">
        <property name="topicService" ref="topicService"/>
    </bean>
    
    • 1
    • 2
    • 3

    7.2 获取这个 topic 关联的所有的回复以及主人回复

    1. 新建一个 ReplyService 层,新建 RelayDAO 层,并且实现这两个中的方法;
    2. topicService 层中需要用到这个 replyService 中的 getReplyListByTopicId 方法来获取所有的 replyList;
    3. 但是如果有的 reply 有 hostReply 的话,我们需要一并获取出来,所以新建 hostReplyService 层和 hostReplyDAO 层,并且实现其中根据 replyId 查询 hostReply 列表的方法。

    TopicController 实现:

    public class TopicController {
        private TopicService topicService ;
    
        public String topicDetail(Integer id , HttpSession session){
            Topic topic = topicService.getTopicById(id);
    
            session.setAttribute("topic",topic);
            return "frames/detail";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    解释:

    • TopicController 控制器只需要调用 topicService 层实现获取日志列表这个功能,具体实现查询其关联的 reply 列表和 hostReply 列表由 service 层来实现;
    • 获取到的 topic 中的 author 只有 id,那么需要在 topicService 的 getTopic 方法中封装,在查询 topic 本身信息时,同时调用 userBasicService 中的获取 userBasic 方法,给 author 属性赋值;
    • 同理,在 reply 类中也有 author,而且这个 author 也是只有 id,那么我们也需要根据 id 查询得到 author,最后设置关联。

    TopicService 实现:

    public class TopicServiceImpl implements TopicService {
    
        private TopicDAO topicDAO ;
        //此处引用的是replyService,而不是replyDAO
        private ReplyService replyService ;
        private UserBasicService userBasicService ;
    
        @Override
        public List<Topic> getTopicList(UserBasic userBasic) {
            return topicDAO.getTopicList(userBasic);
        }
    
        @Override
        public Topic getTopic(Integer id){
            Topic topic = topicDAO.getTopic(id);
            UserBasic author = topic.getAuthor();
            author = userBasicService.getUserBasicById(author.getId());
            topic.setAuthor(author);
            return topic;
        }
    
        @Override
        public Topic getTopicById(Integer id) {
            Topic topic = getTopic(id);
            List<Reply> replyList = replyService.getReplyListByTopicId(topic.getId());
            topic.setReplyList(replyList);
            return topic ;
        }
    }
    
    • 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
    • 28
    • 29

    解释:

    • 将 topic 类其中关联的 replyList 和 author 属性查出来并设置上去,调用 replyService 层查询 replyList 属性,调用 userBasicService 层查询 author 属性;
      在这里插入图片描述
    • 又因为 topic 这个数据库表中存放的只有 author 的 id ,所以我们方法是 getUserBasicById;
      在这里插入图片描述

    ReplyService 层实现:

    public class ReplyServiceImpl implements ReplyService {
        private ReplyDAO replyDAO ;
        //此处引入的是其他POJO对应的Service接口,而不是DAO接口
        //其他POJO对应的业务逻辑是封装在service层的,我需要调用别人的业务逻辑方法,而不要去深入考虑人家内部的细节
        private HostReplyService hostReplyService ;
        private UserBasicService userBasicService ;
    
        @Override
        public List<Reply> getReplyListByTopicId(Integer topicId) {
            List<Reply> replyList = replyDAO.getReplyList(new Topic(topicId));
            for (int i = 0; i < replyList.size(); i++) {
                Reply reply = replyList.get(i);
                //1.将关联的作者设置进去
                UserBasic author = userBasicService.getUserBasicById(reply.getAuthor().getId());
                reply.setAuthor(author);
    
                //2.将关联的HostReply设置进去
                HostReply hostReply = hostReplyService.getHostReplyByReplyId(reply.getId());
                reply.setHostReply(hostReply);
            }
            return replyList ;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    解释:

    • 遍历这个 replyList 中的每一条 reply,调用 userBasicService 层和 hostReplyService 层将其关联的作者信息和主人回复信息获取到并且设置进去,然后这里是根据 topic 查询的 replyList,所以其中的 topic 是本身就有值的,不用我们 set ;
      在这里插入图片描述

    ReplyDAO 层实现:

    public class ReplyDAOImpl extends BaseDAO<Reply> implements ReplyDAO {
        @Override
        public List<Reply> getReplyList(Topic topic) {
            return executeQuery("select * from t_reply where topic = ? " , topic.getId());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    解释:

    • 由于在 t_reply 这个表中关联的 topic 只存储了 topicId 信息,所以我们要根据指定的 topic 先 get 它的 id,再根据这个 topic 将其关联的 reply 列表获取到;
      在这里插入图片描述

    HostReplyService 层实现:

    public class HostReplyServiceImpl implements HostReplyService {
        private HostReplyDAO hostReplyDAO ;
    
        @Override
        public HostReply getHostReplyByReplyId(Integer replyId) {
            return hostReplyDAO.getHostReplyByReplyId(replyId);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    HostReplyDAO 层实现:

    public class HostReplyDAOImpl extends BaseDAO<HostReply> implements HostReplyDAO {
        @Override
        public HostReply getHostReplyByReplyId(Integer replyId) {
            return load("select * from t_host_reply where reply = ? " , replyId);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:

    • 某 service 层尽量调用别的业务封装好的 service 层,而不用别的 DAO 层,不需要关心其他 DAO 层的实现细节;
    • 记得配置配置文件中的 bean 节点和关联信息。
    <bean id="replyDAO" class="com.atguigu.qqzone.dao.impl.ReplyDAOImpl"/>
    <bean id="hostReplyDAO" class="com.atguigu.qqzone.dao.impl.HostReplyDAOImpl"/>
    
    <bean id="topicService" class="com.atguigu.qqzone.service.impl.TopicServiceImpl">
        <property name="topicDAO" ref="topicDAO"/>
        <property name="replyService" ref="replyService"/>
        <property name="userBasicService" ref="userBasicService"/>
    </bean>
    <bean id="replyService" class="com.atguigu.qqzone.service.impl.ReplyServiceImpl">
        <property name="replyDAO" ref="replyDAO"/>
        <property name="hostReplyService" ref="hostReplyService"/>
        <property name="userBasicService" ref="userBasicService"/>
    </bean>
    <bean id="hostReplyService" class="com.atguigu.qqzone.service.impl.HostReplyServiceImpl">
        <property name="hostReplyDAO" ref="hostReplyDAO"/>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    7.3 两个报错信息

    1. Caused by: java.lang.NoSuchMethodException: com.atguigu.qqzone.pojo.Reply.<init>(),代表 reply 类中缺少空参构造器;
    2. Can not set java.util.Date com.atguigu.qqzone.xxxxx to java.time.LocalDateTime ,记得将 reply 和 hostReply 中的这个有关 Datatime 即日期类型都要进行修改,并且修改对应的 get/set 方法。

    7.4 将日志详情动态显示

    1. 我们现在已经获取到了 topic 信息,以及它关联到的所有的 reply 和 hostreply 信息,我们要在页面上把它展示出来;
    • 显示图片:th:src="@{|/imgs/${session.topic.author.headImg}|}"
    • 显示图片下面的名字:th:text="${session.topic.author.nickName}"
    • 显示标题:th:text="${session.topic.title}
    • 显示时间:th:text="${session.topic.topicDate}"
    • 显示日志内容:th:text="${session.topic.content}"

    detail.html 实现:

    <div id="div_topic_info">
    <!-- topic自身信息 -->
        <table id="tbl_topic_info">
            <tr>
                <td rowspan="2" class="w14 h96">
                    <div class="h64 center " style="width:100%;">
                        <img class="img56" th:src="@{|/imgs/${session.topic.author.headImg}|}"/>
                    </div>
                    <div class="h32 center" style="width:100%;" th:text="${session.topic.author.nickName}">乔峰</div>
                </td>
                <td class="topic_title">
                    <span th:text="${session.topic.title}">《萧某今天就和天下群雄决一死战》</span>
                    <span class="title_date_right" th:text="${session.topic.topicDate}">2021-09-01 12:30:55</span>
                </td>
            </tr>
            <tr>
                <td th:text="${session.topic.content}">杀母大仇, 岂可当作买卖交易?</td>
            </tr>
        </table>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. 显示回复的人的头像,名字,回复的是哪一条留言,该条留言的时间,下-面是回复的内容;
    • 回复是一个列表,要进行迭代:th:each="reply : ${session.topic.replyList}"
    • 回复人的头像:th:src="@{|/imgs/${reply.author.headImg}|}"
    • 回复人的名字:th:text="${reply.author.nickName}"
    • 回复的是哪一条留言:th:text="|回复:${session.topic.title}|"
    • 该条回复的时间:th:text="${reply.replyDate}"
    • 回复的内容:th:text="${reply.content}"
    1. 如果该条回复有主人回复,显示主人回复的内容和时间;
    • 判断如果有主人回复:th:if="${reply.hostReply!=null}"
    • 主人回复的内容:th:text="${reply.hostReply.content}"
    • 主人回复的时间:th:text="|主人回复于${reply.hostReply.hostReplyDate}|"
    1. 如果该条没有主人回复,鼠标放上去显示一个主人回复的超链接,鼠标挪开超链接消失。
    • 判断没有主人回复:th:unless="${reply.hostReply!=null}"
    • 设置主人回复的超链接(如果在自己空间且该条回复没有主人回复的时候显示),th:id="|a${reply.id}|" 保证该条回复对应的 id 唯一:
    <a th:id="|a${reply.id}|" th:if="${session.userBasic.id==session.friend.id}" th:unless="${reply.hostReply!=null}"  href="#" style="float: right;display: none;">主人回复</a>
    
    • 1
    <div id="div_reply_list">
        <table class="tbl_reply_info" th:each="reply : ${session.topic.replyList}">
            <tr>
                <td rowspan="2" class="w14 h88">
                    <div class="h56 center" style="width:100%;">
                        <img class="img48" th:src="@{|/imgs/${reply.author.headImg}|}"/>
                    </div>
                    <div class="h32 center" style="width:100%;" th:text="${reply.author.nickName}">段誉</div>
                </td>
                <td class="reply_title" th:onmouseover="|showDelImg('img${reply.id}')|" th:onmouseout="|hiddenDelImg('img${reply.id}')|">
                    <span th:text="|回复:${session.topic.title}|">萧某今天就和天下群雄决一死战,你们一起上吧!</span>
                    <span class="title_date_right" th:text="${reply.replyDate}">2021-09-01 14:35:15</span>
                </td>
            </tr>
            <tr>
                <td>
                    <span th:text="${reply.content}">你可曾见过边关之上、宋辽相互仇杀的惨状?</span><br/>
                    <ul th:if="${reply.hostReply!=null}">
                        <li th:text="${reply.hostReply.content}">你以为我是慕容复的人,所以和我比试?</li>
                        <li th:text="|主人回复于${reply.hostReply.hostReplyDate}|">主人回复于2021/10/01 11:50:30</li>
                    </ul>
                    <a th:id="|a${reply.id}|" th:if="${session.userBasic.id==session.friend.id}" th:unless="${reply.hostReply!=null}"  href="#" style="float: right;display: none;">主人回复</a>
                </td>
            </tr>
        </table>
    </div>
    
    • 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

    8、添加回复功能

    1. 首先这是一个表单:action="reply.do" method="post"
    2. 其次我们要有一个添加回复的 operate,回复的标题是我们当前日志的标题:type="text" th:value="|《${session.topic.title}》|"
    <div id="div_add_reply">
        <p class="add_reply_title">添加回复</p>
        <form action="reply.do" method="post">
            <input type="hidden" name="operate" value="addReply"/>
            <input type="hidden" name="topicId" th:value="${session.topic.id}"/>
            <table>
                <tr>
                    <th style="width: 25%">回复日志:</th>
                    <td><input type="text" th:value="|《${session.topic.title}》|" value="《萧某今天就和天下群雄决一死战,你们一起上吧!》" readonly /></td>
                </tr>
                <tr>
                    <th>回复内容:</th>
                    <td><textarea name="content" rows="3">这里是另一个回复!</textarea></td>
                </tr>
                <tr>
                    <th colspan="2">
                        <input type="submit" value=" 回 复 "/>
                        <input type="reset" value=" 重 置 "/>
                    </th>
                </tr>
            </table>
        </form>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    1. 然后我们需要新建一个 replyController,其中有一个 addReply 方法,看一下 reply 表中有哪些参数是需要从表单的;
      在这里插入图片描述
      解释:

      • reply中有 id(自增)、context(需要从表单获取的)、replyDate(当前日期)、author(当前登录的这个作者)、topic(可以从session中获取,也可以给表单设置隐藏域,type="hidden" name="topicId" th:value="${session.topic.id}"
      • 那么我们需要获取的参数有:String content ,Integer topicId , HttpSession session
    2. 同时需要调用 replyService 层的 addReply 方法,replyService 同样要调用 reolyDAO 层的 addReply 方法和数据库交互;

    ReplyController 控制器实现:

    public class ReplyController {
        private ReplyService replyService ;
    
        public String addReply(String content ,Integer topicId , HttpSession session){
            UserBasic author = (UserBasic)session.getAttribute("userBasic");
            Reply reply = new Reply(content , LocalDateTime.now() , author , new Topic(topicId));
            replyService.addReply(reply);
            return "redirect:topic.do?operate=topicDetail&id="+topicId;
            // detail.html
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意:

    • 对于 LocalDateTime 类,其中新建当前时间的方法为 LocalDateTime.now()
    • 在 reply 类中创建一个这四个参数的构造器。

    ReplyService 层实现:

    @Override
    public void addReply(Reply reply) {
        replyDAO.addReply(reply);
    }
    
    • 1
    • 2
    • 3
    • 4

    ReplyDAO 层实现:

    @Override
    public void addReply(Reply reply) {
        executeUpdate("insert into t_reply values(0,?,?,?,?)",reply.getContent(),reply.getReplyDate(),reply.getAuthor().getId() , reply.getTopic().getId()) ;
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 将各层之间的配置文件配置好;
    <bean id="reply" class="com.atguigu.qqzone.controller.ReplyController">
        <property name="replyService" ref="replyService"/>
    </bean>
    
    • 1
    • 2
    • 3
    1. 现在数据库更新数据之后,我们要重定向,重新发送一次请求请求数据库的最新的日志详情数据;
      • main.html 页面这里请求的 topic 页面详情超链接是:th:href="@{|/topic.do?operate=topicDetail&id=${topic.id}|}"
      • 那么我们重定向的页面就是,这样也可以直接跳转到展示日志详情页面上:"redirect:topic.do?operate=topicDetail&id="+topicId;

    9、删除回复功能

    1. 删除回复功能:如果回复有关联的主人回复,需要先删除主人回复, 因为 对数据库来说,如果需要删除主表数据,需要首先删除子表数据;涉及到和数据库交互,删除数据库的回复,所以要给 replyController 发送请求;
    2. replyController 中添加 delReply 方法,要调用 ReplyService 中的 delReply 方法;
    3. ReplyService 要判断其中是否有关联的 hostReply ,如果有,需要额外多家一步调用 hostReplyService 层的删除 hostReply 方法;
    4. 然后调用 replyDAO 中的 delReply 方法删除掉 reply 列表;
    5. 重定向的时候需要 topic 值,所以我们要么从 session 中获取,要么点击删除图标即发送请求时一起传进来;

    ReplyController 控制器实现:

    public String delReply(Integer replyId , Integer topicId){
        replyService.delReply(replyId);
        return "redirect:topic.do?operate=topicDetail&id="+topicId;
    }
    
    • 1
    • 2
    • 3
    • 4

    ReplyService 层实现:

    @Override
    public void delReply(Integer id) {
        //1.根据id获取到reply
        Reply reply = replyDAO.getReply(id);
        if(reply!=null){
            //2.如果reply有关联的hostReply,则先删除hostReply
            // 这里只能hostReply根据reply的id查询,就是说hostReply依赖reply,而不是reply里面关联hostReply
            HostReply hostReply = hostReplyService.getHostReplyByReplyId(reply.getId());
            if(hostReply!=null){
                hostReplyService.delHostReply(hostReply.getId());
            }
            //3.删除reply
            replyDAO.delReply(id);
        }
    }
    
    @Override
    public void delReplyList(Topic topic) {
        List<Reply> replyList = replyDAO.getReplyList(topic);
        if(replyList!=null){
            for(Reply reply : replyList){
                delReply(reply.getId());
            }
        }
    }
    
    • 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

    HostReplyService 层实现:

    public class HostReplyServiceImpl implements HostReplyService {
        private HostReplyDAO hostReplyDAO ;
    
        @Override
        public HostReply getHostReplyByReplyId(Integer replyId) {
            return hostReplyDAO.getHostReplyByReplyId(replyId);
        }
    
        @Override
        public void delHostReply(Integer id) {
            hostReplyDAO.delHostReply(id);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ReplyDAO 层实现:

    @Override
    public Reply getReply(Integer id) {
        return load("select * from t_reply where id =? " , id);
    }
    
    @Override
    public void delReply(Integer id) {
        executeUpdate("delete from t_reply where id = ? " , id) ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    HostReplyDAO 层实现:

    public class HostReplyDAOImpl extends BaseDAO<HostReply> implements HostReplyDAO {
        @Override
        public HostReply getHostReplyByReplyId(Integer replyId) {
            return load("select * from t_host_reply where reply = ? " , replyId);
        }
    
        @Override
        public void delHostReply(Integer id) {
            super.executeUpdate("delete from t_host_reply where id = ? " , id) ;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 我在自己空间可以删除回复,或者我不在自己空间,但是我可以删除别人空间中我的回复;只需要在删除小图标上做判断 th:if="${session.userBasic.id==session.friend.id || session.userBasic.id==reply.author.id}"
    2. 添加 delReply 动态功能:当点击删除小图标的时候弹出确认框,th:onclick="|delReply(${reply.id} , ${session.topic.id})|"
    <img th:if="${session.userBasic.id==session.friend.id || session.userBasic.id==reply.author.id}" th:id="|img${reply.id}|" class="delReplyImg" th:src="@{/imgs/del.jpg}" th:onclick="|delReply(${reply.id} , ${session.topic.id})|"/>
    
    • 1
    function delReply(replyId , topicId){
        if(window.confirm("是否确认删除?")){
            window.location.href='reply.do?operate=delReply&replyId='+replyId+'&topicId='+topicId;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解释:

    • 删除之后跳转页面超链接为 reply.do,那我们的请求能找到 replyController 控制器,执行其中的 delReply 方法;
    • 根据其中的 replyId 确认删除的是哪一条回复;
    • 根据其中的 topicId 确认删除之后重定向到哪个日志详情页面。

    10、删除日志功能

    1. 在主页面 main.html 页面上设置删除小按钮,如果不是自己的空间,则不能删除日志,即不显示这个小按钮:
    <tr th:unless="${#lists.isEmpty(session.friend.topicList)}" th:each="topic : ${session.friend.topicList}">
        <td><input type="button" value="删除" th:if="${session.userBasic.id==session.friend.id}" th:onclick="|delTopic(${topic.id})|"/></td>
    </tr>
    
    • 1
    • 2
    • 3
    1. 设置动态确认框,是否删除日志确认框(属于 JS 范畴):
    function delTopic(topicId){
        if(window.confirm("是否确认删除日志?")){
            window.location.href="topic.do?operate=delTopic&topicId="+topicId;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 根据确认删除之后跳转链接,我们可以找到 topicController 控制器中的 delTopic 方法;
    public String delTopic(Integer topicId){
        topicService.delTopic(topicId);
        return "redirect:topic.do?operate=getTopicList" ;
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 删除日志,首先需要考虑是否有关联的回复;删除回复,首先需要考虑是否有关联的主人回复;这些具体的关联细节放到 topicService 层去实现;

    TopicService 层实现:

    @Override
    public void delTopic(Integer id) {
        Topic topic = topicDAO.getTopic(id);
        if(topic!=null){
            //删除topic之前删除所有关联的reply
            replyService.delReplyList(topic);
            topicDAO.delTopic(topic);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 获取该日志关联的所有回复,如果不为空的情况下调用之前的 delReply 方法遍历删除每个回复;

    ReplyService 层实现:

    @Override
    public void delReplyList(Topic topic) {
        List<Reply> replyList = replyDAO.getReplyList(topic);
        if(replyList!=null){
            for(Reply reply : replyList){
                delReply(reply.getId());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 删除完之后我们要进行重定向,获取当前用户的所有的日志列表;
      return "redirect:topic.do?operate=getTopicList" ;

    TopicController 控制器中获取当前用户的所有 topic 信息:

    public String getTopicList(HttpSession session){
        //从session中获取当前用户信息
        UserBasic userBasic = (UserBasic)session.getAttribute("userBasic");
        //再次查询当前用户关联的所有的日志
        List<Topic> topicList = topicService.getTopicList(userBasic);
        //设置一下关联的日志列表(因为之前session中关联的friend的topicList和此刻数据库中不一致)
        userBasic.setTopicList(topicList);
        //重新覆盖一下friend中的信息(为什么不覆盖userbasic中?因为main.html页面迭代的是friend这个key中的数据)
        session.setAttribute("friend",userBasic);
        return "frames/main";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    解释:

    • 我们删除一篇日志之后,我们只需要更新页面的中间日志列表 main.html 即可,而不用重新请求 index 页面,所以我们根据当前用户获取它关联的所有日志,然后再 set 进 topicList 这个属性中,然后再保存到保存作用域中就可以了。

    11、实现主人回复和添加新日志功能

    11.1 想要实现的功能

    1. 在自己空间别人的回复下面自己没有回复过的显示一个主人回复超链接;
    2. 点击这个超链接有一个表单显示出来(默认情况下不显示),表单点击提交按钮,给当前的回复添加一个主人回复;
    3. 在 main.html 界面实现添加新日志功能,但是在别人的空间不显示这个超链接。

    11.2 主人回复功能

    这里我尝试了很久,怎么点击超链接之后将隐藏在表格中的表单显示,查了很久也没查到可以先跳转链接再控制 style 显示的方法,所以我就直接设置成只要没有主人回复的话靠近那条回复就显示一个主人回复的框(样式很丑,而且鼠标挪开,框就不见了)。

    detail.html 页面实现添加主人回复功能:

    <div th:id="|a${reply.id}|" style="float: right;display:none;width:80%" th:if="${session.userBasic.id==session.friend.id}"
         th:unless="${reply.hostReply!=null}">
        <p class="add_reply_title">添加主人回复</p>
        <form action="hostReply.do" method="post">
            <input type="hidden" name="operate" value="addHostReply"/>
            <input type="hidden" name="replyId" th:value="${reply.id}"/>
            <textarea name="content" rows="3">这里是主人回复!</textarea><br/>
            <input type="submit" value=" 回 复 "/>
        </form>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    HostReplyController 控制器实现:

    public class HostReplyController {
        private HostReplyService hostReplyService;
    
        //分析它所需要的参数:id自增、context是输入的,date是现在时间,author和reply可以从session作用域中获取
        public String addHostReply(String content, Integer replyId, HttpSession session) {
            //先从作用域中获取当前作者
            UserBasic userBasic = (UserBasic) session.getAttribute("userBasic");
            //根据除自增列的四个参数创建一个hostReply对象
            HostReply hostReply = new HostReply(content, LocalDateTime.now(), userBasic, new Reply(replyId));
            //调用服务层实现添加功能
            hostReplyService.addHostReply(hostReply);
            //重定向之后返回日志详情页面
            Topic topic = (Topic) session.getAttribute("topic");
            return "redirect:topic.do?operate=topicDetail&id=" + topic.getId();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    hostReplyService 层实现:

    @Override
    public void addHostReply(HostReply hostReply) {
        hostReplyDAO.addHostReply(hostReply);
    }
    
    • 1
    • 2
    • 3
    • 4

    hostReplyDAO 层实现:

    @Override
    public void addHostReply(HostReply hostReply) {
        super.executeUpdate("insert into t_host_reply values(0,?,?,?,?)",hostReply.getContent(),hostReply.getHostReplyDate(),hostReply.getAuthor().getId(),hostReply.getReply().getId());
    }
    
    • 1
    • 2
    • 3
    • 4

    配置文件,其他依赖关系之前都配置过了:

    <bean id="hostReply" class="com.atguigu.qqzone.controller.HostReplyController">
        <property name="hostReplyService" ref="hostReplyService"/>
    </bean>
    
    • 1
    • 2
    • 3

    11.3 发表新日志功能

    main.html 中右上角显示发表新日志的超链接:

    <div id="div_to_add">
        <p class="right8"><a th:if="${session.userBasic.id==session.friend.id}" th:href="@{/page.do?operate=page&page=frames/topic}">发表新日志</a></p>
    </div>
    
    • 1
    • 2
    • 3

    topic.html 表单页面:

    <!--<!DOCTYPE html>-->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" th:href="@{/css/common.css}">
        <link rel="stylesheet" th:href="@{/css/topic.css}">
        <script language="JavaScript">
            function getTopic() {
                window.location.href = "page.do?operate=page&page=frames/main";
            }
        </script>
    </head>
    <body>
    <div id="div_add_topic">
        <p class="add_reply_title">添加新日志</p>
        <form action="topic.do" method="post">
            <input type="hidden" name="operate" value="addTopic"/>
            <table>
                <tr>
                    <th style="width: 25%">日志标题:</th>
                    <td><input type="text" name="title" style="width: 90%" value="">
                    </td>
                </tr>
                <tr>
                    <th>日志内容:</th>
                    <td><textarea name="content" rows="5" style="width: 90%">我想再发表一篇日志!</textarea></td>
                </tr>
                <tr>
                    <th colspan="2">
                        <input type="submit" value=" 发 表 "/>
                        <input type="reset" value=" 重 置 "/>
                        <input type="button" value=" 返回日志列表 " th:onclick="|getTopic()|"/>
                    </th>
                </tr>
            </table>
        </form>
    </div>
    </body>
    </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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    TopicController 控制器实现添加新日志功能:

    public String addTopic(String title,String content,HttpSession session){
        UserBasic userBasic = (UserBasic)session.getAttribute("userBasic");
        Topic topic = new Topic(title, content, LocalDateTime.now(), new UserBasic(userBasic.getId()));
        topicService.addTopic(topic);
        return "redirect:topic.do?operate=getTopicList";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    topicService 实现:

    @Override
    public void addTopic(Topic topic) {
        topicDAO.addTopic(topic);
    }
    
    • 1
    • 2
    • 3
    • 4

    topicDAO 层实现:

    @Override
    public void addTopic(Topic topic) {
        executeUpdate("insert into t_topic values(0,?,?,?,?)",topic.getTitle(),topic.getContent(),topic.getTopicDate(),topic.getAuthor().getId());
    }
    
    • 1
    • 2
    • 3
    • 4

    11.4 网页展示

    在这里插入图片描述
    点击发表新日志超链接:
    在这里插入图片描述
    点击发表按钮之后:
    在这里插入图片描述
    鼠标放在没有主人回复的那一条之后,显示一个主人回复表单:
    在这里插入图片描述
    点击回复按钮之后:
    在这里插入图片描述

    12、所有代码

    1. 尚硅谷QQ空间项目(额外功能已实现)
  • 相关阅读:
    【云原生】一篇打通架构设计,Java设计模式6,依赖倒置原则
    【lwip】11-UDP协议&源码分析
    《优雅升级HBase》中不太“优雅”的情况
    基于分布式高可用集群的网购系统优化
    NXP公司K60N512+PWM控制BLDC电机
    openssl官网文档资料
    PCL 透视投影变换(OpenGL)
    第 2 章 线性表 (线性表的静态单链表存储结构(一个数组可生成若干静态链表)实现)
    SAP UI5 sap.ui.base.ManagedObject 的构造函数参数讲解
    扣图(图像色彩空间转换)
  • 原文地址:https://blog.csdn.net/qq_42148002/article/details/124979707