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

2023-10-28

目录

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;}
}

解释:

  • 构造器先定义一个空参构造器,后面需要哪几个参数的构造器再回来补;
  • 下面每个类我就不放 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;
    }
}

日期的继承关系:
父类: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;
    }
}

解释:

  • 这里是因为 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;
    }
}

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;
    }
}

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">

配置文件:

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

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";
    }
}

解释:

  • 首先调用 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);
    }
}

配置文件:

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

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);
    }
}

配置文件:

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

解释:

  • 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);
    }
}

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());
    }
}    

配置文件:

<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>

解释:

  • 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);
}

注意:

  • 这里如果你是 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>

解释:

  • 如果在 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;
    }
}

解释:

  • /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"/>

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>

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>

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>

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. 在 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";
}

问题:

  • 但是我们一点击好友,在左侧(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. 新建一个 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";
    }
}

注意:

  • 这里要 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 ;
}

TopicDAO 实现:

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

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";
    }
}

解释:

  • 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 ;
    }
}

解释:

  • 将 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 ;
    }
}

解释:

  • 遍历这个 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());
    }
}

解释:

  • 由于在 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);
    }
}

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);
    }
}

注意:

  • 某 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>

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. 显示回复的人的头像,名字,回复的是哪一条留言,该条留言的时间,下-面是回复的内容;
  • 回复是一个列表,要进行迭代: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>
<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>

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. 然后我们需要新建一个 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
    }
}

注意:

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

ReplyService 层实现:

@Override
public void addReply(Reply reply) {
    replyDAO.addReply(reply);
}

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. 将各层之间的配置文件配置好;
<bean id="reply" class="com.atguigu.qqzone.controller.ReplyController">
    <property name="replyService" ref="replyService"/>
</bean>
  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;
}

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());
        }
    }
}

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);
    }
}

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) ;
}

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. 我在自己空间可以删除回复,或者我不在自己空间,但是我可以删除别人空间中我的回复;只需要在删除小图标上做判断 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})|"/>
function delReply(replyId , topicId){
    if(window.confirm("是否确认删除?")){
        window.location.href='reply.do?operate=delReply&replyId='+replyId+'&topicId='+topicId;
    }
}

解释:

  • 删除之后跳转页面超链接为 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. 设置动态确认框,是否删除日志确认框(属于 JS 范畴):
function delTopic(topicId){
    if(window.confirm("是否确认删除日志?")){
        window.location.href="topic.do?operate=delTopic&topicId="+topicId;
    }
}
  1. 根据确认删除之后跳转链接,我们可以找到 topicController 控制器中的 delTopic 方法;
public String delTopic(Integer topicId){
    topicService.delTopic(topicId);
    return "redirect:topic.do?operate=getTopicList" ;
}
  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. 获取该日志关联的所有回复,如果不为空的情况下调用之前的 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. 删除完之后我们要进行重定向,获取当前用户的所有的日志列表;
    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";
}

解释:

  • 我们删除一篇日志之后,我们只需要更新页面的中间日志列表 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>

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();
    }
}

hostReplyService 层实现:

@Override
public void addHostReply(HostReply hostReply) {
    hostReplyDAO.addHostReply(hostReply);
}

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());
}

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

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

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>

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>

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";
}

topicService 实现:

@Override
public void addTopic(Topic topic) {
    topicDAO.addTopic(topic);
}

topicDAO 层实现:

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

11.4 网页展示

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

12、所有代码

  1. 尚硅谷QQ空间项目(额外功能已实现)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

