私信
我们定义的私信字段为id,from_id,to_id,conversation_id(为from_id和to_id的拼接,小的在前面),content为内容,status为状态,以及create_time。
在dao层我们主要定义了查询当前用户的会话列表、会话数量、某个会话详情、某个会话消息数量以及新增数量、修改消息状态
@Mapper
public interface MessageMapper {
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信。
List<Message> selectConversation(int userId,int offset,int limit);
// 查询当前用户的会话数量
int selectConversationCount(int userId);
// 查询某个会话所包含的私信列表
List<Message> selectLetters(String conversationId,int offset,int limit);
// 查询某个会话包含的私信数量
int selectLetterCount(String conversationId);
// 查询未读私信的数量
int selectLetterUnreadCount(int userId, String conversationId);
// 新增消息
int insertMessage(Message message);
// 修改消息的状态
int updateStatus(List<Integer> ids, int status);
}
在mapper层我们主要看两个方法的实现。
在这里要注意的就是to和from与用户之间的关系
第一个是查询全部的会话,并返回第一条最新消息。首先我们得到当前用户发送的或者接收的未删除的且不是系统通知的全部会话,然后以conversation_id进行分组,然后得到每个分组最大的id(最新私信),然后在外层得到这些id对应的message即可。
<select id="selectConversation" resultType="Message">
select <include refid="selectFields"></include>
from message
where id in (
select max(id) from message
where status != 2
and from_id != 1
and (from_id = #{userId} or to_id = #{userId})
group by conversation_id
)
order by id desc
limit #{offset}, #{limit}
</select>
第二个是更新私信状态,其中<foreach>标签用于遍历参数中的ids列表,然后用括号括起来,直接用逗号分隔。
<update id="updateStatus">
update message set status = #{status}
where id in
-- open="(" separator="," close=")"表示(id1,id2,id3……)
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
得到私信列表
得到私信列表同得到帖子列表类似,我们也需要将一个私信封装到一个map里,map中需要存放会话、会话的私信数、未读私信数和会话目标,然后将这些map放入一个列表中,然后放入model中返回。在这里,我们还要查询得到所有会话的未读消息数
@RequestMapping(path = "/letter/list",method = RequestMethod.GET)
public String getLetterList(Model model, Page page){
User user = hostHolder.getUser();
// 分页信息
page.setLimit(5);
page.setPath("/letter/list");
page.setRows(messageService.findConversationCount(user.getId()));
// 会话列表
List<Message> conversationList =
messageService.findConversations(user.getId(),page.getOffset(),page.getLimit());
List<Map<String, Object>> conversations = new ArrayList<>();
if (conversationList != null){
for (Message message : conversationList){
Map<String, Object> map = new HashMap<>();
map.put("conversation",message);
map.put("letterCount",messageService.findLetterCount(message.getConversationId()));
map.put("UnreadCount",messageService.findLetterUnreadCount(user.getId(),message.getConversationId()));
int targetId = user.getId() == message.getFromId()? message.getToId() : message.getFromId();
map.put("target",userService.findUserById(targetId));
conversations.add(map);
}
}
model.addAttribute("conversations",conversations);
// 查询未读消息数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(),null);
model.addAttribute("letterUnreadCount",letterUnreadCount);
return "/site/letter";
}
查看会话详情
查看会话详情也是类似,我们需要根据conversation_id查到该会话的所有私信,然后每个私信是一个map,map中存放了from_user和私信内容,所有私信放入一个列表中,然后加入model中,除此之外,model中还要放入该会话的目标user。其实这些需求都是根据前端的页面进行具体实现的,知道了需求才知道要设计什么接口。
@RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId,Page page, Model model){
// 设置分页信息
page.setLimit(5);
page.setPath("/letter/detail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
// 私信列表
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
List<Map<String, Object>> letters = new ArrayList<>();
if (letterList != null){
for (Message message : letterList){
Map<String, Object> map = new HashMap<>();
map.put("letter",message);
map.put("fromUser",userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters",letters);
// 私信目标
model.addAttribute("target",getLetterTarget(conversationId));
// 将消息列表里的未读消息改为已读
List<Integer> ids = getLetterIds(letterList);
if (!ids.isEmpty()){
messageService.readMessage(ids);
}
return "/site/letter-detail";
}
最后用户既然已经查看了该会话,那么该会话的所有私信就不应该是未读信息了,要将它们全部置为已读,因此需要得到置为已读私信的id,这些私信满足to_id是当前用户且status为0,当然肯定是该会话的私信。
private List<Integer> getLetterIds(List<Message> letterList){
List<Integer> ids = new ArrayList<>();
if (letterList != null){
for (Message message : letterList){
// 如果这条消息是发送给你的且是未读状态,那么现在你已读了
if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0){
ids.add(message.getId());
}
}
}
return ids;
}
发送私信
发送私信我们使用ajax的方式,在js中实现,只要声明好路径、数据和回调函数即可。
function send_letter() {
$("#sendModal").modal("hide");
var toName = $("#recipient-name").val();
var content = $("#message-text").val();
$.post(
CONTEXT_PATH + "/letter/send",
{"toName":toName,"content":content},
function (data) {
data = $.parseJSON(data);
if (data.code == 0){
$("#hintBody").text("发送成功!");
} else {
$("#hintBody").text(data.msg);
}
$("#hintModal").modal("show");
setTimeout(function(){
$("#hintModal").modal("hide");
location.reload();
}, 2000);
}
);
}
在controller层,由于是异步请求,我们需要用@ResponseBody注解声明,然后使用JSON工具返回结果。
主要就是检查目标是否存在,然后设置消息的发送方、接收方和会话id,内容和日期,然后调用service即可
@RequestMapping(path = "letter/send",method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName,String content){
User target = userService.findUserByName(toName);
if (target == null){
return CommunityUtil.getJSONString(1,"目标用户不存在");
}
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(target.getId());
if (message.getFromId() < message.getToId()){
message.setConversationId(message.getFromId() + "_" + message.getToId());
} else {
message.setConversationId(message.getToId() + "_" + message.getFromId());
}
message.setContent(content);
message.setCreateTime(new Date());
messageService.addMessage(message);
return CommunityUtil.getJSONString(0);
}
异常统一处理
首先使用thymeleaf模板,我们只需要在templates文件夹下创建一个error文件夹,然后把404.html,500.html页面放入,这样发生404、500等错误就会自动显示这两个页面。
然后我们在homecontroller中创建一个/error请求,用于重定向到500页面
@RequestMapping(path = "/error",method = RequestMethod.GET)
public String getErrorPage(){
return "/error/500";
}
最后我们再创建一个异常处理类,统一处理controller发生的异常。
使用@ControllerAdvice注解,通过annotations参数声明只扫描带有Controller注解的Bean。然后用@ExceptionHandler({Exception.class})修饰handleException()方法,该注解表示该方法在Controller出现异常(全部异常)之后调用。通过获取request中的“x-requested-with”参数得到请求方式,如果是XML,则是异步请求,此时通过response的输出流输出JSON对象即可,如果不是异步请求,就重定向到/error请求。
// 只扫描带有controller注解的Bean
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
// 用于修饰方法,该方法在Controller出现异常后被调用
@ExceptionHandler({Exception.class})
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response){
logger.error("服务器发生异常:"+e.getMessage());
for (StackTraceElement element : e.getStackTrace()){
logger.error(element.toString());
}
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)){
// 这说明这是一个异步请求
// 返回的是一个常规字符串
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
} catch (IOException ex) {
ex.printStackTrace();
}
writer.write(CommunityUtil.getJSONString(1,"服务器异常!"));
} else {
try {
response.sendRedirect(request.getContextPath() + "/error");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
统一记录日志
在统一记录日志这里我们需要用到AOP的编程方式。
AOP是一种编程思想,即面向切面编程,是对OOP的补充,可以进一步提高编程的效率。
我们需要定义一个系统组件贯穿多个业务组件,声明Pointcut(切点),Advice(解决的是具体织入的系统逻辑),将系统业务封装在Aspect中,将Aspect织入(Weaving),Joinpoint表示目标对象织入的位置
AOP的实现
在SpringBoot中使用AOP很简单,我们需要声明一个组件,并用@Aspect注解修饰,首先我们需要定义切点,下面这个切点即service包下的所有java文件的所有方法,对参数也没有限制。
// 第一个*代表方法的返回值,可以为任意值
// com.nowcoder.community.service.*.* 表示service包下的所有类的所有方法
// (..)表示所有的参数
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut(){
}
我们可以在切点之前(@Before(“pointcut()”))
切点之后(@After(“pointcut()”))
有返回值之后(@AfterReturning(“pointcut()”))
抛异常之后(@AfterThrowing(“pointcut()”))
切点之前之后(@Around(“pointcut()”))
声明并实现相关的方法。
而目前我们只要记录日志,因此只需要在切点之前即可。首先使用@Component
@Aspect注解,然后创建一个Logger实例,声明好切点,然后在切点之前调用方法即可,该方法得到request,再用request得到ip,然后使用joinPoint得到类型名和方法名(也就说这些切点实际就是方法?),最后使用“用户xx,在xx时间,访问了xx方法”这样的格式记录日志
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void before(JoinPoint joinPoint){
// 用户{1.2.3.4},在{xxx},访问了[com.nowcoder.community.service.xxx()].
// 使用工具类得到request
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
// 得到类型名和方法名
String target = joinPoint.getSignature().getDeclaringType() + "." + joinPoint.getSignature().getName();
logger.info(String.format("用户[%s],在[%s],访问了[%s].",ip,now,target));
}
}