💩 评论消息-邮件通知功能

This commit is contained in:
ronger 2021-04-22 22:34:38 +08:00
parent 00a82ff67e
commit 0ade2bdc53
5 changed files with 345 additions and 150 deletions

View File

@ -1,5 +1,7 @@
package com.rymcu.forest.service; package com.rymcu.forest.service;
import com.rymcu.forest.dto.NotificationDTO;
import javax.mail.MessagingException; import javax.mail.MessagingException;
/** /**
@ -12,6 +14,7 @@ public interface JavaMailService {
* 发送验证码邮件 * 发送验证码邮件
* @param email 收件人邮箱 * @param email 收件人邮箱
* @return 执行结果 0失败1成功 * @return 执行结果 0失败1成功
* @throws MessagingException
* */ * */
Integer sendEmailCode(String email) throws MessagingException; Integer sendEmailCode(String email) throws MessagingException;
@ -19,6 +22,15 @@ public interface JavaMailService {
* 发送找回密码邮件 * 发送找回密码邮件
* @param email 收件人邮箱 * @param email 收件人邮箱
* @return 执行结果 0失败1成功 * @return 执行结果 0失败1成功
* @throws MessagingException
* */ * */
Integer sendForgetPasswordEmail(String email) throws MessagingException; Integer sendForgetPasswordEmail(String email) throws MessagingException;
/**
* 发送下消息通知
* @param notification
* @return
* @throws MessagingException
*/
Integer sendNotification(NotificationDTO notification) throws MessagingException;
} }

View File

@ -1,7 +1,11 @@
package com.rymcu.forest.service.impl; package com.rymcu.forest.service.impl;
import com.rymcu.forest.core.constant.NotificationConstant;
import com.rymcu.forest.core.service.redis.RedisService; import com.rymcu.forest.core.service.redis.RedisService;
import com.rymcu.forest.dto.NotificationDTO;
import com.rymcu.forest.entity.User;
import com.rymcu.forest.service.JavaMailService; import com.rymcu.forest.service.JavaMailService;
import com.rymcu.forest.service.UserService;
import com.rymcu.forest.util.Utils; import com.rymcu.forest.util.Utils;
import org.apache.commons.lang.time.StopWatch; import org.apache.commons.lang.time.StopWatch;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -34,6 +38,8 @@ public class JavaMailServiceImpl implements JavaMailService {
private JavaMailSenderImpl mailSender; private JavaMailSenderImpl mailSender;
@Resource @Resource
private RedisService redisService; private RedisService redisService;
@Resource
private UserService userService;
/** /**
* thymeleaf模板引擎 * thymeleaf模板引擎
*/ */
@ -61,6 +67,43 @@ public class JavaMailServiceImpl implements JavaMailService {
return sendCode(email, 1); return sendCode(email, 1);
} }
@Override
public Integer sendNotification(NotificationDTO notification) throws MessagingException {
Properties props = new Properties();
// 表示SMTP发送邮件需要进行身份验证
props.put("mail.smtp.auth", true);
props.put("mail.smtp.ssl.enable", true);
props.put("mail.smtp.host", SERVER_HOST);
props.put("mail.smtp.port", SERVER_PORT);
// 如果使用ssl则去掉使用25端口的配置进行如下配置,
props.put("mail.smtp.socketFactory.class", "com.rymcu.forest.util.MailSSLSocketFactory");
props.put("mail.smtp.socketFactory.port", SERVER_PORT);
// 发件人的账号填写控制台配置的发信地址,比如xxx@xxx.com
props.put("mail.user", USERNAME);
// 访问SMTP服务时需要提供的密码(在控制台选择发信地址进行设置)
props.put("mail.password", PASSWORD);
mailSender.setJavaMailProperties(props);
User user = userService.findById(String.valueOf(notification.getIdUser()));
if (NotificationConstant.Comment.equals(notification.getDataType())) {
String url = notification.getDataUrl();
String thymeleafTemplatePath = "mail/commentNotification";
Map<String, Object> thymeleafTemplateVariable = new HashMap<String, Object>(4);
thymeleafTemplateVariable.put("user", notification.getAuthor().getUserNickname());
thymeleafTemplateVariable.put("articleTitle", notification.getDataTitle());
thymeleafTemplateVariable.put("content", notification.getDataSummary());
thymeleafTemplateVariable.put("url", url);
sendTemplateEmail(USERNAME,
new String[]{user.getEmail()},
new String[]{},
"【RYMCU】 消息通知",
thymeleafTemplatePath,
thymeleafTemplateVariable);
return 1;
}
return 0;
}
private Integer sendCode(String to, Integer type) throws MessagingException { private Integer sendCode(String to, Integer type) throws MessagingException {
Properties props = new Properties(); Properties props = new Properties();
// 表示SMTP发送邮件需要进行身份验证 // 表示SMTP发送邮件需要进行身份验证
@ -92,7 +135,7 @@ public class JavaMailServiceImpl implements JavaMailService {
redisService.set(code, to, 15 * 60); redisService.set(code, to, 15 * 60);
String thymeleafTemplatePath = "mail/forgetPasswordTemplate"; String thymeleafTemplatePath = "mail/forgetPasswordTemplate";
Map<String, Object> thymeleafTemplateVariable = new HashMap<String, Object>(); Map<String, Object> thymeleafTemplateVariable = new HashMap<String, Object>(1);
thymeleafTemplateVariable.put("url", url); thymeleafTemplateVariable.put("url", url);
sendTemplateEmail(USERNAME, sendTemplateEmail(USERNAME,
@ -109,18 +152,12 @@ public class JavaMailServiceImpl implements JavaMailService {
/** /**
* 发送thymeleaf模板邮件 * 发送thymeleaf模板邮件
* *
* @param deliver * @param deliver 发送人邮箱名 javalsj@163.com
* 发送人邮箱名 javalsj@163.com * @param receivers 收件人可多个收件人 11111@qq.com,2222@163.com
* @param receivers * @param carbonCopys 抄送人可多个抄送人 33333@sohu.com
* 收件人可多个收件人 11111@qq.com,2222@163.com * @param subject 邮件主题 您收到一封高大上的邮件请查收
* @param carbonCopys * @param thymeleafTemplatePath 邮件模板 mail\mailTemplate.html
* 抄送人可多个抄送人 33333@sohu.com * @param thymeleafTemplateVariable 邮件模板变量集
* @param subject
* 邮件主题 您收到一封高大上的邮件请查收
* @param thymeleafTemplatePath
* 邮件模板 mail\mailTemplate.html
* @param thymeleafTemplateVariable
* 邮件模板变量集
*/ */
public void sendTemplateEmail(String deliver, String[] receivers, String[] carbonCopys, String subject, String thymeleafTemplatePath, public void sendTemplateEmail(String deliver, String[] receivers, String[] carbonCopys, String subject, String thymeleafTemplatePath,
Map<String, Object> thymeleafTemplateVariable) throws MessagingException { Map<String, Object> thymeleafTemplateVariable) throws MessagingException {
@ -136,22 +173,15 @@ public class JavaMailServiceImpl implements JavaMailService {
/** /**
* 发送的邮件(支持带附件/html类型的邮件) * 发送的邮件(支持带附件/html类型的邮件)
* *
* @param deliver * @param deliver 发送人邮箱名 javalsj@163.com
* 发送人邮箱名 javalsj@163.com * @param receivers 收件人可多个收件人 11111@qq.com,2222@163.com
* @param receivers * @param carbonCopys 抄送人可多个抄送人 3333@sohu.com
* 收件人可多个收件人 11111@qq.com,2222@163.com * @param subject 邮件主题 您收到一封高大上的邮件请查收
* @param carbonCopys * @param text 邮件内容 测试邮件逗你玩的 <html><body><img
* 抄送人可多个抄送人 3333@sohu.com
* @param subject
* 邮件主题 您收到一封高大上的邮件请查收
* @param text
* 邮件内容 测试邮件逗你玩的 <html><body><img
* src=\"cid:attchmentFileName\"></body></html> * src=\"cid:attchmentFileName\"></body></html>
* @param attachmentFilePaths * @param attachmentFilePaths 附件文件路径
* 附件文件路径
* 需要注意的是addInline函数中资源名称attchmentFileName需要与正文中cid:attchmentFileName对应起来 * 需要注意的是addInline函数中资源名称attchmentFileName需要与正文中cid:attchmentFileName对应起来
* @throws Exception * @throws Exception 邮件发送过程中的异常信息
* 邮件发送过程中的异常信息
*/ */
private void sendMimeMail(String deliver, String[] receivers, String[] carbonCopys, String subject, String text, private void sendMimeMail(String deliver, String[] receivers, String[] carbonCopys, String subject, String text,
boolean isHtml, String[] attachmentFilePaths) throws MessagingException { boolean isHtml, String[] attachmentFilePaths) throws MessagingException {
@ -186,7 +216,6 @@ public class JavaMailServiceImpl implements JavaMailService {
} }
mailSender.send(mimeMessage); mailSender.send(mimeMessage);
stopWatch.stop(); stopWatch.stop();
//logger.info("邮件发送成功, 花费时间{}秒", stopWatch.getStartTime());
} }

View File

@ -1,22 +1,16 @@
package com.rymcu.forest.service.impl; package com.rymcu.forest.service.impl;
import com.rymcu.forest.core.service.AbstractService; import com.rymcu.forest.core.service.AbstractService;
import com.rymcu.forest.dto.ArticleDTO;
import com.rymcu.forest.dto.Author;
import com.rymcu.forest.dto.NotificationDTO; import com.rymcu.forest.dto.NotificationDTO;
import com.rymcu.forest.entity.Comment;
import com.rymcu.forest.entity.Follow;
import com.rymcu.forest.entity.Notification; import com.rymcu.forest.entity.Notification;
import com.rymcu.forest.entity.User;
import com.rymcu.forest.mapper.NotificationMapper; import com.rymcu.forest.mapper.NotificationMapper;
import com.rymcu.forest.service.*; import com.rymcu.forest.service.NotificationService;
import com.rymcu.forest.util.BeanCopierUtil; import com.rymcu.forest.util.BeanCopierUtil;
import org.springframework.beans.factory.annotation.Value; import com.rymcu.forest.util.NotificationUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -28,16 +22,6 @@ public class NotificationServiceImpl extends AbstractService<Notification> imple
@Resource @Resource
private NotificationMapper notificationMapper; private NotificationMapper notificationMapper;
@Resource
private ArticleService articleService;
@Resource
private CommentService commentService;
@Resource
private UserService userService;
@Resource
private FollowService followService;
@Value("${resource.domain}")
private String domain;
private final static String unRead = "0"; private final static String unRead = "0";
@ -51,7 +35,7 @@ public class NotificationServiceImpl extends AbstractService<Notification> imple
public List<NotificationDTO> findNotifications(Integer idUser) { public List<NotificationDTO> findNotifications(Integer idUser) {
List<NotificationDTO> list = notificationMapper.selectNotifications(idUser); List<NotificationDTO> list = notificationMapper.selectNotifications(idUser);
list.forEach(notification -> { list.forEach(notification -> {
NotificationDTO notificationDTO = genNotification(notification); NotificationDTO notificationDTO = NotificationUtils.genNotification(notification);
// 判断关联数据是否已删除 // 判断关联数据是否已删除
if (Objects.nonNull(notificationDTO.getAuthor())) { if (Objects.nonNull(notificationDTO.getAuthor())) {
BeanCopierUtil.copy(notificationDTO, notification); BeanCopierUtil.copy(notificationDTO, notification);
@ -71,84 +55,6 @@ public class NotificationServiceImpl extends AbstractService<Notification> imple
return list; return list;
} }
private NotificationDTO genNotification(Notification notification) {
NotificationDTO notificationDTO = new NotificationDTO();
BeanCopierUtil.copy(notification, notificationDTO);
ArticleDTO article;
Comment comment;
User user;
Follow follow;
switch (notification.getDataType()) {
case "0":
// 系统公告/帖子
article = articleService.findArticleDTOById(notification.getDataId(), 0);
if (Objects.nonNull(article)) {
notificationDTO.setDataTitle("系统公告");
notificationDTO.setDataUrl(article.getArticlePermalink());
user = userService.findById(article.getArticleAuthorId().toString());
notificationDTO.setAuthor(genAuthor(user));
}
break;
case "1":
// 关注
follow = followService.findById(notification.getDataId().toString());
notificationDTO.setDataTitle("关注提醒");
if (Objects.nonNull(follow)) {
user = userService.findById(follow.getFollowerId().toString());
notificationDTO.setDataUrl(getFollowLink(follow.getFollowingType(), user.getNickname()));
notificationDTO.setAuthor(genAuthor(user));
}
break;
case "2":
// 回帖
comment = commentService.findById(notification.getDataId().toString());
article = articleService.findArticleDTOById(comment.getCommentArticleId(), 0);
if (Objects.nonNull(article)) {
notificationDTO.setDataTitle(article.getArticleTitle());
notificationDTO.setDataUrl(comment.getCommentSharpUrl());
user = userService.findById(comment.getCommentAuthorId().toString());
notificationDTO.setAuthor(genAuthor(user));
}
break;
case "3":
// 关注用户发布文章
case "4":
// 关注文章更新
article = articleService.findArticleDTOById(notification.getDataId(), 0);
if (Objects.nonNull(article)) {
notificationDTO.setDataTitle("关注通知");
notificationDTO.setDataUrl(article.getArticlePermalink());
user = userService.findById(article.getArticleAuthorId().toString());
notificationDTO.setAuthor(genAuthor(user));
}
break;
default:
break;
}
return notificationDTO;
}
private String getFollowLink(String followingType, String id) {
StringBuilder url = new StringBuilder();
url.append(domain);
switch (followingType) {
case "0":
url = url.append("/user/").append(id);
break;
default:
url.append("/notification");
}
return url.toString();
}
private Author genAuthor(User user) {
Author author = new Author();
author.setUserNickname(user.getNickname());
author.setUserAvatarURL(user.getAvatarUrl());
author.setIdUser(user.getIdUser());
return author;
}
@Override @Override
public Notification findNotification(Integer idUser, Integer dataId, String dataType) { public Notification findNotification(Integer idUser, Integer dataId, String dataType) {
return notificationMapper.selectNotification(idUser, dataId, dataType); return notificationMapper.selectNotification(idUser, dataId, dataType);

View File

@ -1,15 +1,17 @@
package com.rymcu.forest.util; package com.rymcu.forest.util;
import com.rymcu.forest.core.constant.NotificationConstant; import com.rymcu.forest.core.constant.NotificationConstant;
import com.rymcu.forest.dto.ArticleDTO;
import com.rymcu.forest.dto.Author;
import com.rymcu.forest.dto.NotificationDTO;
import com.rymcu.forest.entity.Comment;
import com.rymcu.forest.entity.Follow; import com.rymcu.forest.entity.Follow;
import com.rymcu.forest.entity.Notification; import com.rymcu.forest.entity.Notification;
import com.rymcu.forest.entity.User; import com.rymcu.forest.entity.User;
import com.rymcu.forest.service.FollowService; import com.rymcu.forest.service.*;
import com.rymcu.forest.service.NotificationService;
import com.rymcu.forest.service.UserService;
import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
/** /**
@ -19,12 +21,13 @@ import java.util.concurrent.*;
*/ */
public class NotificationUtils { public class NotificationUtils {
@Resource
private static NotificationService notificationService = SpringContextHolder.getBean(NotificationService.class); private static NotificationService notificationService = SpringContextHolder.getBean(NotificationService.class);
@Resource
private static UserService userService = SpringContextHolder.getBean(UserService.class); private static UserService userService = SpringContextHolder.getBean(UserService.class);
@Resource
private static FollowService followService = SpringContextHolder.getBean(FollowService.class); private static FollowService followService = SpringContextHolder.getBean(FollowService.class);
private static JavaMailService mailService = SpringContextHolder.getBean(JavaMailService.class);
private static ArticleService articleService = SpringContextHolder.getBean(ArticleService.class);
private static CommentService commentService = SpringContextHolder.getBean(CommentService.class);
public static void sendAnnouncement(Integer dataId, String dataType, String dataSummary) { public static void sendAnnouncement(Integer dataId, String dataType, String dataSummary) {
ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
@ -53,6 +56,11 @@ public class NotificationUtils {
// TODO 记录操作失败数据 // TODO 记录操作失败数据
} }
} }
if (NotificationConstant.Comment.equals(dataType)) {
notification = notificationService.findNotification(idUser, dataId, dataType);
NotificationDTO notificationDTO = genNotification(notification);
mailService.sendNotification(notificationDTO);
}
} catch (Exception ex) { } catch (Exception ex) {
// TODO 记录操作失败数据 // TODO 记录操作失败数据
ex.printStackTrace(); ex.printStackTrace();
@ -83,4 +91,82 @@ public class NotificationUtils {
return 0; return 0;
}, executor); }, executor);
} }
public static NotificationDTO genNotification(Notification notification) {
NotificationDTO notificationDTO = new NotificationDTO();
BeanCopierUtil.copy(notification, notificationDTO);
ArticleDTO article;
Comment comment;
User user;
Follow follow;
switch (notification.getDataType()) {
case "0":
// 系统公告/帖子
article = articleService.findArticleDTOById(notification.getDataId(), 0);
if (Objects.nonNull(article)) {
notificationDTO.setDataTitle("系统公告");
notificationDTO.setDataUrl(article.getArticlePermalink());
user = userService.findById(article.getArticleAuthorId().toString());
notificationDTO.setAuthor(genAuthor(user));
}
break;
case "1":
// 关注
follow = followService.findById(notification.getDataId().toString());
notificationDTO.setDataTitle("关注提醒");
if (Objects.nonNull(follow)) {
user = userService.findById(follow.getFollowerId().toString());
notificationDTO.setDataUrl(getFollowLink(follow.getFollowingType(), user.getNickname()));
notificationDTO.setAuthor(genAuthor(user));
}
break;
case "2":
// 回帖
comment = commentService.findById(notification.getDataId().toString());
article = articleService.findArticleDTOById(comment.getCommentArticleId(), 0);
if (Objects.nonNull(article)) {
notificationDTO.setDataTitle(article.getArticleTitle());
notificationDTO.setDataUrl(comment.getCommentSharpUrl());
user = userService.findById(comment.getCommentAuthorId().toString());
notificationDTO.setAuthor(genAuthor(user));
}
break;
case "3":
// 关注用户发布文章
case "4":
// 关注文章更新
article = articleService.findArticleDTOById(notification.getDataId(), 0);
if (Objects.nonNull(article)) {
notificationDTO.setDataTitle("关注通知");
notificationDTO.setDataUrl(article.getArticlePermalink());
user = userService.findById(article.getArticleAuthorId().toString());
notificationDTO.setAuthor(genAuthor(user));
}
break;
default:
break;
}
return notificationDTO;
}
private static String getFollowLink(String followingType, String id) {
StringBuilder url = new StringBuilder();
url.append(Utils.getProperty("resource.domain"));
switch (followingType) {
case "0":
url = url.append("/user/").append(id);
break;
default:
url.append("/notification");
}
return url.toString();
}
private static Author genAuthor(User user) {
Author author = new Author();
author.setUserNickname(user.getNickname());
author.setUserAvatarURL(user.getAvatarUrl());
author.setIdUser(user.getIdUser());
return author;
}
} }

View File

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title> RYMCU 消息通知 </title>
<!--[if !mso]> -->
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css2?family=Quattrocento:wght@400;700&amp;display=swap" rel="stylesheet"
type="text/css"/>
<style type="text/css">
@import url(https://fonts.googleapis.com/css2?family=Quattrocento:wght@400;700&amp;display=swap);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
<style type="text/css">
a,
span,
td,
th {
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
</style>
</head>
<body style="background-color:#f6ab0c;text-align: center;">
<div style="background-color:#ffffff;margin:0px auto;border-radius:8px 8px 0 0;max-width:600px;">
<div style="max-width:600px;">
<div style="margin-top: 40px;">
<table align="center">
<tbody>
<tr>
<td width="150" style="padding: 20px 0;">
<img alt="Logo" height="auto"
src="https://static.rymcu.com/article/1619013470064"
style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:14px;"
width="150">
</td>
</tr>
<tr>
<td width="200">
<img alt="welcome image" height="auto"
src="https://static.rymcu.com/article/1619013419068"
style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:14px;"
width="200"/>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<table>
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-size:18px;font-weight:400;line-height:24px;text-align:left;color:#000000;">
<h1 style="margin: 0; font-size: 32px; line-height: 40px; font-weight: 700;">
消息通知</h1>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-size:18px;font-weight:400;line-height:24px;text-align:left;color:#000000;">
<div style="margin: 0;">
<span th:text="${user}"></span> 评论了您的文章《 <span th:text="${articleTitle}"></span>》:
</div>
<div th:text="*{content}"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div style="margin:0px auto;max-width:600px;">
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody><tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Quattrocento;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#333333;">
<a th:href="${url}" style="color: #428dfc; text-decoration: none; font-weight: bold;"> 点击这里进行查看 </a>
</div>
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
</div>
</body>
</html>