(尚硅谷)JavaWeb新版教程08-QQZone项目的实现 的相关文章

  • 如何在 JPA 中使用枚举

    我有一个电影租赁系统的现有数据库 每部电影都有一个评级属性 在 SQL 中 他们使用约束来限制该属性的允许值 CONSTRAINT film rating check CHECK rating text text OR rating tex
  • 如何获取枚举的子集

    大多数情况下 包含所有元素的枚举显示在用户界面的下拉列表中 我们只需要在用户界面中显示 5 个字段中的 2 个 通过某种方式利用可用于枚举的相同函数来获取此数据的更简单方法是什么 enum Color RED GREEN BLACK BLU
  • 在 MongoDB Java 驱动程序中如何使用 $filter

    我有一个适用于 MQL 的查询 我需要将其翻译成Java MQL 中的查询如下所示 db
  • 使用 Spring MVC 返回 PDF 文件

    实际上 我有这个功能 我有一个框架 可以在其中设置 URL ip port birt preview report report rptdesign format pdf parameters 并且该框架呈现 PDF 文件 但我想隐藏该网址
  • 如何将列表转换为地图?

    最近我和一位同事讨论了转换的最佳方式是什么List to Map在 Java 中 这样做是否有任何具体的好处 我想知道最佳的转换方法 如果有人可以指导我 我将非常感激 这是个好方法吗 List
  • 使用 xuggle 将 mp3 转换为 wav 出现异常

    我正在尝试将 mp3 转换为 wav 代码在这里 String mp3 F work pic2talk38512 mp3 String wav F work pic2talk38512 wav TranscodeAudioAndVideo
  • 从 Bitmap 类创建 .bmp 图像文件

    我创建了一个使用套接字的应用程序 客户端在其中接收图像并将图像数据存储在 Bitmap 类中 谁能告诉我如何创建一个名为我的图像 png or 我的图像 bmp来自此 Bitmap 对象 String base64Code dataInpu
  • 将 JSON Map 传递到 Spring MVC 控制器

    我正在尝试将 Map 的 JSON 表示形式作为 POST 参数发送到我的控制器中 RequestMapping value search do method RequestMethod GET consumes application j
  • JPA:如何将字符串持久保存到数据库字段中,输入 MYSQL Text

    需求是用户可以写文章 所以我选择typeText为了contentmysql数据库内的字段 我怎样才能转换Java String into MySQL Text 干得好Jim Tough Entity public class Articl
  • 在 Java 中填充布尔数组

    作为一名相当新手的 Java 程序员 我给自己设定了一个艰巨的挑战 尝试编写一个简单的文本冒险 不出所料 我已经遇到了困难 我试图为我的 Location 类提供一个属性来存储它包含的退出 我为此使用了一个布尔数组 本质上保存代表每个出口的
  • DateTimeFormatter.parseLocalDate 抛出 UnsupportedOperationException

    该API用于解析本地日期 http joda time sourceforge net apidocs org joda time format DateTimeFormatter html parseLocalDate 28java la
  • ASM之前看一下maxStack指令吗?

    我正在尝试使用 ASM 库将字节代码转换为不同的格式 这可以使用 MethodVisitor 来完成 就像这个简单的测试代码一样 return new MethodVisitor ASM7 Override public void visi
  • 确定代码是否在 App Engine 运行时 (Java) 上运行

    如何确定某些代码 Serv let 或简单的类 是否正在 Google App Engine 运行时 Java 上运行 以便决定是否使用 App Engine 的特定库 是否有一些可靠的运行时环境 ID 您可以检查com google ap
  • JSON 对象数组转 Java POJO

    将此 JSON 对象转换为 java 中的类 您的 POJO 类中的映射将如何 ownerName Robert pets name Kitty name Rex name Jake This kind of question is ver
  • 我们必须将 .class 文件放在 Tomcat 目录中的位置

    我必须把我的 class文件在 Tomcat 目录中 在我的 Java Complete Reference 书中 他们告诉将其放入C Program Files Apache Tomcat 4 0 webapps examples WEB
  • Java 7 中 Object 和 int 的比较

    最近我偶然发现了一个问题 让我停下来思考 对我来说 下面的代码应该总是会触发错误 但是当我的一位同事问我为什么 Eclipse 没有显示错误时 我无法回答任何问题 class A public static void main String
  • 设置 JAVA_HOME 变量时出现问题

    所以我刚刚下载了 Android Studio 并尝试设置 JAVA HOME 变量以便我可以运行它 我使用的是 Windows 8 并按照我找到的所有说明进行操作 但无济于事 转到高级系统设置 gt 环境变量 然后使用包含我的 jre7
  • Storm Spout 未收到 Ack

    我已经开始使用storm 所以我使用创建简单的拓扑本教程 https github com nathanmarz storm wiki Tutorial 当我运行我的拓扑时LocalCluster一切看起来都很好 我的问题是我没有得到元组的
  • 如何隐藏或删除 Android HoneyComb 中的状态栏?

    如何隐藏或删除 Android HoneyComb 中的状态栏 每次运行应用程序时 我都会发现某些内容必须被状态栏覆盖 我尝试改变AndroidManifest xml 但没有任何改变 你不知道 它被认为是永久的屏幕装饰 就像电容式主页 菜
  • Java,如何管理线程读取socket(websocket)?

    我有一个 WebSocket 服务器 我的服务器创建一个新线程来处理新连接 该线程一直处于活动状态 直到 websocket 中断 我的问题 对于 1 000 000 个连接 我需要 1 000 000 个线程 我如何通过一个线程处理多个

