用户登录
登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词,如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表
查看日志详情:
删除日志
删除特定回复
删除特定主人回复
添加日志、添加回复、添加主人回复
点击左侧好友链接,进入好友的空间
注意:
用户登录信息(一键快速登录)、用户详情信息(后面自己再修改手机号和邮箱等等详细信息) 、 日志 、 回贴 、 主人回复

一般数据库设计中遵循的规则:
数据库设计的范式和数据库的查询性能很多时候是相悖的,我们需要根据实际的业务情况做一个选择:
比如这个 QQ空间项目,明明主人回复中的作者,我们可以根据 日志-回复-主人回复 之间的关联关系查到作者信息,为什么我们还要在主人回复中设置作者这一项呢(这样做不满足第三范式)?

ORM 编程思想:(object relational mapping),有点万事万物皆对象那种意思
注意:
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 方法。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 时分秒
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;
}
}
解释:
LocalDateTime,所以这里要和老师定义的 Data 类型不一致,否则后面会报错,下面所有涉及到日期类型的都替换成 LocalDateTime。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;
}
}
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;
}
}
接下来,我们大致要完成的事情是:
实际上开发中,我们都是根据想实现的功能需求来一步一步完善 DAO层、service 层、controller 控制器以及配置文件的。
首先,我们程序运行之后,首先看到的界面是一个登陆界面,界面如下:

输入用户名和密码之后,需要验证是否登陆成功,也就是要去数据库里面查询是否有对应的账号和密码。
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 层中的登陆方法进行登陆验证;topicService 层获取日志列表,这些都是要展示在主页上的;所有的实现方法均需要提前在接口中定义规范,这里实现类中再重写这些方法,这里省略接口步骤。
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"/>
解释:

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>
解释:
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;
给 fid 起别名,TopicDAOImpl 实现类中的 getUserBasicList 方法要封装成一个 UserBasic 的 list,这个类里面没有 fid 这个属性,所以要起一个别名;
并且在 BaseDAO 中的获取别名的方法修改为 rsmd.getColumnLabel(), 而不是 rsmd.getColumnName() (获取列的列名);
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);
}
注意:
"java.time.LocalDateTime".equals(typeName); 。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" >版权所有®,欢迎盗版</p>
</div>
</div>
解释:
th:src="@{/page.do?operate=page&page=frames/left}",目的是执行 super.processTemplate() 方法,让 thymeleaf 生效,数据动态的显示在界面上。这里的 PageController 是通用的,所有的静态页面上的动态数据均需要经过 PageController 的作用:
public class PageController {
public String page(String page) {
return page;
}
}
解释:
/page.do?operate=page&page=login 通过 servletPath 解析后从 bean 容器中找到 pageController;page 方法;"login" 返回给 DispatcherServlet;/login.html 页面。配置文件:
<bean id="page" class="com.atguigu.myssm.myspringmvc.PageController"/>
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>
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>
${session.friend.nickName},如果不是,我们要右边显示一块超链接是返回自己的空间;${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>
第一次发送请求:
/page.do?operate=page&page=login 通过 servletPath 解析后从 bean 容器中找到 pageController;将 login.html 页面响应给客户端:
客户端看到一个登陆页面,我们点击登陆之后,发送了第二次请求。
第二次请求(登陆验证):
客户端看到这三个 src 之后给服务器发送三次请求,分别请求这三个页面:
后面的三次请求的 /page.do?operate=page&page=frames/left 后端的处理的具体过程和第一次发送请求的处理过程一样。
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>
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";
}
问题:
<a> 里面加 target="_top" 属性,上面返回自己空间的超链接也需要设置这个属性。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>
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 资源。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);
}
<bean id="topic" class="com.atguigu.qqzone.controller.TopicController">
<property name="topicService" ref="topicService"/>
</bean>
ReplyService 层,新建 RelayDAO 层,并且实现这两个中的方法;topicService 层中需要用到这个 replyService 中的 getReplyListByTopicId 方法来获取所有的 replyList;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 层来实现;topicService 的 getTopic 方法中封装,在查询 topic 本身信息时,同时调用 userBasicService 中的获取 userBasic 方法,给 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 ;
}
}
解释:
replyService 层查询 replyList 属性,调用 userBasicService 层查询 author 属性;

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

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);
}
}
注意:
<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>
Caused by: java.lang.NoSuchMethodException: com.atguigu.qqzone.pojo.Reply.<init>(),代表 reply 类中缺少空参构造器;Can not set java.util.Date com.atguigu.qqzone.xxxxx to java.time.LocalDateTime ,记得将 reply 和 hostReply 中的这个有关 Datatime 即日期类型都要进行修改,并且修改对应的 get/set 方法。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>
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}"th:if="${reply.hostReply!=null}"th:text="${reply.hostReply.content}"th:text="|主人回复于${reply.hostReply.hostReplyDate}|"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>
action="reply.do" method="post";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>
然后我们需要新建一个 replyController,其中有一个 addReply 方法,看一下 reply 表中有哪些参数是需要从表单的;

解释:
type="hidden" name="topicId" th:value="${session.topic.id}")同时需要调用 replyService 层的 addReply 方法,replyService 同样要调用 reolyDAO 层的 addReply 方法和数据库交互;
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.now();@Override
public void addReply(Reply reply) {
replyDAO.addReply(reply);
}
@Override
public void addReply(Reply reply) {
executeUpdate("insert into t_reply values(0,?,?,?,?)",reply.getContent(),reply.getReplyDate(),reply.getAuthor().getId() , reply.getTopic().getId()) ;
}
<bean id="reply" class="com.atguigu.qqzone.controller.ReplyController">
<property name="replyService" ref="replyService"/>
</bean>
th:href="@{|/topic.do?operate=topicDetail&id=${topic.id}|}""redirect:topic.do?operate=topicDetail&id="+topicId;replyController 发送请求;ReplyService 中的 delReply 方法;hostReplyService 层的删除 hostReply 方法;replyDAO 中的 delReply 方法删除掉 reply 列表;public String delReply(Integer replyId , Integer topicId){
replyService.delReply(replyId);
return "redirect:topic.do?operate=topicDetail&id="+topicId;
}
@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());
}
}
}
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);
}
}
@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) ;
}
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) ;
}
}
th:if="${session.userBasic.id==session.friend.id || session.userBasic.id==reply.author.id}";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;
}
}
解释:
<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>
function delTopic(topicId){
if(window.confirm("是否确认删除日志?")){
window.location.href="topic.do?operate=delTopic&topicId="+topicId;
}
}
topicController 控制器中的 delTopic 方法;public String delTopic(Integer topicId){
topicService.delTopic(topicId);
return "redirect:topic.do?operate=getTopicList" ;
}
topicService 层去实现;TopicService 层实现:
@Override
public void delTopic(Integer id) {
Topic topic = topicDAO.getTopic(id);
if(topic!=null){
//删除topic之前删除所有关联的reply
replyService.delReplyList(topic);
topicDAO.delTopic(topic);
}
}
ReplyService 层实现:
@Override
public void delReplyList(Topic topic) {
List<Reply> replyList = replyDAO.getReplyList(topic);
if(replyList!=null){
for(Reply reply : replyList){
delReply(reply.getId());
}
}
}
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";
}
解释:
这里我尝试了很久,怎么点击超链接之后将隐藏在表格中的表单显示,查了很久也没查到可以先跳转链接再控制 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>
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());
}

点击发表新日志超链接:

点击发表按钮之后:

鼠标放在没有主人回复的那一条之后,显示一个主人回复表单:

点击回复按钮之后:
