diff --git a/pom.xml b/pom.xml index 0816680b..e20e83e1 100644 --- a/pom.xml +++ b/pom.xml @@ -282,6 +282,12 @@ itextpdf 5.5.13.2 + + + org.springframework.boot + spring-boot-starter-websocket + + diff --git a/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java b/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java index 0f61d591..bc6071c8 100644 --- a/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java +++ b/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java @@ -1,133 +1,134 @@ -package com.ai.da.common.security.filter; - -import cn.hutool.core.util.StrUtil; -import com.ai.da.common.context.UserContext; -import com.ai.da.common.security.config.SecurityProperties; -import com.ai.da.common.security.jwt.JWTTokenHelper; -import com.ai.da.common.utils.LocalCacheUtils; -import com.ai.da.common.utils.MultiReadHttpServletRequest; -import com.ai.da.common.utils.MultiReadHttpServletResponse; -import com.ai.da.common.utils.RequestInfoUtil; -import com.ai.da.model.vo.AuthPrincipalVo; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Configuration; -import org.springframework.lang.NonNull; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.StopWatch; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.annotation.Resource; -import javax.security.sasl.AuthenticationException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -/** - * @author: dangweijian - * @description: 认证拦截器 - * @create: 2020-07-10 16:50 - **/ -@Slf4j -@Configuration -public class AuthenticationFilter extends OncePerRequestFilter { - - @Resource - private JWTTokenHelper jwtTokenHelper; - @Resource - private SecurityProperties properties; - - private static final List FILTER_URL = - Arrays.asList("/favicon.ico", "/doc.html", "api/account/login", "api/account/preLogin", "api/account/sendEmail","api/account/noLoginRequired", - "/webjars/", "/swagger-resources", "/v2/api-docs", "api/account/resetPwd", - "/api/python/saveGeneratePicture", "/api/python/getLibraryByUserId", - "/api/third/party/addUser","/api/third/party/addTrialUser", "/api/third/party/editUser", "/api/element/initDefaultSysFile", - "/api/third/party/addNoLoginRequiredNew","/api/third/party/deleteNoLoginRequiredNew", - "/api/third/party/existNoLoginRequired","/api/third/party/getRedirectUrl", - "/api/python/flush","/api/account/healthy","/api/ali-pay/trade/notify","/api/paypal/ipn/back","/api/alipay-hk/trade/notify", - "/api/portfolio/page", "/api/portfolio/detail", "/api/portfolio/commentPage", "/api/portfolio/viewsIncrease", - "/api/account/designWorksRegister","/api/account/questionnaire","/api/stripe/trade/notify" - ); - - @Override - protected void doFilterInternal(HttpServletRequest httpServletRequest, @NonNull HttpServletResponse httpServletResponse, @NonNull FilterChain filterChain) throws ServletException, IOException { - String requestURI = httpServletRequest.getRequestURI(); - - if (calculateUrl(requestURI) || hasAuthorizationToken(httpServletRequest)) { - StopWatch stopWatch = new StopWatch(); - HttpServletRequest wrappedRequest = httpServletRequest; - HttpServletResponse wrappedResponse = httpServletResponse; - try { - stopWatch.start(); - if ((httpServletRequest.getContentType() == null && httpServletRequest.getContentLength() > 0) || (httpServletRequest.getContentType() != null && !httpServletRequest.getContentType().contains("application/json"))) { - extracted(wrappedRequest); - filterChain.doFilter(wrappedRequest, wrappedResponse); - } else { - wrappedRequest = new MultiReadHttpServletRequest(httpServletRequest); - wrappedResponse = new MultiReadHttpServletResponse(httpServletResponse); - extracted(wrappedRequest); - filterChain.doFilter(wrappedRequest, wrappedResponse); - } - } catch (Exception e) { - SecurityContextHolder.clearContext(); - throw e; - } finally { - stopWatch.stop(); - } - } else { - filterChain.doFilter(httpServletRequest, httpServletResponse); - } - } - - private Boolean calculateUrl(String requestURI) { - String filterUrl = FILTER_URL.stream().filter(url -> requestURI.contains(url)).findFirst().orElse(null); - return null == filterUrl ? Boolean.TRUE : Boolean.FALSE; - } - - private boolean hasAuthorizationToken(HttpServletRequest request) { - String authorizationHeader = request.getHeader("Authorization"); - return authorizationHeader != null && authorizationHeader.startsWith("Bearer"); - } - - private void extracted(HttpServletRequest request) throws AuthenticationException { - String jwtToken = request.getHeader(properties.getJwtTokenHeader()); -// log.debug("后台检查令牌:{}", jwtToken); - - if (StrUtil.isBlank(jwtToken)) { - String ipAddress = RequestInfoUtil.getIpAddress(request); - log.info("本次请求的ip为 : " + ipAddress); - throw new RuntimeException("请传入token!"); - } - if(jwtToken.equals("Bearer-eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoie1wiaWRcIjoyLFwidXNlcm5hbWVcIjpcImxpcnNcIn0iLCJpYXQiOjE2NjU3NDEwODcsImlzcyI6IkRXSiIsImF1dGhvcml0aWVzIjoiW10iLCJleHAiOjE2NzQzODEwODd9.ShM9R_NNFD7oo1OvxrEgg7PFeWinOuAKkuInUCMQupp66s64Hhv8tN0Wwr83nIN4rHPqtn95wmd4msWcvaFYJA")){ - //写死 暂时放行 - return; - } - // 检查token - boolean validate = jwtTokenHelper.validateToken(jwtToken); - if (validate) { - AuthPrincipalVo principal = jwtTokenHelper.parserToUser(jwtToken); - if (principal == null) { - throw new RuntimeException("TOKEN已过期,请重新登录!"); - } - //先清空当前线程变量,防止上一个线程遗留 - UserContext.delete(); - //存取用户信息到缓存 - UserContext.setUserHolder(principal); - //校验token - String cacheToken = LocalCacheUtils.getTokenCache(String.valueOf(principal.getId())); - - if(StringUtils.isEmpty(cacheToken)){ - throw new RuntimeException("TOKEN已过期,请重新登录!"); - } - if(!cacheToken.equals(jwtToken) ){ - throw new RuntimeException("TOKEN已过期,请重新登录!"); - } -// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(null, null); -// SecurityContextHolder.getContext().setAuthentication(authentication); - } - } -} +package com.ai.da.common.security.filter; + +import cn.hutool.core.util.StrUtil; +import com.ai.da.common.context.UserContext; +import com.ai.da.common.security.config.SecurityProperties; +import com.ai.da.common.security.jwt.JWTTokenHelper; +import com.ai.da.common.utils.LocalCacheUtils; +import com.ai.da.common.utils.MultiReadHttpServletRequest; +import com.ai.da.common.utils.MultiReadHttpServletResponse; +import com.ai.da.common.utils.RequestInfoUtil; +import com.ai.da.model.vo.AuthPrincipalVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StopWatch; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.security.sasl.AuthenticationException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * @author: dangweijian + * @description: 认证拦截器 + * @create: 2020-07-10 16:50 + **/ +@Slf4j +@Configuration +public class AuthenticationFilter extends OncePerRequestFilter { + + @Resource + private JWTTokenHelper jwtTokenHelper; + @Resource + private SecurityProperties properties; + + private static final List FILTER_URL = + Arrays.asList("/favicon.ico", "/doc.html", "api/account/login", "api/account/preLogin", "api/account/sendEmail","api/account/noLoginRequired", + "/webjars/", "/swagger-resources", "/v2/api-docs", "api/account/resetPwd", + "/api/python/saveGeneratePicture", "/api/python/getLibraryByUserId", + "/api/third/party/addUser","/api/third/party/addTrialUser", "/api/third/party/editUser", "/api/element/initDefaultSysFile", + "/api/third/party/addNoLoginRequiredNew","/api/third/party/deleteNoLoginRequiredNew", + "/api/third/party/existNoLoginRequired","/api/third/party/getRedirectUrl", + "/api/python/flush","/api/account/healthy","/api/ali-pay/trade/notify","/api/paypal/ipn/back","/api/alipay-hk/trade/notify", + "/api/portfolio/page", "/api/portfolio/detail", "/api/portfolio/commentPage", "/api/portfolio/viewsIncrease", + "/api/account/designWorksRegister","/api/account/questionnaire","/api/stripe/trade/notify", + "/notification" + ); + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, @NonNull HttpServletResponse httpServletResponse, @NonNull FilterChain filterChain) throws ServletException, IOException { + String requestURI = httpServletRequest.getRequestURI(); + + if (calculateUrl(requestURI) || hasAuthorizationToken(httpServletRequest)) { + StopWatch stopWatch = new StopWatch(); + HttpServletRequest wrappedRequest = httpServletRequest; + HttpServletResponse wrappedResponse = httpServletResponse; + try { + stopWatch.start(); + if ((httpServletRequest.getContentType() == null && httpServletRequest.getContentLength() > 0) || (httpServletRequest.getContentType() != null && !httpServletRequest.getContentType().contains("application/json"))) { + extracted(wrappedRequest); + filterChain.doFilter(wrappedRequest, wrappedResponse); + } else { + wrappedRequest = new MultiReadHttpServletRequest(httpServletRequest); + wrappedResponse = new MultiReadHttpServletResponse(httpServletResponse); + extracted(wrappedRequest); + filterChain.doFilter(wrappedRequest, wrappedResponse); + } + } catch (Exception e) { + SecurityContextHolder.clearContext(); + throw e; + } finally { + stopWatch.stop(); + } + } else { + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + } + + private Boolean calculateUrl(String requestURI) { + String filterUrl = FILTER_URL.stream().filter(url -> requestURI.contains(url)).findFirst().orElse(null); + return null == filterUrl ? Boolean.TRUE : Boolean.FALSE; + } + + private boolean hasAuthorizationToken(HttpServletRequest request) { + String authorizationHeader = request.getHeader("Authorization"); + return authorizationHeader != null && authorizationHeader.startsWith("Bearer"); + } + + private void extracted(HttpServletRequest request) throws AuthenticationException { + String jwtToken = request.getHeader(properties.getJwtTokenHeader()); +// log.debug("后台检查令牌:{}", jwtToken); + + if (StrUtil.isBlank(jwtToken)) { + String ipAddress = RequestInfoUtil.getIpAddress(request); + log.info("本次请求的ip为 : " + ipAddress); + throw new RuntimeException("请传入token!"); + } + if(jwtToken.equals("Bearer-eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoie1wiaWRcIjoyLFwidXNlcm5hbWVcIjpcImxpcnNcIn0iLCJpYXQiOjE2NjU3NDEwODcsImlzcyI6IkRXSiIsImF1dGhvcml0aWVzIjoiW10iLCJleHAiOjE2NzQzODEwODd9.ShM9R_NNFD7oo1OvxrEgg7PFeWinOuAKkuInUCMQupp66s64Hhv8tN0Wwr83nIN4rHPqtn95wmd4msWcvaFYJA")){ + //写死 暂时放行 + return; + } + // 检查token + boolean validate = jwtTokenHelper.validateToken(jwtToken); + if (validate) { + AuthPrincipalVo principal = jwtTokenHelper.parserToUser(jwtToken); + if (principal == null) { + throw new RuntimeException("TOKEN已过期,请重新登录!"); + } + //先清空当前线程变量,防止上一个线程遗留 + UserContext.delete(); + //存取用户信息到缓存 + UserContext.setUserHolder(principal); + //校验token + String cacheToken = LocalCacheUtils.getTokenCache(String.valueOf(principal.getId())); + + if(StringUtils.isEmpty(cacheToken)){ + throw new RuntimeException("TOKEN已过期,请重新登录!"); + } + if(!cacheToken.equals(jwtToken) ){ + throw new RuntimeException("TOKEN已过期,请重新登录!"); + } +// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(null, null); +// SecurityContextHolder.getContext().setAuthentication(authentication); + } + } +} diff --git a/src/main/java/com/ai/da/common/websocket/NotificationConnection.java b/src/main/java/com/ai/da/common/websocket/NotificationConnection.java new file mode 100644 index 00000000..f9a9cb52 --- /dev/null +++ b/src/main/java/com/ai/da/common/websocket/NotificationConnection.java @@ -0,0 +1,43 @@ +package com.ai.da.common.websocket; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +@ServerEndpoint(value = "/notification") +@Component +@Slf4j +public class NotificationConnection { + + static Map sessionMap = new ConcurrentHashMap<>(); + //连接建立时执行的操作 + @OnOpen + public void onOpen(Session session){ + sessionMap.put(session.getId(),session); + log.info("websocket is open"); + } + //收到了客户端消息执行的操作 + @OnMessage + public void onMessage(String text){ + log.info("收到了一条消息:"+text); +// return "已收到你的消息"; + } + //连接关闭的时候执行的操作 + @OnClose + public void onClose(Session session){ + sessionMap.remove(session.getId()); + log.info("websocket is close"); + } + + public void sendMsg(String message) throws IOException { + for(String key:sessionMap.keySet()){ + sessionMap.get(key).getBasicRemote().sendText(message); + } + } +} diff --git a/src/main/java/com/ai/da/common/websocket/config/WebSocketConfig.java b/src/main/java/com/ai/da/common/websocket/config/WebSocketConfig.java new file mode 100644 index 00000000..df5b252b --- /dev/null +++ b/src/main/java/com/ai/da/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,18 @@ +package com.ai.da.common.websocket.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * Configuration of WebSocket + * + * @author db1995 + */ +@Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/src/main/java/com/ai/da/controller/MessageCenterController.java b/src/main/java/com/ai/da/controller/MessageCenterController.java new file mode 100644 index 00000000..385c7557 --- /dev/null +++ b/src/main/java/com/ai/da/controller/MessageCenterController.java @@ -0,0 +1,56 @@ +package com.ai.da.controller; + +import com.ai.da.common.response.PageBaseResponse; +import com.ai.da.common.response.Response; +import com.ai.da.model.vo.GetNotificationVO; +import com.ai.da.model.vo.NotificationVO; +import com.ai.da.model.vo.PublishSysNotificationVO; +import com.ai.da.service.MessageCenterService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +@Api(tags = "消息中心模块") +@Slf4j +@RestController +@RequestMapping("/api/message") +public class MessageCenterController { + + @Resource + private MessageCenterService messageCenterService; + + // 获取未读消息总数 + @ApiOperation(value = "获取未读消息数") + @GetMapping("/getUnreadCount") + public Response> getUnreadMessage(){ + return Response.success(messageCenterService.getAllTypeMessageUnreadCount()); + } + + // 获取历史消息 + @ApiOperation(value = "获取历史消息") + @PostMapping("/getHistoryNotification") + public Response> getHistoryNotification(@Valid @RequestBody GetNotificationVO getNotificationVO) { + return Response.success(messageCenterService.getHistoryNotification(getNotificationVO)); + } + + // 已读消息 + @ApiOperation(value = "设置消息状态为已读") + @PostMapping("/setReadStatus") + public Response setReadStatus(@RequestParam("notificationIdList") List notificationIdList, @RequestParam("type") String type) { + return Response.success(messageCenterService.setReadStatus(notificationIdList, type)); + } + + // 发布系统消息 + @ApiOperation(value = "发布系统消息") + @PostMapping("/publishSysMessage") + public Response publishSysMessage(@Valid @RequestBody PublishSysNotificationVO message) { + messageCenterService.publishSystemNotification(message); + return Response.success("success"); + } +} diff --git a/src/main/java/com/ai/da/mapper/primary/NotificationMapper.java b/src/main/java/com/ai/da/mapper/primary/NotificationMapper.java new file mode 100644 index 00000000..54427ace --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/NotificationMapper.java @@ -0,0 +1,20 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.common.config.mybatis.plus.CommonMapper; +import com.ai.da.mapper.primary.entity.Notification; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public interface NotificationMapper extends CommonMapper { + + List> getTypeCount(Long receiverId); + + /** 解决mybatis-plus自动过滤 is_deleted为1的数据 问题 */ + Notification getUniqueLikeAndFollow(String type, Long senderId, Long receiverId, Long portfolioId); + + void updateUniqueLikeAndFollow(Long id, LocalDateTime time); + + void deleteNotification(Long id, LocalDateTime time); +} diff --git a/src/main/java/com/ai/da/mapper/primary/SysNotificationReadStatusMapper.java b/src/main/java/com/ai/da/mapper/primary/SysNotificationReadStatusMapper.java new file mode 100644 index 00000000..fedf035f --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/SysNotificationReadStatusMapper.java @@ -0,0 +1,7 @@ +package com.ai.da.mapper.primary; + +import com.ai.da.common.config.mybatis.plus.CommonMapper; +import com.ai.da.mapper.primary.entity.SysNotificationReadStatus; + +public interface SysNotificationReadStatusMapper extends CommonMapper { +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Account.java b/src/main/java/com/ai/da/mapper/primary/entity/Account.java index 8e39896b..285acd90 100644 --- a/src/main/java/com/ai/da/mapper/primary/entity/Account.java +++ b/src/main/java/com/ai/da/mapper/primary/entity/Account.java @@ -99,4 +99,9 @@ public class Account implements Serializable { * 4 : 参加活动获取30天有效期和6000个积分的用户 */ private Integer systemUser; + + /** + * 头像 + */ + private String avatar; } diff --git a/src/main/java/com/ai/da/mapper/primary/entity/Notification.java b/src/main/java/com/ai/da/mapper/primary/entity/Notification.java new file mode 100644 index 00000000..2e743a8f --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/Notification.java @@ -0,0 +1,67 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@TableName("t_notification") +@Data +public class Notification extends BaseEntity{ + /** + * 操作类型 system/like/comment/follow + */ + private String type; + /** + * 发起操作者用户id + */ + private Long senderId; + /** + * 被操作对象用户id + */ + private Long receiverId; + /** + * 点赞评论时的作品id + */ + private Long portfolioId; + /** + * 消息内容 + */ + private String content; + /** + * 评论id + */ + private Long commentId; + /** + * 个人消息已读状态 + */ + private Integer isRead; + /** + * 系统消息发布状态 + */ + private Integer publishFlag; + + /** + * 是否被删除 + */ + private Integer isDeleted; + + public Notification() { + } + + public Notification(String type, Long senderId, Long receiverId, Long portfolioId) { + this.type = type; + this.senderId = senderId; + this.receiverId = receiverId; + this.portfolioId = portfolioId; + } + + public Notification(String type, Long senderId, Long receiverId, Long portfolioId, String content, Long commentId) { + this.type = type; + this.senderId = senderId; + this.receiverId = receiverId; + this.portfolioId = portfolioId; + this.content = content; + this.commentId = commentId; + } +} diff --git a/src/main/java/com/ai/da/mapper/primary/entity/SysNotificationReadStatus.java b/src/main/java/com/ai/da/mapper/primary/entity/SysNotificationReadStatus.java new file mode 100644 index 00000000..4e0537dc --- /dev/null +++ b/src/main/java/com/ai/da/mapper/primary/entity/SysNotificationReadStatus.java @@ -0,0 +1,26 @@ +package com.ai.da.mapper.primary.entity; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@EqualsAndHashCode(callSuper = true) +@TableName("t_sys_notification_read_status") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SysNotificationReadStatus extends BaseEntity{ + /** + * 系统消息id + */ + private Long system_notification_id; + + /** + * 已读当前消息的用户id将被存储 + */ + private Long account_id; +} diff --git a/src/main/java/com/ai/da/model/vo/GetNotificationVO.java b/src/main/java/com/ai/da/model/vo/GetNotificationVO.java new file mode 100644 index 00000000..e241e163 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/GetNotificationVO.java @@ -0,0 +1,15 @@ +package com.ai.da.model.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@ApiModel +@Data +public class GetNotificationVO extends PageQueryBaseVo{ + + @ApiModelProperty("system/like/comment/follow") + private String type; +} \ No newline at end of file diff --git a/src/main/java/com/ai/da/model/vo/NotificationVO.java b/src/main/java/com/ai/da/model/vo/NotificationVO.java new file mode 100644 index 00000000..e6645661 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/NotificationVO.java @@ -0,0 +1,17 @@ +package com.ai.da.model.vo; + +import com.ai.da.mapper.primary.entity.Notification; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class NotificationVO extends Notification { + + private String senderUserName; + + private String senderUserAvatar; + + private String portfolioName; + +} diff --git a/src/main/java/com/ai/da/model/vo/PublishSysNotificationVO.java b/src/main/java/com/ai/da/model/vo/PublishSysNotificationVO.java new file mode 100644 index 00000000..1379aca0 --- /dev/null +++ b/src/main/java/com/ai/da/model/vo/PublishSysNotificationVO.java @@ -0,0 +1,19 @@ +package com.ai.da.model.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("发布系统消息") +public class PublishSysNotificationVO { + + @ApiModelProperty("系统消息标题") + private String title; + + @ApiModelProperty("系统消息内容") + private String content; + + @ApiModelProperty("系统消息 活动链接") + private String link; +} diff --git a/src/main/java/com/ai/da/python/PythonService.java b/src/main/java/com/ai/da/python/PythonService.java index 3dd32bb3..edb5c573 100644 --- a/src/main/java/com/ai/da/python/PythonService.java +++ b/src/main/java/com/ai/da/python/PythonService.java @@ -3349,6 +3349,7 @@ public class PythonService { } } catch (IOException e) { log.error("promptTranslation 用户输入翻译失败; error message => " + e.getMessage()); + response.close(); throw new RuntimeException(e); } finally { diff --git a/src/main/java/com/ai/da/service/MessageCenterService.java b/src/main/java/com/ai/da/service/MessageCenterService.java new file mode 100644 index 00000000..f4f26f43 --- /dev/null +++ b/src/main/java/com/ai/da/service/MessageCenterService.java @@ -0,0 +1,26 @@ +package com.ai.da.service; + +import com.ai.da.common.response.PageBaseResponse; +import com.ai.da.mapper.primary.entity.Notification; +import com.ai.da.model.vo.GetNotificationVO; +import com.ai.da.model.vo.NotificationVO; +import com.ai.da.model.vo.PublishSysNotificationVO; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; +import java.util.Map; + +public interface MessageCenterService extends IService { + + Map getAllTypeMessageUnreadCount(); + + PageBaseResponse getHistoryNotification(GetNotificationVO getNotificationVO); + + void prePushMessage(Notification notification); + + void cancelPushMessage(String type, Long senderId, Long receiverId, Long portfolioId, Long commentId); + + Boolean setReadStatus(List notificationIdList, String type); + + void publishSystemNotification(PublishSysNotificationVO message); +} diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index 3d102279..829ce33c 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -1024,7 +1024,7 @@ public class AccountServiceImpl extends ServiceImpl impl account.setIsTrial(1); account.setIsBeginner(1); account.setCreateDate(new Date()); - account.setCredits(BigDecimal.valueOf(500)); + account.setCredits(BigDecimal.valueOf(100)); accountMapper.insert(account); AccountLoginVO response = CopyUtil.copyObject(account, AccountLoginVO.class); response.setEmail(account.getUserEmail()); diff --git a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java index 46e68d41..95f17e90 100644 --- a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java @@ -744,7 +744,7 @@ public class GenerateServiceImpl extends ServiceImpl i redisUtil.addToString(key, new Gson().toJson(generateResultVO), CommonConstant.GENERATE_RESULT_EXPIRE_TIME); Long accountId = Long.parseLong(taskId.substring(taskId.lastIndexOf("-") + 1)); - if (!status.equals("Invalid")){ + if (!status.equals("Invalid")) { // 4、扣除积分 Boolean b = creditsService.taskCreditsDeduction(accountId, taskId); // 3、记录积分变更 diff --git a/src/main/java/com/ai/da/service/impl/MessageCenterServiceImpl.java b/src/main/java/com/ai/da/service/impl/MessageCenterServiceImpl.java new file mode 100644 index 00000000..114ac00c --- /dev/null +++ b/src/main/java/com/ai/da/service/impl/MessageCenterServiceImpl.java @@ -0,0 +1,243 @@ +package com.ai.da.service.impl; + +import com.ai.da.common.constant.CommonConstant; +import com.ai.da.common.context.UserContext; +import com.ai.da.common.response.PageBaseResponse; +import com.ai.da.common.utils.CopyUtil; +import com.ai.da.common.utils.MinioUtil; +import com.ai.da.common.websocket.NotificationConnection; +import com.ai.da.mapper.primary.NotificationMapper; +import com.ai.da.mapper.primary.SysNotificationReadStatusMapper; +import com.ai.da.mapper.primary.entity.Account; +import com.ai.da.mapper.primary.entity.Notification; +import com.ai.da.mapper.primary.entity.SysNotificationReadStatus; +import com.ai.da.model.vo.GetNotificationVO; +import com.ai.da.model.vo.NotificationVO; +import com.ai.da.model.vo.PublishSysNotificationVO; +import com.ai.da.service.AccountService; +import com.ai.da.service.MessageCenterService; +import com.ai.da.service.PortfolioService; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.gson.Gson; +import com.mysql.cj.util.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + + +@Service +@Slf4j +public class MessageCenterServiceImpl extends ServiceImpl implements MessageCenterService { + + @Resource + private NotificationConnection notificationConnection; + @Resource + private SysNotificationReadStatusMapper sysNotificationReadStatusMapper; + @Resource + private AccountService accountService; + @Resource + private MinioUtil minioUtil; + @Resource + private PortfolioService portfolioService; + + + @Override + public Map getAllTypeMessageUnreadCount() { + List> typeCount = baseMapper.getTypeCount(UserContext.getUserHolder().getId()); + Map msgTypeCount = typeCount.stream() + .collect(Collectors.toMap( + map -> (String) map.get("type"), + map -> Objects.isNull(map.get("count")) ? 0L : (Long) map.get("count"))); + msgTypeCount.put("system", getUnreadSystemNotification()); + log.info(msgTypeCount.toString()); + // 整理数据 加上系统消息未读数 + return msgTypeCount; + } + + + // 获取历史消息 可指定消息类型 分页查询 + @Override + public PageBaseResponse getHistoryNotification(GetNotificationVO getNotificationVO) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isNullOrEmpty(getNotificationVO.getType())) { + queryWrapper.eq("type", getNotificationVO.getType()); + } + queryWrapper.eq("receiver_id", UserContext.getUserHolder().getId()); + Page notificationPage = baseMapper.selectPage(new Page<>(getNotificationVO.getPage(), getNotificationVO.getSize()), queryWrapper); + IPage convert = notificationPage.convert(o -> { + NotificationVO notificationVO = CopyUtil.copyObject(o, NotificationVO.class); + Account account = accountService.getById(notificationVO.getSenderId()); + notificationVO.setSenderUserName(account.getUserName()); + notificationVO.setSenderUserAvatar(StringUtils.isNullOrEmpty(account.getAvatar()) ? null : minioUtil.getPreSignedUrl(account.getAvatar(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + notificationVO.setPortfolioName(portfolioService.getById(notificationVO.getPortfolioId()).getPortfolioName()); + return notificationVO; + }); + + return PageBaseResponse.success(convert); + } + + public void prePushMessage(Notification notification){ + // 先判断点赞、关注记录是不是唯一 + if (!getUniqueLikeAndFollow(notification)){ + // 1、将数据存数据库 + if (!notification.getType().equals("system")) { + notification.setIsRead(0); + } + notification.setIsDeleted(0); + notification.setCreateTime(LocalDateTime.now()); + save(notification); + } + // 推送消息 + pushMessage(notification.getType(), notification.getSenderId()); + } + + + /** + * 只记录唯一点赞和关注 + * 重复点赞、关注只会记录最新的一次操作 + * @param notification + * @return + */ + public Boolean getUniqueLikeAndFollow(Notification notification){ + Notification existNotification= null; + // 对单个作品的点赞和对某个人的关注 具有唯一性 + if (notification.getType().equals("like") || notification.getType().equals("follow")){ + existNotification = baseMapper.getUniqueLikeAndFollow(notification.getType(), notification.getSenderId(), + notification.getReceiverId(), notification.getPortfolioId()); + } + + if (!Objects.isNull(existNotification)){ + // 有记录,则更新 1、删除状态 为0 2、已读状态 为0 3、创建时间 为最新时间 4、更新时间 为最新时间 + baseMapper.updateUniqueLikeAndFollow(existNotification.getId(), LocalDateTime.now()); + return Boolean.TRUE; + } + + return Boolean.FALSE; + } + + + // 消息推送 只需要返回当前操作类型和该操作未读消息数量 + public void pushMessage(String type, Long senderId) { + // 推送消息到前端 + ArrayList> resp = new ArrayList<>(); + HashMap data = new HashMap<>(); + Long count; + if (!type.equals("system")) { + // 个人未读消息 + count = getUnreadCountByType(type, senderId); + } else { + // 系统未读消息 + count = getUnreadSystemNotification(); + } + + data.put(type, count); + resp.add(data); + String jsonString = JSON.toJSONString(resp); + log.info("消息推送 : {}", jsonString); + + try { + notificationConnection.sendMsg(jsonString); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 取消点赞、删除评论、取消关注 + public void cancelPushMessage(String type, Long senderId, Long receiverId, Long portfolioId, Long commentId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("type", type) + .eq("sender_id", senderId) + .eq("receiver_id", receiverId) + .eq("is_deleted", 0); + + if (!type.equals("follow")){ + queryWrapper.eq("portfolio_id", portfolioId); + } + if (type.equals("comment")) { + queryWrapper.eq("comment_id", commentId); + } + Notification notification = baseMapper.selectOne(queryWrapper); + if (!Objects.isNull(notification)) { + baseMapper.deleteNotification(notification.getId(), LocalDateTime.now()); + } + // 推送消息 + pushMessage(type, senderId); + } + + // 获取个人指定消息类型未读消息数量 + private Long getUnreadCountByType(String type, Long receiverId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("type", type) + .eq("is_read", 0) + .eq("receiver_id", receiverId); + + return baseMapper.selectCount(queryWrapper); + } + + private Long getUnreadSystemNotification() { + // 计算总的系统通知数量 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("type", "system"); + Long totalSysCount = baseMapper.selectCount(queryWrapper); + + // 计算单个用户读了多少条系统数据 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("account_id", UserContext.getUserHolder().getId()); + Long readCount = sysNotificationReadStatusMapper.selectCount(wrapper); + + // 计算差 + return totalSysCount - readCount; + } + + // 设置个人消息的已读状态 (允许一次已读多条个人消息) + public Boolean setReadStatus(List notificationIdList, String type) { + + if (type.equals("system")) { + setReadStatusSystem(notificationIdList); + } else { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.in("id", notificationIdList) +// .eq("type", type) + .set("is_read", 1) + .set("update_time", LocalDateTime.now()); + + baseMapper.update(null, updateWrapper); + } + return Boolean.TRUE; + + } + + // 设置系统消息的已读状态 (允许一次已读多条系统消息) + public void setReadStatusSystem(List notificationIdList) { + Long id = UserContext.getUserHolder().getId(); + for (Long notificationId : notificationIdList) { + SysNotificationReadStatus sysNotificationReadStatus = new SysNotificationReadStatus(notificationId, id); + sysNotificationReadStatus.setCreateTime(LocalDateTime.now()); + sysNotificationReadStatusMapper.insert(sysNotificationReadStatus); + } + } + + // todo 全部已读 + + // 发布系统消息 + public void publishSystemNotification(PublishSysNotificationVO message) { + Notification notification = new Notification(); + notification.setType("system"); + notification.setSenderId(UserContext.getUserHolder().getId()); + notification.setContent(new Gson().toJson(message)); + // todo 是否需要定时发送系统通知 + prePushMessage(notification); + } + + +} diff --git a/src/main/java/com/ai/da/service/impl/PortfolioServiceImpl.java b/src/main/java/com/ai/da/service/impl/PortfolioServiceImpl.java index a002c37d..5ca97162 100644 --- a/src/main/java/com/ai/da/service/impl/PortfolioServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/PortfolioServiceImpl.java @@ -25,7 +25,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; -import sun.security.krb5.internal.crypto.Des; import javax.annotation.Resource; import java.math.BigDecimal; @@ -795,10 +794,16 @@ public class PortfolioServiceImpl extends ServiceImpl + + + + + + + + + UPDATE `t_notification` + SET + is_read = 0, + create_time = #{time}, + update_time = #{time}, + is_deleted = 0 + WHERE id = #{id} + + + + UPDATE `t_notification` + SET + update_time = #{time}, + is_deleted = 1 + WHERE id = #{id} + + +