随机推荐

  • redis集群搭建(6节点单实例)

    1 分布式缓存的搭建 地址与服务器规划 Master1 192 168 232 128 slaver1 192 168 232 131 Master2 192 168 232 129 slaver2 192 168 232 132 Mast
  • Python中类的访问限制

    学习要点 在Python中 类的访问限制可以通过使用单下划线 和双下划线 进行控制 但这并不是严格的访问控制 而是一种命名约定 单下划线 约定性私有 一个下划线前缀表示一个属性或方法应该被视为 内部 使用 这只是一种约定 并没有严格的强制规
  • 数据库试题

    1 单选题 下列关于关系型数据库说法错误的是 A 使用键值对存储数据 B 关系型数据库 是指采用了关系模型来组织数据的数据库 C 关系型数据库的最大特点就是事务的一致性 D 关系型数据库的不足 大量数据的操作 字段的不固定 对表的索引以及表
  • 分支与循环语句_C语言入门

    目录 2 分支语句 什么是分支语句 2 1 if 语句 2 1 1 单分支 2 1 2 双分支 2 1 3 多分支 2 2 switch 选择结构 2 2 2 default 子句 3 循环语句 3 1for 循环 3 1 1 for循环的
  • Tomcat 详解(安装Tomcat、启动和配置,发布一个web网站)

    接下来开始 Tomcat DE 详细介绍 文章目录 一 安装Tomcat 二 Tomcat 启动和配置 2 1 目录文件 2 2 启动 Tomcat 测试 2 3 关闭 Tomcat 三 配置 四 发布一个 web 网站 五 常见面试题 一
  • Android系统之Bundle用法

    1 Bundle概述 Bundle在Android开发中非常常见 它的作用主要时用于传递数据 Bundle传递的数据包括 string int boolean byte float long double等基本类型或它们对应的数组 也可以是
  • springboot项目maven老是加载不了jar包如SpringBootApplication找不到

    困扰我好久了所以开贴记录一下 解决办法 我是清理了一些里面的杂乱的项目 就好了 具体的原因不太清楚 不过 应该事其他的项目造成的干扰 导致项目不能加载 以上方法不知道所以 下面方法可以尝试 在Maven命令后加入参数 Dmaven wago
  • 华为OD机试真题-静态代码扫描服务【2023Q1】【JAVA、Python、C++】

    题目描述 静态扫描快速快速识别源代码的缺陷 静态扫描的结果以扫描报告作为输出 1 文件扫描的成本和文件大小相关 如果文件大小为N 则扫描成本为N个金币 2 扫描报告的缓存成本和文件大小无关 每缓存一个报告需要M个金币 3 扫描报告缓存后 后
  • StringUtils详细介绍

    转自 https www oschina net code snippet 239959 8724 commentform public static void TestStr null 和 操作 判断是否Null 或者 System ou
  • 【Shell牛客刷题系列】SHELL16 判断输入的是否为IP地址:来练习正则表达式~

    该系列是基于牛客Shell题库 针对具体题目进行查漏补缺 学习相应的命令 刷题链接 牛客题霸 Shell篇 该系列文章都放到专栏下 专栏链接为 专栏 Linux 欢迎关注专栏 本文知识预告 本文首先结合之前正则表达式的相关知识 给出了IP地
  • 苹果ipcc下载

    http ax phobos apple com edgesuite net WebObjects MZStore woa wa com apple jingle appserver client MZITunesClientCheck v
  • 飞桨学习笔记之经典图像分类模型

    1 LeNet 2 AlexNet 3 VGG 4 GoogLeNet 5 ResNet 1 LeNet LeNet 通过连续使用卷积和池化层的组合提取图像特征 网络结构示意图 第一模块 包含5 5的6通道卷积和2 2的池化 卷积提取图像中
  • request_time和upstream_response_time详解

    下图是request time 下图是upstream response time 精准的描述就是 request time是从接收到客户端的第一个字节开始 到把所有的响应数据都发送完为止 upstream response time是从与
  • CTK系列之编译

    CTK编译 CTK编译笔记 资源下载 CTK 源码下载 master ctk github 链接 cmake环境准备 下载最新版本cmake的window安装包直接安装即可 camke下载链接 编译前准备 以本次安装为例 安装目录选择实在C
  • 考研复试数据库原理课后习题(十五)——数据仓库和联机分析处理技术

    数据仓库和联机分析处理技术 1 数据仓库的4个基本特征是什么 数据仓库的4个基本特征如下 数据仓库的数据是面向主题的 主题是一个抽象的概念 是在较高层次上将企业信息系统中的数据综合 归类并进行分析利用的抽象 面向主题的数据组织方式是根据分析
  • Python执行windows命令,报中文乱码解决方法

    笔者通过subprocess模块在windows机器上执行ping 服务器命令 结果报如下错误 Ping hostname 在这里可以用python的第三发插件chardet来探测一下原有字符是什么编码 如果没有安装 可以用命令 pip i
  • 闻达(wenda+chatGLM-6B)一键部署包

    本文是 基于闻达 wenda chatGLM 6B 构建自己的知识库小助手 的一键部署包 将处理数据 模型启动整理为 可执行脚本 并提供内置python虚拟环境 无需任何额外的环境配置 只需要上传自己的知识库文件即可使用 部署包链接 链接
  • 硬件系统工程师宝典(14)-----建议收藏!常用的多层板叠层结构“大揭秘”

    各位同学大家好 欢迎继续做客电子工程学习圈 今天我们继续来讲这本书 硬件系统工程师宝典 上篇我们说到PCB的布局可根据功能 频率 信号类型划分 布局时考虑敏感信号远离噪声源 今天我们来看看多层板的常用叠层结构 四层板的叠层结构 四层板的叠层
  • 用python对excel进行批量处理(1):将表格中的url用requests替换成图片

    前言 事情是这样的 npy说今天的工作里有一个900个项目的excel 表格的样子大概如下图所示 需求呢是将下图中的url全部替换成对应的图片添加到excel中 作为一个程序员 这样的任务要是手动做真是太难为人了 所以打算帮npy减轻一下负
  • (尚硅谷)JavaWeb新版教程08-QQZone项目的实现

    目录 1 熟悉 QQZone 业务需求 2 数据库设计 2 1 抽取实体 2 2 分析其中的属性 2 3 分析实体之间的关系 2 4 数据库的范式 3 根据数据库的表新建 pojo 类 ORM编程思想 3 1 UserBasic 类 3 2