mirror of
https://github.com/zongzibinbin/MallChat.git
synced 2026-03-14 06:03:42 +08:00
项目基础搭建
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package com.abin.mallchat.custom;
|
||||
|
||||
import com.abin.mallchat.custom.user.controller.WxPortalController;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||
|
||||
/**
|
||||
* @author zhongzb
|
||||
* @date 2021/05/27
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.abin.mallchat"})
|
||||
@MapperScan({"com.abin.mallchat.common.**.mapper" })
|
||||
@ServletComponentScan
|
||||
public class MallchatCustomApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MallchatCustomApplication.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.abin.mallchat.custom.chat.controller;
|
||||
|
||||
|
||||
import com.abin.mallchat.common.common.annotation.FrequencyControl;
|
||||
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.IdRespVO;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageMarkReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessagePageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberStatisticResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatRoomResp;
|
||||
import com.abin.mallchat.custom.chat.service.ChatService;
|
||||
import com.abin.mallchat.common.common.utils.RequestHolder;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 群聊相关接口
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/capi/chat")
|
||||
@Api(tags = "聊天室相关接口")
|
||||
public class ChatController {
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
@GetMapping("/public/room/page")
|
||||
@ApiOperation("会话列表")
|
||||
public ApiResult<CursorPageBaseResp<ChatRoomResp>> getRoomPage(CursorPageBaseReq request) {
|
||||
return ApiResult.success(chatService.getRoomPage(request, RequestHolder.get().getUid()));
|
||||
}
|
||||
@GetMapping("/public/member/page")
|
||||
@ApiOperation("群成员列表")
|
||||
public ApiResult<CursorPageBaseResp<ChatMemberResp>> getMemberPage(CursorPageBaseReq request) {
|
||||
return ApiResult.success(chatService.getMemberPage(request));
|
||||
}
|
||||
|
||||
@GetMapping("/public/member/statistic")
|
||||
@ApiOperation("群成员人数统计")
|
||||
public ApiResult<ChatMemberStatisticResp> getMemberStatistic() {
|
||||
return ApiResult.success(chatService.getMemberStatistic());
|
||||
}
|
||||
|
||||
@GetMapping("/public/msg/page")
|
||||
@ApiOperation("消息列表")
|
||||
public ApiResult<CursorPageBaseResp<ChatMessageResp>> getMsgPage(ChatMessagePageReq request) {
|
||||
return ApiResult.success(chatService.getMsgPage(request, RequestHolder.get().getUid()));
|
||||
}
|
||||
|
||||
@PostMapping("/msg")
|
||||
@ApiOperation("发送消息")
|
||||
@FrequencyControl(time = 5,count = 2)
|
||||
@FrequencyControl(time = 30,count = 5)
|
||||
@FrequencyControl(time = 60,count = 10)
|
||||
public ApiResult<IdRespVO> sendMsg(@Valid @RequestBody ChatMessageReq request) {
|
||||
return ApiResult.success(IdRespVO.id(chatService.sendMsg(request, RequestHolder.get().getUid())));
|
||||
}
|
||||
|
||||
@PutMapping("/msg/mark")
|
||||
@ApiOperation("消息标记")
|
||||
public ApiResult<Void> setMsgMark(@Valid @RequestBody ChatMessageMarkReq request) {//分布式锁
|
||||
chatService.setMsgMark(RequestHolder.get().getUid(),request);
|
||||
return ApiResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.request;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Description: 消息标记请求
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-29
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMessageMarkReq{
|
||||
@NotNull
|
||||
@ApiModelProperty("消息id")
|
||||
private Long msgId;
|
||||
|
||||
@NotNull
|
||||
@ApiModelProperty("标记类型 1点赞 2举报")
|
||||
private Integer markType;
|
||||
|
||||
@NotNull
|
||||
@ApiModelProperty("动作类型 1确认 2取消")
|
||||
private Integer actType;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.request;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Description: 消息列表请求
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-29
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMessagePageReq extends CursorPageBaseReq {
|
||||
@NotNull
|
||||
@ApiModelProperty("会话id")
|
||||
private Long roomId;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.request;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
|
||||
/**
|
||||
* Description: 消息发送请求体
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMessageReq {
|
||||
|
||||
@NotNull
|
||||
@Length( max = 10000,message = "消息内容过长,服务器扛不住啊,兄dei")
|
||||
@ApiModelProperty("消息内容")
|
||||
private String content;
|
||||
|
||||
@NotNull
|
||||
@ApiModelProperty("会话id")
|
||||
private Long roomId;
|
||||
|
||||
@ApiModelProperty("回复的消息id,如果没有别传就好")
|
||||
private Long replyMsgId;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.response;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Description: 群成员列表的成员信息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMemberResp {
|
||||
@ApiModelProperty("uid")
|
||||
private Long uid;
|
||||
@ApiModelProperty("用户名称")
|
||||
private String name;
|
||||
@ApiModelProperty("头像")
|
||||
private String avatar;
|
||||
/**
|
||||
* @see com.abin.mallchat.common.user.domain.enums.ChatActiveStatusEnum
|
||||
*/
|
||||
@ApiModelProperty("在线状态 1在线 2离线")
|
||||
private Integer activeStatus;
|
||||
@ApiModelProperty("最后一次上下线时间")
|
||||
private Date lastOptTime;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.response;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Description: 群成员统计信息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMemberStatisticResp {
|
||||
|
||||
@ApiModelProperty("在线人数")
|
||||
private Long onlineNum;//在线人数
|
||||
@ApiModelProperty("总人数")
|
||||
private Long totalNum;//总人数
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.response;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Description: 消息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatMessageResp {
|
||||
|
||||
@ApiModelProperty("发送者信息")
|
||||
private UserInfo fromUser;
|
||||
@ApiModelProperty("消息详情")
|
||||
private Message message;
|
||||
|
||||
@Data
|
||||
public static class UserInfo {
|
||||
@ApiModelProperty("用户名称")
|
||||
private String username;
|
||||
@ApiModelProperty("用户id")
|
||||
private Long uid;
|
||||
@ApiModelProperty("头像")
|
||||
private String avatar;
|
||||
@ApiModelProperty("徽章标识,如果没有展示null")
|
||||
private Badge badge;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Message {
|
||||
@ApiModelProperty("消息id")
|
||||
private Long id;
|
||||
@ApiModelProperty("消息发送时间")
|
||||
private Date sendTime;
|
||||
@ApiModelProperty("消息内容")
|
||||
private String content;
|
||||
@ApiModelProperty("消息类型 1正常文本 2.爆赞 (点赞超过10)3.危险发言(举报超5)")
|
||||
private Integer type;
|
||||
@ApiModelProperty("消息标记")
|
||||
private MessageMark messageMark;
|
||||
@ApiModelProperty("父消息,如果没有父消息,返回的是null")
|
||||
private ReplyMsg reply;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ReplyMsg {
|
||||
@ApiModelProperty("消息id")
|
||||
private Long id;
|
||||
@ApiModelProperty("用户名称")
|
||||
private String username;
|
||||
@ApiModelProperty("消息内容")
|
||||
private String content;
|
||||
@ApiModelProperty("是否可消息跳转 0否 1是")
|
||||
private Integer canCallback;
|
||||
@ApiModelProperty("跳转间隔的消息条数")
|
||||
private Integer gapCount;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MessageMark {
|
||||
@ApiModelProperty("点赞数")
|
||||
private Integer likeCount;
|
||||
@ApiModelProperty("该用户是否已经点赞 0否 1是")
|
||||
private Integer userLike;
|
||||
@ApiModelProperty("举报数")
|
||||
private Integer dislikeCount;
|
||||
@ApiModelProperty("该用户是否已经举报 0否 1是")
|
||||
private Integer userDislike;
|
||||
}
|
||||
@Data
|
||||
private static class Badge {
|
||||
@ApiModelProperty("徽章图像")
|
||||
private String img;
|
||||
@ApiModelProperty("徽章说明")
|
||||
private String describe;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.response;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Description: 群成员列表的成员信息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatRoomResp {
|
||||
@ApiModelProperty("会话id")
|
||||
private Long id;
|
||||
@ApiModelProperty("会话名称")
|
||||
private String name;
|
||||
@ApiModelProperty("会话类型 1大群聊 2沸点")
|
||||
private Integer type;
|
||||
@ApiModelProperty("房间最后活跃时间")
|
||||
private Date lastActiveTime;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.abin.mallchat.custom.chat.service;
|
||||
|
||||
import com.abin.mallchat.common.chat.dao.MessageDao;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageMarkReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessagePageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberStatisticResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatRoomResp;
|
||||
import com.abin.mallchat.custom.chat.service.adapter.MessageAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Description: 消息处理类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-26
|
||||
*/
|
||||
public interface ChatService {
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
Long sendMsg(ChatMessageReq request, Long uid);
|
||||
|
||||
/**
|
||||
* 根据消息获取消息前端展示的物料
|
||||
* @param message
|
||||
* @param receiveUid 接受消息的uid,可null
|
||||
* @return
|
||||
*/
|
||||
ChatMessageResp getMsgResp(Message message,Long receiveUid);
|
||||
|
||||
/**
|
||||
* 获取群成员列表
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
CursorPageBaseResp<ChatMemberResp> getMemberPage(CursorPageBaseReq request);
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
CursorPageBaseResp<ChatMessageResp> getMsgPage(ChatMessagePageReq request,@Nullable Long receiveUid);
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
* @param request
|
||||
* @param uid
|
||||
* @return
|
||||
*/
|
||||
CursorPageBaseResp<ChatRoomResp> getRoomPage(CursorPageBaseReq request, Long uid);
|
||||
|
||||
ChatMemberStatisticResp getMemberStatistic();
|
||||
|
||||
void setMsgMark(Long uid, ChatMessageMarkReq request);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.abin.mallchat.custom.chat.service.adapter;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.lang.Pair;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.enums.ChatActiveStatusEnum;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: 成员适配器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-26
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MemberAdapter {
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
|
||||
public List<ChatMemberResp> buildMember(List<Pair<Long, Double>> list, ChatActiveStatusEnum statusEnum) {
|
||||
return list.stream().map(a -> {
|
||||
ChatMemberResp resp = new ChatMemberResp();
|
||||
resp.setActiveStatus(statusEnum.getStatus());
|
||||
resp.setLastOptTime(new Date(a.getValue().longValue()));
|
||||
resp.setUid(a.getKey());
|
||||
User userInfo = userCache.getUserInfo(a.getKey());
|
||||
resp.setName(userInfo.getName());
|
||||
resp.setAvatar(userInfo.getAvatar());
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.abin.mallchat.custom.chat.service.adapter;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.entity.MessageMark;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageTypeEnum;
|
||||
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.sun.org.apache.regexp.internal.RE;
|
||||
import org.yaml.snakeyaml.error.Mark;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: 消息适配器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-26
|
||||
*/
|
||||
public class MessageAdapter {
|
||||
public static final int CAN_CALLBACK_GAP_COUNT = 100;
|
||||
|
||||
public static Message buildMsgSave(ChatMessageReq request, Long uid) {
|
||||
return Message.builder()
|
||||
.replyMsgId(request.getReplyMsgId())
|
||||
.content(request.getContent())
|
||||
.fromUid(uid)
|
||||
.roomId(request.getRoomId())
|
||||
.status(MessageStatusEnum.NORMAL.getStatus())
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
public static List<ChatMessageResp> buildMsgResp(List<Message> messages, Map<Long, Message> replyMap, Map<Long, User> userMap, List<MessageMark> msgMark, Long receiveUid) {
|
||||
Map<Long, List<MessageMark>> markMap = msgMark.stream().collect(Collectors.groupingBy(MessageMark::getMsgId));
|
||||
return messages.stream().map(a -> {
|
||||
ChatMessageResp resp = new ChatMessageResp();
|
||||
resp.setFromUser(buildFromUser(userMap.get(a.getFromUid())));
|
||||
resp.setMessage(buildMessage(a, replyMap, userMap, markMap.getOrDefault(a.getId(), new ArrayList<>()), receiveUid));
|
||||
return resp;
|
||||
})
|
||||
.sorted(Comparator.comparing(a -> a.getMessage().getSendTime()))//帮前端排好序,更方便它展示
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static ChatMessageResp.Message buildMessage(Message message, Map<Long, Message> replyMap, Map<Long, User> userMap, List<MessageMark> marks, Long receiveUid) {
|
||||
ChatMessageResp.Message messageVO = new ChatMessageResp.Message();
|
||||
BeanUtil.copyProperties(message, messageVO);
|
||||
messageVO.setSendTime(message.getCreateTime());
|
||||
Message replyMessage = replyMap.get(message.getReplyMsgId());
|
||||
//回复消息
|
||||
if (Objects.nonNull(replyMessage)) {
|
||||
ChatMessageResp.ReplyMsg replyMsgVO = new ChatMessageResp.ReplyMsg();
|
||||
replyMsgVO.setId(replyMessage.getId());
|
||||
replyMsgVO.setContent(replyMessage.getContent());
|
||||
User replyUser = userMap.get(replyMessage.getFromUid());
|
||||
replyMsgVO.setUsername(replyUser.getName());
|
||||
replyMsgVO.setCanCallback(YesOrNoEnum.toStatus(Objects.nonNull(message.getGapCount()) && message.getGapCount() <= CAN_CALLBACK_GAP_COUNT));
|
||||
replyMsgVO.setGapCount(message.getGapCount());
|
||||
messageVO.setReply(replyMsgVO);
|
||||
}
|
||||
//消息标记
|
||||
messageVO.setMessageMark(buildMsgMark(marks, receiveUid));
|
||||
return messageVO;
|
||||
}
|
||||
|
||||
private static ChatMessageResp.MessageMark buildMsgMark(List<MessageMark> marks, Long receiveUid) {
|
||||
Map<Integer, List<MessageMark>> typeMap = marks.stream().collect(Collectors.groupingBy(MessageMark::getType));
|
||||
List<MessageMark> likeMarks = typeMap.getOrDefault(MessageMarkTypeEnum.LIKE.getType(), new ArrayList<>());
|
||||
List<MessageMark> dislikeMarks = typeMap.getOrDefault(MessageMarkTypeEnum.DISLIKE.getType(), new ArrayList<>());
|
||||
ChatMessageResp.MessageMark mark = new ChatMessageResp.MessageMark();
|
||||
mark.setLikeCount(likeMarks.size());
|
||||
mark.setUserLike(Optional.ofNullable(receiveUid).filter(uid -> likeMarks.stream().anyMatch(a -> a.getUid().equals(uid))).map(a -> YesOrNoEnum.YES.getStatus()).orElse(YesOrNoEnum.NO.getStatus()));
|
||||
mark.setDislikeCount(dislikeMarks.size());
|
||||
mark.setUserDislike(Optional.ofNullable(receiveUid).filter(uid -> dislikeMarks.stream().anyMatch(a -> a.getUid().equals(uid))).map(a -> YesOrNoEnum.YES.getStatus()).orElse(YesOrNoEnum.NO.getStatus()));
|
||||
return mark;
|
||||
}
|
||||
|
||||
private static ChatMessageResp.UserInfo buildFromUser(User fromUser) {
|
||||
ChatMessageResp.UserInfo userInfo = new ChatMessageResp.UserInfo();
|
||||
userInfo.setUsername(fromUser.getName());
|
||||
userInfo.setAvatar(fromUser.getAvatar());
|
||||
userInfo.setUid(fromUser.getId());
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.abin.mallchat.custom.chat.service.adapter;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Room;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatRoomResp;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: 消息适配器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-26
|
||||
*/
|
||||
public class RoomAdapter {
|
||||
|
||||
|
||||
public static List<ChatRoomResp> buildResp(List<Room> list) {
|
||||
return list.stream()
|
||||
.map(a -> {
|
||||
ChatRoomResp resp = new ChatRoomResp();
|
||||
BeanUtil.copyProperties(a, resp);
|
||||
resp.setLastActiveTime(a.getActiveTime());
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.abin.mallchat.custom.chat.service.helper;
|
||||
|
||||
import cn.hutool.core.lang.Pair;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.user.domain.enums.ChatActiveStatusEnum;
|
||||
|
||||
/**
|
||||
* Description: 成员列表工具类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-28
|
||||
*/
|
||||
public class ChatMemberHelper {
|
||||
private static final String SEPARATOR = "_";
|
||||
|
||||
public static Pair<ChatActiveStatusEnum, String> getCursorPair(String cursor) {
|
||||
ChatActiveStatusEnum activeStatusEnum = ChatActiveStatusEnum.ONLINE;
|
||||
String timeCursor = null;
|
||||
if (StrUtil.isNotBlank(cursor)) {
|
||||
String activeStr = cursor.split(SEPARATOR)[0];
|
||||
String timeStr = cursor.split(SEPARATOR)[1];
|
||||
activeStatusEnum = ChatActiveStatusEnum.of(Integer.parseInt(activeStr));
|
||||
timeCursor = timeStr;
|
||||
}
|
||||
return Pair.of(activeStatusEnum, timeCursor);
|
||||
}
|
||||
|
||||
public static String generateCursor(ChatActiveStatusEnum activeStatusEnum, String timeCursor) {
|
||||
return activeStatusEnum.getStatus() + SEPARATOR + timeCursor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.abin.mallchat.custom.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.lang.Pair;
|
||||
import com.abin.mallchat.common.chat.dao.MessageDao;
|
||||
import com.abin.mallchat.common.chat.dao.MessageMarkDao;
|
||||
import com.abin.mallchat.common.chat.dao.RoomDao;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.entity.MessageMark;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Room;
|
||||
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
|
||||
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
|
||||
import com.abin.mallchat.common.common.exception.BusinessException;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.enums.ChatActiveStatusEnum;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageMarkReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessagePageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberStatisticResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatRoomResp;
|
||||
import com.abin.mallchat.custom.chat.service.adapter.MemberAdapter;
|
||||
import com.abin.mallchat.custom.chat.service.adapter.RoomAdapter;
|
||||
import com.abin.mallchat.custom.chat.service.helper.ChatMemberHelper;
|
||||
import com.abin.mallchat.custom.common.event.MessageMarkEvent;
|
||||
import com.abin.mallchat.custom.common.event.MessageSendEvent;
|
||||
import com.abin.mallchat.custom.chat.service.ChatService;
|
||||
import com.abin.mallchat.custom.chat.service.adapter.MessageAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Description: 消息处理类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-26
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ChatServiceImpl implements ChatService {
|
||||
public static final long ROOM_GROUP_ID = 1L;
|
||||
@Autowired
|
||||
private MessageDao messageDao;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
@Autowired
|
||||
private MemberAdapter memberAdapter;
|
||||
@Autowired
|
||||
private RoomDao roomDao;
|
||||
@Autowired
|
||||
private MessageMarkDao messageMarkDao;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Long sendMsg(ChatMessageReq request, Long uid) {
|
||||
//校验下回复消息
|
||||
Message replyMsg =null;
|
||||
if(Objects.nonNull(request.getReplyMsgId())){
|
||||
replyMsg = messageDao.getById(request.getReplyMsgId());
|
||||
AssertUtil.isNotEmpty(replyMsg,"回复消息不存在");
|
||||
AssertUtil.equal(replyMsg.getRoomId(),request.getRoomId(),"只能回复相同会话内的消息");
|
||||
|
||||
}
|
||||
Message insert = MessageAdapter.buildMsgSave(request, uid);
|
||||
messageDao.save(insert);
|
||||
//如果有回复消息
|
||||
if(Objects.nonNull(replyMsg)){
|
||||
Integer gapCount = messageDao.getGapCount(request.getRoomId(), replyMsg.getId(), insert.getId());
|
||||
messageDao.updateGapCount(insert.getId(),gapCount);
|
||||
}
|
||||
//发布消息发送事件
|
||||
applicationEventPublisher.publishEvent(new MessageSendEvent(this, insert.getId()));
|
||||
return insert.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatMessageResp getMsgResp(Message message, Long receiveUid) {
|
||||
return CollUtil.getFirst(getMsgRespBatch(Collections.singletonList(message), receiveUid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CursorPageBaseResp<ChatMemberResp> getMemberPage(CursorPageBaseReq request) {
|
||||
Pair<ChatActiveStatusEnum, String> pair = ChatMemberHelper.getCursorPair(request.getCursor());
|
||||
ChatActiveStatusEnum activeStatusEnum = pair.getKey();
|
||||
String timeCursor = pair.getValue();
|
||||
List<ChatMemberResp> resultList = new ArrayList<>();//最终列表
|
||||
Boolean isLast = Boolean.FALSE;
|
||||
if (activeStatusEnum == ChatActiveStatusEnum.ONLINE) {//在线列表
|
||||
CursorPageBaseResp<Pair<Long, Double>> cursorPage = userCache.getOnlineCursorPage(new CursorPageBaseReq(request.getPageSize(), timeCursor));
|
||||
resultList.addAll(memberAdapter.buildMember(cursorPage.getList(), ChatActiveStatusEnum.ONLINE));//添加在线列表
|
||||
if (cursorPage.getIsLast()) {//如果是最后一页,从离线列表再补点数据
|
||||
Integer leftSize = request.getPageSize() - cursorPage.getList().size();
|
||||
cursorPage = userCache.getOfflineCursorPage(new CursorPageBaseReq(leftSize, null));
|
||||
resultList.addAll(memberAdapter.buildMember(cursorPage.getList(), ChatActiveStatusEnum.OFFLINE));//添加离线线列表
|
||||
activeStatusEnum = ChatActiveStatusEnum.OFFLINE;
|
||||
}
|
||||
timeCursor = cursorPage.getCursor();
|
||||
isLast = cursorPage.getIsLast();
|
||||
} else if (activeStatusEnum == ChatActiveStatusEnum.OFFLINE) {//离线列表
|
||||
CursorPageBaseResp<Pair<Long, Double>> cursorPage = userCache.getOfflineCursorPage(new CursorPageBaseReq(request.getPageSize(), timeCursor));
|
||||
resultList.addAll(memberAdapter.buildMember(cursorPage.getList(), ChatActiveStatusEnum.OFFLINE));//添加离线线列表
|
||||
timeCursor = cursorPage.getCursor();
|
||||
isLast = cursorPage.getIsLast();
|
||||
}
|
||||
//组装结果
|
||||
return new CursorPageBaseResp<>(ChatMemberHelper.generateCursor(activeStatusEnum, timeCursor), isLast, resultList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CursorPageBaseResp<ChatMessageResp> getMsgPage(ChatMessagePageReq request, Long receiveUid) {
|
||||
CursorPageBaseResp<Message> cursorPage = messageDao.getCursorPage(request.getRoomId(), request);
|
||||
if (cursorPage.isEmpty()) {
|
||||
return CursorPageBaseResp.empty();
|
||||
}
|
||||
return CursorPageBaseResp.init(cursorPage, getMsgRespBatch(cursorPage.getList(), receiveUid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CursorPageBaseResp<ChatRoomResp> getRoomPage(CursorPageBaseReq request, Long uid) {
|
||||
CursorPageBaseResp<Room> cursorPage = roomDao.getCursorPage(request);
|
||||
if (request.isFirstPage()) {
|
||||
//第一页插入置顶的大群聊
|
||||
Room group = roomDao.getById(ROOM_GROUP_ID);
|
||||
cursorPage.getList().add(0, group);
|
||||
}
|
||||
return CursorPageBaseResp.init(cursorPage, RoomAdapter.buildResp(cursorPage.getList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatMemberStatisticResp getMemberStatistic() {
|
||||
System.out.println(Thread.currentThread().getName());
|
||||
Long onlineNum = userCache.getOnlineNum();
|
||||
Long offlineNum = userCache.getOfflineNum();
|
||||
ChatMemberStatisticResp resp = new ChatMemberStatisticResp();
|
||||
resp.setOnlineNum(onlineNum);
|
||||
resp.setTotalNum(onlineNum + offlineNum);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMsgMark(Long uid, ChatMessageMarkReq request) {
|
||||
//用户对该消息的标记
|
||||
MessageMark messageMark = messageMarkDao.get(uid, request.getMsgId(), request.getMarkType());
|
||||
if (Objects.nonNull(messageMark)) {//有标记过消息修改一下就好
|
||||
MessageMark update = MessageMark.builder()
|
||||
.id(messageMark.getId())
|
||||
.status(transformAct(request.getActType()))
|
||||
.build();
|
||||
messageMarkDao.updateById(update);
|
||||
}
|
||||
//没标记过消息,插入一条新消息
|
||||
MessageMark insert = MessageMark.builder()
|
||||
.uid(uid)
|
||||
.msgId(request.getMsgId())
|
||||
.type(request.getMarkType())
|
||||
.status(transformAct(request.getActType()))
|
||||
.build();
|
||||
messageMarkDao.save(insert);
|
||||
//发布消息标记事件
|
||||
applicationEventPublisher.publishEvent(new MessageMarkEvent(this,request));
|
||||
}
|
||||
|
||||
private Integer transformAct(Integer actType) {
|
||||
if (actType == 1) {
|
||||
return YesOrNoEnum.NO.getStatus();
|
||||
} else if (actType == 2) {
|
||||
return YesOrNoEnum.YES.getStatus();
|
||||
}
|
||||
throw new BusinessException("动作类型 1确认 2取消");
|
||||
}
|
||||
|
||||
public List<ChatMessageResp> getMsgRespBatch(List<Message> messages, Long receiveUid) {
|
||||
if (CollectionUtil.isEmpty(messages)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<Long, Message> replyMap = new HashMap<>();
|
||||
Map<Long, User> userMap = new HashMap<>();
|
||||
//批量查出回复的消息
|
||||
List<Long> replyIds = messages.stream().map(Message::getReplyMsgId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (CollectionUtil.isNotEmpty(replyIds)) {
|
||||
replyMap = messageDao.listByIds(replyIds).stream().collect(Collectors.toMap(Message::getId, Function.identity()));
|
||||
}
|
||||
//批量查询消息关联用户
|
||||
Set<Long> uidSet = Stream.concat(replyMap.values().stream().map(Message::getFromUid), messages.stream().map(Message::getFromUid)).collect(Collectors.toSet());
|
||||
userMap = userCache.getUserInfoBatch(uidSet);
|
||||
//查询消息标志
|
||||
List<MessageMark> msgMark = messageMarkDao.getValidMarkByMsgIdBatch(messages.stream().map(Message::getId).collect(Collectors.toList()));
|
||||
return MessageAdapter.buildMsgResp(messages, replyMap, userMap,msgMark,receiveUid);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.abin.mallchat.custom.common.config;
|
||||
|
||||
import com.abin.mallchat.custom.common.intecepter.CollectorInterceptor;
|
||||
import com.abin.mallchat.custom.common.intecepter.TokenInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Description: 配置所有拦截器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-04-05
|
||||
*/
|
||||
@Configuration
|
||||
public class InterceptorConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private TokenInterceptor tokenInterceptor;
|
||||
@Autowired
|
||||
private CollectorInterceptor collectorInterceptor;
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(tokenInterceptor)
|
||||
.addPathPatterns("/capi/**");
|
||||
registry.addInterceptor(collectorInterceptor)
|
||||
.addPathPatterns("/capi/**");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.abin.mallchat.custom.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.Profiles;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.service.Contact;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Configuration
|
||||
@EnableSwagger2WebMvc
|
||||
public class SwaggerConfig {
|
||||
@Bean(value = "defaultApi2")
|
||||
Docket docket() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
//配置网站的基本信息
|
||||
.apiInfo(new ApiInfoBuilder()
|
||||
//网站标题
|
||||
.title("mallchat接口文档")
|
||||
//标题后面的版本号
|
||||
.version("v1.0")
|
||||
.description("mallchat接口文档")
|
||||
//联系人信息
|
||||
.contact(new Contact("阿斌", "http://www.mallchat.cn", "972627721@qq.com"))
|
||||
.build())
|
||||
.select()
|
||||
//指定接口的位置
|
||||
.apis(RequestHandlerSelectors
|
||||
.withClassAnnotation(RestController.class)
|
||||
)
|
||||
.paths(PathSelectors.any())
|
||||
.build();
|
||||
}
|
||||
/**
|
||||
* swagger 配置
|
||||
* @param environment 环境
|
||||
*/
|
||||
// @Bean
|
||||
// public Docket docket(Environment environment) {
|
||||
//
|
||||
// // 设置环境范围
|
||||
// Profiles profiles = Profiles.of("dev","test");
|
||||
// // 如果在该环境返回内则返回:true,反之返回 false
|
||||
// boolean flag = environment.acceptsProfiles(profiles);
|
||||
//
|
||||
// // 创建一个 swagger 的 bean 实例
|
||||
// return new Docket(DocumentationType.SWAGGER_2)
|
||||
// .enable(flag) // 是否开启 swagger:true -> 开启,false -> 关闭
|
||||
// ;
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.abin.mallchat.custom.common.config;
|
||||
|
||||
import com.abin.mallchat.custom.user.service.handler.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import me.chanjar.weixin.common.redis.JedisWxRedisOps;
|
||||
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
|
||||
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
|
||||
import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static me.chanjar.weixin.common.api.WxConsts.EventType;
|
||||
import static me.chanjar.weixin.common.api.WxConsts.EventType.SUBSCRIBE;
|
||||
import static me.chanjar.weixin.common.api.WxConsts.EventType.UNSUBSCRIBE;
|
||||
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
|
||||
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType.EVENT;
|
||||
import static me.chanjar.weixin.mp.constant.WxMpEventConstants.CustomerService.*;
|
||||
import static me.chanjar.weixin.mp.constant.WxMpEventConstants.POI_CHECK_NOTIFY;
|
||||
|
||||
/**
|
||||
* wechat mp configuration
|
||||
*
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(WxMpProperties.class)
|
||||
public class WxMpConfiguration {
|
||||
private final LogHandler logHandler;
|
||||
private final MsgHandler msgHandler;
|
||||
private final SubscribeHandler subscribeHandler;
|
||||
private final ScanHandler scanHandler;
|
||||
private final WxMpProperties properties;
|
||||
|
||||
@Bean
|
||||
public WxMpService wxMpService() {
|
||||
// 代码里 getConfigs()处报错的同学,请注意仔细阅读项目说明,你的IDE需要引入lombok插件!!!!
|
||||
final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
|
||||
if (configs == null) {
|
||||
throw new RuntimeException("大哥,拜托先看下项目首页的说明(readme文件),添加下相关配置,注意别配错了!");
|
||||
}
|
||||
|
||||
WxMpService service = new WxMpServiceImpl();
|
||||
service.setMultiConfigStorages(configs
|
||||
.stream().map(a -> {
|
||||
WxMpDefaultConfigImpl configStorage;
|
||||
configStorage = new WxMpDefaultConfigImpl();
|
||||
|
||||
configStorage.setAppId(a.getAppId());
|
||||
configStorage.setSecret(a.getSecret());
|
||||
configStorage.setToken(a.getToken());
|
||||
configStorage.setAesKey(a.getAesKey());
|
||||
return configStorage;
|
||||
}).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o)));
|
||||
return service;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
|
||||
final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
|
||||
|
||||
// 记录所有事件的日志 (异步执行)
|
||||
newRouter.rule().handler(this.logHandler).next();
|
||||
|
||||
// 关注事件
|
||||
newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
|
||||
|
||||
// 扫码事件
|
||||
newRouter.rule().async(false).msgType(EVENT).event(EventType.SCAN).handler(this.scanHandler).end();
|
||||
|
||||
// 默认
|
||||
newRouter.rule().async(false).handler(this.msgHandler).end();
|
||||
|
||||
return newRouter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.abin.mallchat.custom.common.config;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* wechat mp properties
|
||||
*
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "wx.mp")
|
||||
public class WxMpProperties {
|
||||
/**
|
||||
* 是否使用redis存储access token
|
||||
*/
|
||||
private boolean useRedis;
|
||||
|
||||
/**
|
||||
* redis 配置
|
||||
*/
|
||||
private RedisConfig redisConfig;
|
||||
|
||||
@Data
|
||||
public static class RedisConfig {
|
||||
/**
|
||||
* redis服务器 主机地址
|
||||
*/
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* redis服务器 端口号
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* redis服务器 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* redis 服务连接超时时间
|
||||
*/
|
||||
private Integer timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个公众号配置信息
|
||||
*/
|
||||
private List<MpConfig> configs;
|
||||
|
||||
@Data
|
||||
public static class MpConfig {
|
||||
/**
|
||||
* 设置微信公众号的appid
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 设置微信公众号的app secret
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* 设置微信公众号的token
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 设置微信公众号的EncodingAESKey
|
||||
*/
|
||||
private String aesKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return JSONUtil.toJsonStr(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.abin.mallchat.custom.common.event;
|
||||
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageMarkReq;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
@Getter
|
||||
public class MessageMarkEvent extends ApplicationEvent {
|
||||
private ChatMessageMarkReq req;
|
||||
|
||||
public MessageMarkEvent(Object source, ChatMessageMarkReq req) {
|
||||
super(source);
|
||||
this.req = req;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.abin.mallchat.custom.common.event;
|
||||
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
@Getter
|
||||
public class MessageSendEvent extends ApplicationEvent {
|
||||
private Long msgId;
|
||||
|
||||
public MessageSendEvent(Object source, Long msgId) {
|
||||
super(source);
|
||||
this.msgId = msgId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.abin.mallchat.custom.common.event;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
@Getter
|
||||
public class UserOfflineEvent extends ApplicationEvent {
|
||||
private User user;
|
||||
|
||||
public UserOfflineEvent(Object source, User user) {
|
||||
super(source);
|
||||
this.user = user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.abin.mallchat.custom.common.event;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Getter
|
||||
public class UserOnlineEvent extends ApplicationEvent {
|
||||
private User user;
|
||||
|
||||
public UserOnlineEvent(Object source, User user) {
|
||||
super(source);
|
||||
this.user = user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.abin.mallchat.custom.common.event;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
@Getter
|
||||
public class UserRegisterEvent extends ApplicationEvent {
|
||||
private User user;
|
||||
|
||||
public UserRegisterEvent(Object source, User user) {
|
||||
super(source);
|
||||
this.user = user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.abin.mallchat.custom.common.event.listener;
|
||||
|
||||
import com.abin.mallchat.common.chat.dao.MessageDao;
|
||||
import com.abin.mallchat.common.chat.dao.MessageMarkDao;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageTypeEnum;
|
||||
import com.abin.mallchat.common.common.domain.enums.IdempotentEnum;
|
||||
import com.abin.mallchat.common.user.domain.enums.ItemEnum;
|
||||
import com.abin.mallchat.common.user.service.IUserBackpackService;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageMarkReq;
|
||||
import com.abin.mallchat.custom.common.event.MessageMarkEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 消息标记监听器
|
||||
*
|
||||
* @author zhongzb create on 2022/08/26
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MessageMarkListener {
|
||||
@Autowired
|
||||
private MessageMarkDao messageMarkDao;
|
||||
@Autowired
|
||||
private MessageDao messageDao;
|
||||
@Autowired
|
||||
private IUserBackpackService iUserBackpackService;
|
||||
|
||||
@Async
|
||||
@EventListener(classes = MessageMarkEvent.class)
|
||||
public void changeMsgType(MessageMarkEvent event) {
|
||||
ChatMessageMarkReq req = event.getReq();
|
||||
Message msg = messageDao.getById(req.getMsgId());
|
||||
if (!Objects.equals(msg, MessageTypeEnum.NORMAL.getType())) {//普通消息才需要升级
|
||||
return;
|
||||
}
|
||||
//消息被标记次数
|
||||
Integer markCount = messageMarkDao.getMarkCount(req.getMsgId(), req.getMarkType());
|
||||
MessageMarkTypeEnum markTypeEnum = MessageMarkTypeEnum.of(req.getMarkType());
|
||||
if (markCount < markTypeEnum.getRiseNum()) {
|
||||
return;
|
||||
}
|
||||
boolean updateSuccess = messageDao.riseOptimistic(msg.getId(), msg.getType(), markTypeEnum.getRiseEnum().getType());
|
||||
if (MessageMarkTypeEnum.LIKE.getType().equals(req.getMarkType()) && updateSuccess) {//尝试给用户发送一张徽章
|
||||
iUserBackpackService.acquireItem(msg.getFromUid(), ItemEnum.LIKE_BADGE.getId(), IdempotentEnum.MSG_ID, msg.getId().toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.abin.mallchat.custom.common.event.listener;
|
||||
|
||||
import com.abin.mallchat.common.chat.dao.MessageDao;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.common.event.MessageSendEvent;
|
||||
import com.abin.mallchat.custom.chat.service.ChatService;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 消息发送监听器
|
||||
*
|
||||
* @author zhongzb create on 2022/08/26
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MessageSendListener {
|
||||
@Autowired
|
||||
private WebSocketService webSocketService;
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
@Autowired
|
||||
private MessageDao messageDao;
|
||||
|
||||
@Async
|
||||
@EventListener(classes = MessageSendEvent.class)
|
||||
public void notifyAllOnline(MessageSendEvent event) {
|
||||
Message message = messageDao.getById(event.getMsgId());
|
||||
ChatMessageResp msgResp = chatService.getMsgResp(message, null);
|
||||
webSocketService.sendToAllOnline(WSAdapter.buildMsgSend(msgResp), message.getFromUid());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.abin.mallchat.custom.common.event.listener;
|
||||
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.common.event.UserOfflineEvent;
|
||||
import com.abin.mallchat.custom.common.event.UserOnlineEvent;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 用户下线监听器
|
||||
*
|
||||
* @author zhongzb create on 2022/08/26
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserOfflineListener {
|
||||
@Autowired
|
||||
private WebSocketService webSocketService;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
@Autowired
|
||||
private WSAdapter wsAdapter;
|
||||
|
||||
@Async
|
||||
@EventListener(classes = UserOfflineEvent.class)
|
||||
public void saveRedisAndPush(UserOfflineEvent event) {
|
||||
User user = event.getUser();
|
||||
userCache.offline(user.getId(), user.getLastOptTime());
|
||||
//推送给所有在线用户,该用户下线
|
||||
webSocketService.sendToAllOnline(wsAdapter.buildOfflineNotifyResp(event.getUser()), event.getUser().getId());
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener(classes = UserOfflineEvent.class)
|
||||
public void saveDB(UserOfflineEvent event) {
|
||||
User user = event.getUser();
|
||||
User update = new User();
|
||||
update.setId(user.getId());
|
||||
update.setLastOptTime(user.getLastOptTime());
|
||||
userDao.updateById(update);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.abin.mallchat.custom.common.event.listener;
|
||||
|
||||
import com.abin.mallchat.common.chat.dao.MessageDao;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.service.IpService;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.service.ChatService;
|
||||
import com.abin.mallchat.custom.common.event.MessageSendEvent;
|
||||
import com.abin.mallchat.custom.common.event.UserOnlineEvent;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 用户上线监听器
|
||||
*
|
||||
* @author zhongzb create on 2022/08/26
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserOnlineListener {
|
||||
@Autowired
|
||||
private WebSocketService webSocketService;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
@Autowired
|
||||
private WSAdapter wsAdapter;
|
||||
@Autowired
|
||||
private IpService ipService;
|
||||
|
||||
@Async
|
||||
@EventListener(classes = UserOnlineEvent.class)
|
||||
public void saveRedisAndPush(UserOnlineEvent event) {
|
||||
User user = event.getUser();
|
||||
userCache.online(user.getId(), user.getLastOptTime());
|
||||
//推送给所有在线用户,该用户登录成功
|
||||
webSocketService.sendToAllOnline(wsAdapter.buildOnlineNotifyResp(event.getUser()), event.getUser().getId());
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener(classes = UserOnlineEvent.class)
|
||||
public void saveDB(UserOnlineEvent event) {
|
||||
User user = event.getUser();
|
||||
User update = new User();
|
||||
update.setId(user.getId());
|
||||
update.setLastOptTime(user.getLastOptTime());
|
||||
update.setIpInfo(user.getIpInfo());
|
||||
userDao.updateById(update);
|
||||
//更新用户ip详情
|
||||
ipService.refreshIpDetailAsync(user.getId());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.abin.mallchat.custom.common.event.listener;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.enums.IdempotentEnum;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.enums.ItemEnum;
|
||||
import com.abin.mallchat.common.user.service.IUserBackpackService;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.common.event.UserOnlineEvent;
|
||||
import com.abin.mallchat.custom.common.event.UserRegisterEvent;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 用户上线监听器
|
||||
*
|
||||
* @author zhongzb create on 2022/08/26
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserRegisterListener {
|
||||
@Autowired
|
||||
private WebSocketService webSocketService;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
@Autowired
|
||||
private WSAdapter wsAdapter;
|
||||
@Autowired
|
||||
private IUserBackpackService iUserBackpackService;
|
||||
|
||||
@Async
|
||||
@EventListener(classes = UserRegisterEvent.class)
|
||||
public void sendCard(UserRegisterEvent event) {
|
||||
User user = event.getUser();
|
||||
//送一张改名卡
|
||||
iUserBackpackService.acquireItem(user.getId(), ItemEnum.MODIFY_NAME_CARD.getId(), IdempotentEnum.UID, user.getId().toString());
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener(classes = UserRegisterEvent.class)
|
||||
public void sendBadge(UserOnlineEvent event) {
|
||||
User user = event.getUser();
|
||||
int count = userDao.count();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.abin.mallchat.custom.common.intecepter;
|
||||
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import com.abin.mallchat.common.common.utils.RequestHolder;
|
||||
import com.abin.mallchat.common.common.domain.dto.RequestInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 信息收集的拦截器
|
||||
*/
|
||||
@Order
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CollectorInterceptor implements HandlerInterceptor, WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(this)
|
||||
.addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
RequestInfo info = new RequestInfo();
|
||||
info.setUid(Optional.ofNullable(request.getAttribute(TokenInterceptor.ATTRIBUTE_UID)).map(Object::toString).map(Long::parseLong).orElse(null));
|
||||
info.setIp(ServletUtil.getClientIP(request));
|
||||
RequestHolder.set(info);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
RequestHolder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.abin.mallchat.custom.common.intecepter;
|
||||
|
||||
import com.abin.mallchat.common.common.constant.MDCKey;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.WebApplicationContextUtils;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.annotation.WebFilter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Description: 设置链路追踪的值,初期单体项目先简单用
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-04-05
|
||||
*/
|
||||
@Slf4j
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
public class HttpTraceIdFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
String tid = UUID.randomUUID().toString();
|
||||
MDC.put(MDCKey.TID, tid);
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.abin.mallchat.custom.common.intecepter;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.abin.mallchat.common.common.constant.MDCKey;
|
||||
import com.abin.mallchat.common.common.exception.HttpErrorEnum;
|
||||
import com.abin.mallchat.custom.user.service.LoginService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Order(-2)
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TokenInterceptor implements HandlerInterceptor {
|
||||
|
||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
public static final String AUTHORIZATION_SCHEMA = "Bearer ";
|
||||
public static final String ATTRIBUTE_UID = "uid";
|
||||
|
||||
@Autowired
|
||||
private LoginService loginService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
//获取用户登录token
|
||||
String token = getToken(request);
|
||||
Long validUid = loginService.getValidUid(token);
|
||||
if (Objects.nonNull(validUid)) {//有登录态
|
||||
request.setAttribute(ATTRIBUTE_UID, validUid);
|
||||
} else {
|
||||
boolean isPublicURI = isPublicURI(request.getRequestURI());
|
||||
if (!isPublicURI) {//又没有登录态,又不是公开路径,直接401
|
||||
HttpErrorEnum.ACCESS_DENIED.sendHttpError(response);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
MDC.put(MDCKey.UID, String.valueOf(validUid));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是不是公共方法,可以未登录访问的
|
||||
*
|
||||
* @param requestURI
|
||||
*/
|
||||
private boolean isPublicURI(String requestURI) {
|
||||
String[] split = requestURI.split("/");
|
||||
return split.length > 2 && "public".equals(split[3]);
|
||||
}
|
||||
|
||||
private String getToken(HttpServletRequest request) {
|
||||
String header = request.getHeader(AUTHORIZATION_HEADER);
|
||||
return Optional.ofNullable(header)
|
||||
.filter(h -> h.startsWith(AUTHORIZATION_SCHEMA))
|
||||
.map(h -> h.substring(AUTHORIZATION_SCHEMA.length()))
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.abin.mallchat.custom.common.intecepter;
|
||||
|
||||
import cn.hutool.core.date.StopWatch;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.abin.mallchat.common.common.utils.RequestHolder;
|
||||
import com.abin.mallchat.common.common.domain.dto.RequestInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
/**
|
||||
* 日志切面
|
||||
*
|
||||
* @author wayne
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebLogAspect {
|
||||
|
||||
|
||||
/**
|
||||
* 接收到请求,记录请求内容
|
||||
* 所有controller包下所有的类的方法,都是切点
|
||||
* <p>
|
||||
* 如果ApiResult返回success=false,则打印warn日志;
|
||||
* warn日志只能打印在同一行,因为只有等到ApiResult结果才知道是success=false。
|
||||
* <p>
|
||||
* 如果ApiResult返回success=true,则打印info日志;
|
||||
* 特别注意:由于info级别日志已经包含了warn级别日志。如果开了info级别日志,warn就不会打印了。
|
||||
*/
|
||||
@Around("execution(* com..controller..*.*(..))")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
|
||||
String method = request.getMethod();
|
||||
String uri = request.getRequestURI();
|
||||
//如果参数有HttpRequest,ServletResponse,直接移除,不打印这些
|
||||
List<Object> paramList = Stream.of(joinPoint.getArgs())
|
||||
.filter(args -> !(args instanceof ServletRequest))
|
||||
.filter(args -> !(args instanceof ServletResponse))
|
||||
.collect(Collectors.toList());
|
||||
String printParamStr = paramList.size() == 1 ? JSONUtil.toJsonStr(paramList.get(0)) : JSONUtil.toJsonStr(paramList);
|
||||
RequestInfo requestInfo = RequestHolder.get();
|
||||
String userHeaderStr = JSONUtil.toJsonStr(requestInfo);
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("[{}][{}]【base:{}】【request:{}】", method, uri, userHeaderStr, printParamStr);
|
||||
}
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
Object result = joinPoint.proceed();
|
||||
stopWatch.stop();
|
||||
long cost = stopWatch.getTotalTimeMillis();
|
||||
String printResultStr = JSONUtil.toJsonStr(result);
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("[{}]【response:{}】[cost:{}ms]", uri, printResultStr, cost);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.abin.mallchat.custom.user.controller;
|
||||
|
||||
|
||||
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
|
||||
import com.abin.mallchat.common.common.utils.RequestHolder;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.ModifyNameReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.WearingBadgeReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.BadgeResp;
|
||||
import com.abin.mallchat.custom.user.service.UserService;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserInfoResp;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户表 前端控制器
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/capi/user")
|
||||
@Api(tags = "用户管理相关接口")
|
||||
public class UserController {
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/userInfo")
|
||||
@ApiOperation("用户详情")
|
||||
public ApiResult<UserInfoResp> getUserInfo() {
|
||||
return ApiResult.success(userService.getUserInfo(RequestHolder.get().getUid()));
|
||||
}
|
||||
|
||||
@PutMapping("/name")
|
||||
@ApiOperation("修改用户名")
|
||||
public ApiResult<Void> modifyName(@Valid @RequestBody ModifyNameReq req) {
|
||||
userService.modifyName(RequestHolder.get().getUid(), req);
|
||||
return ApiResult.success();
|
||||
}
|
||||
|
||||
@GetMapping("/badges")
|
||||
@ApiOperation("可选徽章预览")
|
||||
public ApiResult<List<BadgeResp>> badges() {
|
||||
return ApiResult.success(userService.badges(RequestHolder.get().getUid()));
|
||||
}
|
||||
|
||||
@PutMapping("/badge")
|
||||
@ApiOperation("佩戴徽章")
|
||||
public ApiResult<Void> wearingBadge(@Valid @RequestBody WearingBadgeReq req) {
|
||||
userService.wearingBadge(RequestHolder.get().getUid(),req);
|
||||
return ApiResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.abin.mallchat.custom.user.controller;
|
||||
|
||||
import com.abin.mallchat.custom.user.service.WxMsgService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
|
||||
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
/**
|
||||
* Description: 微信api交互接口
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("wx/portal/public")
|
||||
public class WxPortalController {
|
||||
|
||||
private final WxMpService wxService;
|
||||
private final WxMpMessageRouter messageRouter;
|
||||
private final WxMsgService wxMsgService;
|
||||
|
||||
@GetMapping(produces = "text/plain;charset=utf-8")
|
||||
public String authGet(@RequestParam(name = "signature", required = false) String signature,
|
||||
@RequestParam(name = "timestamp", required = false) String timestamp,
|
||||
@RequestParam(name = "nonce", required = false) String nonce,
|
||||
@RequestParam(name = "echostr", required = false) String echostr) {
|
||||
|
||||
log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,
|
||||
timestamp, nonce, echostr);
|
||||
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
|
||||
throw new IllegalArgumentException("请求参数非法,请核实!");
|
||||
}
|
||||
|
||||
|
||||
if (wxService.checkSignature(timestamp, nonce, signature)) {
|
||||
return echostr;
|
||||
}
|
||||
|
||||
return "非法请求";
|
||||
}
|
||||
|
||||
@GetMapping("/callBack")
|
||||
public RedirectView callBack(@RequestParam String code) throws WxErrorException {
|
||||
try {
|
||||
WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code);
|
||||
WxOAuth2UserInfo userInfo = wxService.getOAuth2Service().getUserInfo(accessToken, "zh_CN");
|
||||
wxMsgService.authorize(userInfo);
|
||||
}catch (Exception e){
|
||||
log.error("callBack error",e);
|
||||
}
|
||||
RedirectView redirectView = new RedirectView();
|
||||
redirectView.setUrl("https://mp.weixin.qq.com/s/MKCWzoCIzvh5G_1sK5sLoA");
|
||||
return redirectView;
|
||||
}
|
||||
|
||||
@PostMapping(produces = "application/xml; charset=UTF-8")
|
||||
public String post(@RequestBody String requestBody,
|
||||
@RequestParam("signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestParam("openid") String openid,
|
||||
@RequestParam(name = "encrypt_type", required = false) String encType,
|
||||
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
|
||||
log.info("\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
|
||||
+ " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
|
||||
openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
|
||||
|
||||
if (!wxService.checkSignature(timestamp, nonce, signature)) {
|
||||
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
|
||||
}
|
||||
|
||||
String out = null;
|
||||
if (encType == null) {
|
||||
// 明文传输的消息
|
||||
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
|
||||
WxMpXmlOutMessage outMessage = this.route(inMessage);
|
||||
if (outMessage == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
out = outMessage.toXml();
|
||||
} else if ("aes".equalsIgnoreCase(encType)) {
|
||||
// aes加密的消息
|
||||
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(),
|
||||
timestamp, nonce, msgSignature);
|
||||
log.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
|
||||
WxMpXmlOutMessage outMessage = this.route(inMessage);
|
||||
if (outMessage == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage());
|
||||
}
|
||||
|
||||
log.debug("\n组装回复信息:{}", out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private WxMpXmlOutMessage route(WxMpXmlMessage message) {
|
||||
try {
|
||||
return this.messageRouter.route(message);
|
||||
} catch (Exception e) {
|
||||
log.error("路由消息时出现异常!", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.abin.mallchat.custom.user.domain.dto.ws;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Description: 记录和前端连接的一些映射信息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-21
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class WSChannelExtraDTO {
|
||||
/**
|
||||
* 前端如果登录了,记录uid
|
||||
*/
|
||||
private Long uid;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.abin.mallchat.custom.user.domain.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: ws前端请求类型枚举
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum WSReqTypeEnum {
|
||||
LOGIN(1, "请求登录二维码"),
|
||||
HEARTBEAT(2, "心跳包"),
|
||||
AUTHORIZE(3, "登录认证"),
|
||||
;
|
||||
|
||||
private final Integer type;
|
||||
private final String desc;
|
||||
|
||||
private static Map<Integer, WSReqTypeEnum> cache;
|
||||
|
||||
static {
|
||||
cache = Arrays.stream(WSReqTypeEnum.values()).collect(Collectors.toMap(WSReqTypeEnum::getType, Function.identity()));
|
||||
}
|
||||
|
||||
public static WSReqTypeEnum of(Integer type) {
|
||||
return cache.get(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.abin.mallchat.custom.user.domain.enums;
|
||||
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.WSLoginSuccess;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.WSLoginUrl;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.WSMessage;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.WSOnlineOfflineNotify;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: ws前端请求类型枚举
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum WSRespTypeEnum {
|
||||
LOGIN_URL(1, "登录二维码返回", WSLoginUrl.class),
|
||||
LOGIN_SCAN_SUCCESS(2, "用户扫描成功等待授权", null),
|
||||
LOGIN_SUCCESS(3, "用户登录成功返回用户信息", WSLoginSuccess.class),
|
||||
MESSAGE(4, "新消息", WSMessage.class),
|
||||
ONLINE_OFFLINE_NOTIFY(5, "上下线通知", WSOnlineOfflineNotify.class),
|
||||
INVALIDATE_TOKEN(6, "使前端的token失效,意味着前端需要重新登录", null),
|
||||
;
|
||||
|
||||
private final Integer type;
|
||||
private final String desc;
|
||||
private final Class dataClass;
|
||||
|
||||
private static Map<Integer, WSRespTypeEnum> cache;
|
||||
|
||||
static {
|
||||
cache = Arrays.stream(WSRespTypeEnum.values()).collect(Collectors.toMap(WSRespTypeEnum::getType, Function.identity()));
|
||||
}
|
||||
|
||||
public static WSRespTypeEnum of(Integer type) {
|
||||
return cache.get(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.request.user;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
|
||||
/**
|
||||
* Description: 修改用户名
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ModifyNameReq {
|
||||
|
||||
@NotNull
|
||||
@Length(max = 6,message = "用户名可别取太长,不然我记不住噢")
|
||||
@ApiModelProperty("用户名")
|
||||
private String name;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.request.user;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
|
||||
/**
|
||||
* Description: 佩戴徽章
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class WearingBadgeReq {
|
||||
|
||||
@NotNull
|
||||
@ApiModelProperty("徽章id")
|
||||
private Long badgeId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.request.ws;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class WSAuthorize {
|
||||
private String token;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.request.ws;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Description: websocket前端请求体
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
public class WSBaseReq {
|
||||
/**
|
||||
* 请求类型 1.请求登录二维码,2心跳检测
|
||||
* @see com.abin.mallchat.custom.user.domain.enums.WSReqTypeEnum
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 每个请求包具体的数据,类型不同结果不同
|
||||
*/
|
||||
private String data;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.user;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Description: 徽章信息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-22
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("徽章信息")
|
||||
public class BadgeResp {
|
||||
|
||||
@ApiModelProperty(value = "徽章id")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "徽章图标")
|
||||
private String image;
|
||||
|
||||
@ApiModelProperty(value = "徽章描述")
|
||||
private String describe;
|
||||
|
||||
@ApiModelProperty(value = "是否拥有 0否 1是")
|
||||
private Integer obtain;
|
||||
|
||||
@ApiModelProperty(value = "是否佩戴 0否 1是")
|
||||
private Integer wearing;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.user;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Description: 用户信息返回
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-22
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("用户详情")
|
||||
public class UserInfoResp {
|
||||
|
||||
@ApiModelProperty(value = "用户id")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称")
|
||||
private String name;
|
||||
|
||||
@ApiModelProperty(value = "用户头像")
|
||||
private String avatar;
|
||||
|
||||
@ApiModelProperty(value = "性别 1为男性,2为女性")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "剩余改名次数")
|
||||
private Integer modifyNameChance;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.ws;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Description: ws的基本返回信息体
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
public class WSBaseResp<T> {
|
||||
/**
|
||||
* ws推送给前端的消息
|
||||
* @see com.abin.mallchat.custom.user.domain.enums.WSRespTypeEnum
|
||||
*/
|
||||
private Integer type;
|
||||
private T data;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.ws;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class WSLoginSuccess {
|
||||
private Long uid;
|
||||
private String avatar;
|
||||
private String token;
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.ws;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class WSLoginUrl {
|
||||
private String loginUrl;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.ws;
|
||||
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Description: 用户消息推送
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
public class WSMessage extends ChatMessageResp {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.ws;
|
||||
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Description:用户上下线变动的推送类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class WSOnlineOfflineNotify {
|
||||
private List<ChatMemberResp> changeList = new ArrayList<>();//新的上下线用户
|
||||
private Long onlineNum;//在线人数
|
||||
private Long totalNum;//总人数
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.abin.mallchat.custom.user.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Description: 登录相关处理类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
public interface LoginService {
|
||||
|
||||
|
||||
/**
|
||||
* 校验token是不是有效
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
boolean verify(String token);
|
||||
|
||||
/**
|
||||
* 刷新token有效期
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
void renewalTokenIfNecessary(String token);
|
||||
|
||||
/**
|
||||
* 登录成功,获取token
|
||||
*
|
||||
* @param uid
|
||||
* @return 返回token
|
||||
*/
|
||||
String login(Long uid);
|
||||
|
||||
/**
|
||||
* 如果token有效,返回uid
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
Long getValidUid(String token);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.abin.mallchat.custom.user.service;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.ModifyNameReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.WearingBadgeReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.BadgeResp;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserInfoResp;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户表 服务类
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface UserService {
|
||||
|
||||
/**
|
||||
* 获取前端展示信息
|
||||
* @param uid
|
||||
* @return
|
||||
*/
|
||||
UserInfoResp getUserInfo(Long uid);
|
||||
|
||||
/**
|
||||
* 修改用户名
|
||||
* @param uid
|
||||
* @param req
|
||||
*/
|
||||
void modifyName(Long uid, ModifyNameReq req);
|
||||
|
||||
/**
|
||||
* 用户徽章列表
|
||||
* @param uid
|
||||
*/
|
||||
List<BadgeResp> badges(Long uid);
|
||||
|
||||
/**
|
||||
* 佩戴徽章
|
||||
* @param uid
|
||||
* @param req
|
||||
*/
|
||||
void wearingBadge(Long uid, WearingBadgeReq req);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param openId
|
||||
*/
|
||||
void register(String openId);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.abin.mallchat.custom.user.service;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.ws.WSAuthorize;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.WSBaseResp;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
public interface WebSocketService {
|
||||
/**
|
||||
* 处理用户登录请求,需要返回一张带code的二维码
|
||||
*
|
||||
* @param channel
|
||||
*/
|
||||
void handleLoginReq(Channel channel);
|
||||
|
||||
/**
|
||||
* 处理所有ws连接的事件
|
||||
*
|
||||
* @param channel
|
||||
*/
|
||||
void connect(Channel channel);
|
||||
|
||||
/**
|
||||
* 处理ws断开连接的事件
|
||||
*
|
||||
* @param channel
|
||||
*/
|
||||
void removed(Channel channel);
|
||||
|
||||
/**
|
||||
* 主动认证登录
|
||||
*
|
||||
* @param channel
|
||||
* @param wsAuthorize
|
||||
*/
|
||||
void authorize(Channel channel, WSAuthorize wsAuthorize);
|
||||
|
||||
/**
|
||||
* 扫码用户登录成功通知
|
||||
*
|
||||
* @param loginCode
|
||||
* @param user
|
||||
* @param token
|
||||
*/
|
||||
Boolean scanLoginSuccess(Integer loginCode, User user, String token);
|
||||
|
||||
/**
|
||||
* 用户扫码成功
|
||||
*
|
||||
* @param loginCode
|
||||
*/
|
||||
Boolean scanSuccess(Integer loginCode);
|
||||
|
||||
/**
|
||||
* 推动消息给所有在线的人
|
||||
*
|
||||
* @param wsBaseResp 发送的消息体
|
||||
* @param skipUid 需要跳过的人
|
||||
*/
|
||||
void sendToAllOnline(WSBaseResp wsBaseResp, Long skipUid);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.abin.mallchat.custom.user.service;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.common.config.ThreadPoolConfig;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.user.service.adapter.TextBuilder;
|
||||
import com.abin.mallchat.custom.user.service.adapter.UserAdapter;
|
||||
import io.netty.channel.Channel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* Description: 处理与微信api的交互逻辑
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class WxMsgService {
|
||||
/**
|
||||
* 用户的openId和前端登录场景code的映射关系
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, Integer> OPENID_EVENT_CODE_MAP = new ConcurrentHashMap<>();
|
||||
private static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
|
||||
@Value("${wx.mp.callback}")
|
||||
private String callback;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private WebSocketService webSocketService;
|
||||
@Autowired
|
||||
private LoginService loginService;
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
@Autowired
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
|
||||
public WxMpXmlOutMessage scan(WxMpService wxMpService, WxMpXmlMessage wxMpXmlMessage) {
|
||||
String fromUser = wxMpXmlMessage.getFromUser();
|
||||
Integer eventKey = Integer.parseInt(getEventKey(wxMpXmlMessage));
|
||||
User user = userDao.getByOpenId(fromUser);
|
||||
if (Objects.nonNull(user) && Objects.nonNull(user.getAvatar())) {
|
||||
//注册且已经授权的用户,直接登录成功
|
||||
login(user.getId(), eventKey);
|
||||
return null;
|
||||
}
|
||||
if (Objects.isNull(user)) {
|
||||
//未注册的先注册
|
||||
userService.register(fromUser);
|
||||
}
|
||||
//保存openid和场景code的关系,后续才能通知到前端
|
||||
OPENID_EVENT_CODE_MAP.put(fromUser, eventKey);
|
||||
//授权流程,给用户发送授权消息,并且异步通知前端扫码成功
|
||||
threadPoolTaskExecutor.execute(()->webSocketService.scanSuccess(eventKey));
|
||||
String skipUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(callback + "/wx/portal/public/callBack"));
|
||||
WxMpXmlOutMessage.TEXT().build();
|
||||
return new TextBuilder().build("请点击链接授权:<a href=\"" + skipUrl + "\">登录</a>", wxMpXmlMessage, wxMpService);
|
||||
}
|
||||
|
||||
private String getEventKey(WxMpXmlMessage wxMpXmlMessage) {
|
||||
//扫码关注的渠道事件有前缀,需要去除
|
||||
return wxMpXmlMessage.getEventKey().replace("qrscene_", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户授权
|
||||
*
|
||||
* @param userInfo
|
||||
*/
|
||||
public void authorize(WxOAuth2UserInfo userInfo) {
|
||||
User user = userDao.getByOpenId(userInfo.getOpenid());
|
||||
User update = UserAdapter.buildAuthorizeUser(user.getId(), userInfo);
|
||||
//更新用户信息
|
||||
userDao.updateById(update);
|
||||
//触发用户登录成功操作
|
||||
Integer eventKey = OPENID_EVENT_CODE_MAP.get(userInfo.getOpenid());
|
||||
login(user.getId(), eventKey);
|
||||
}
|
||||
|
||||
private void login(Long uid, Integer eventKey) {
|
||||
User user = userDao.getById(uid);
|
||||
//调用用户登录模块
|
||||
String token = loginService.login(uid);
|
||||
//推送前端登录成功
|
||||
webSocketService.scanLoginSuccess(eventKey, user, token);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.abin.mallchat.custom.user.service.adapter;
|
||||
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
public abstract class AbstractBuilder {
|
||||
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
public abstract WxMpXmlOutMessage build(String content,
|
||||
WxMpXmlMessage wxMessage, WxMpService service);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.abin.mallchat.custom.user.service.adapter;
|
||||
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutImageMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
public class ImageBuilder extends AbstractBuilder {
|
||||
|
||||
@Override
|
||||
public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage,
|
||||
WxMpService service) {
|
||||
|
||||
WxMpXmlOutImageMessage m = WxMpXmlOutMessage.IMAGE().mediaId(content)
|
||||
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
|
||||
.build();
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.abin.mallchat.custom.user.service.adapter;
|
||||
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
public class TextBuilder extends AbstractBuilder {
|
||||
|
||||
@Override
|
||||
public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage,
|
||||
WxMpService service) {
|
||||
WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content(content)
|
||||
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
|
||||
.build();
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.abin.mallchat.custom.user.service.adapter;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
|
||||
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.BadgeResp;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserInfoResp;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
|
||||
import me.chanjar.weixin.mp.bean.result.WxMpUser;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: 用户适配器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserAdapter {
|
||||
|
||||
public static User buildUser(String openId) {
|
||||
User user = new User();
|
||||
user.setOpenId(openId);
|
||||
return user;
|
||||
}
|
||||
|
||||
public static User buildAuthorizeUser(Long id, WxOAuth2UserInfo userInfo) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setAvatar(userInfo.getHeadImgUrl());
|
||||
user.setName(userInfo.getNickname());
|
||||
user.setSex(userInfo.getSex());
|
||||
return user;
|
||||
}
|
||||
|
||||
public static UserInfoResp buildUserInfoResp(User userInfo, Integer countByValidItemId) {
|
||||
UserInfoResp userInfoResp = new UserInfoResp();
|
||||
BeanUtil.copyProperties(userInfo, userInfoResp);
|
||||
userInfoResp.setModifyNameChance(countByValidItemId);
|
||||
return userInfoResp;
|
||||
}
|
||||
|
||||
public static List<BadgeResp> buildBadgeResp(List<ItemConfig> itemConfigs, List<UserBackpack> backpacks, User user) {
|
||||
Set<Long> obtainItemSet = backpacks.stream().map(UserBackpack::getItemId).collect(Collectors.toSet());
|
||||
return itemConfigs.stream().map(a -> {
|
||||
BadgeResp resp = new BadgeResp();
|
||||
BeanUtil.copyProperties(a, resp);
|
||||
resp.setObtain(obtainItemSet.contains(a.getId()) ? YesOrNoEnum.YES.getStatus() : YesOrNoEnum.NO.getStatus());
|
||||
resp.setWearing(ObjectUtil.equal(a.getId(), user.getItemId()) ? YesOrNoEnum.YES.getStatus() : YesOrNoEnum.NO.getStatus());
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.abin.mallchat.custom.user.service.adapter;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.enums.ChatActiveStatusEnum;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberStatisticResp;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.service.ChatService;
|
||||
import com.abin.mallchat.custom.user.domain.enums.WSRespTypeEnum;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMemberResp;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.*;
|
||||
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Description: ws消息适配器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Component
|
||||
public class WSAdapter {
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
|
||||
public static WSBaseResp<WSLoginUrl> buildLoginResp(WxMpQrCodeTicket wxMpQrCodeTicket) {
|
||||
WSBaseResp<WSLoginUrl> wsBaseResp = new WSBaseResp<>();
|
||||
wsBaseResp.setType(WSRespTypeEnum.LOGIN_URL.getType());
|
||||
wsBaseResp.setData(WSLoginUrl.builder().loginUrl(wxMpQrCodeTicket.getUrl()).build());
|
||||
return wsBaseResp;
|
||||
}
|
||||
|
||||
public static WSBaseResp<WSLoginSuccess> buildLoginSuccessResp(User user, String token) {
|
||||
WSBaseResp<WSLoginSuccess> wsBaseResp = new WSBaseResp<>();
|
||||
wsBaseResp.setType(WSRespTypeEnum.LOGIN_SUCCESS.getType());
|
||||
WSLoginSuccess wsLoginSuccess = WSLoginSuccess.builder()
|
||||
.avatar(user.getAvatar())
|
||||
.name(user.getName())
|
||||
.token(token)
|
||||
.uid(user.getId())
|
||||
.build();
|
||||
wsBaseResp.setData(wsLoginSuccess);
|
||||
return wsBaseResp;
|
||||
}
|
||||
|
||||
public static WSBaseResp buildScanSuccessResp() {
|
||||
WSBaseResp wsBaseResp = new WSBaseResp();
|
||||
wsBaseResp.setType(WSRespTypeEnum.LOGIN_SCAN_SUCCESS.getType());
|
||||
return wsBaseResp;
|
||||
}
|
||||
|
||||
public WSBaseResp<WSOnlineOfflineNotify> buildOnlineNotifyResp(User user) {
|
||||
WSBaseResp<WSOnlineOfflineNotify> wsBaseResp = new WSBaseResp<>();
|
||||
wsBaseResp.setType(WSRespTypeEnum.ONLINE_OFFLINE_NOTIFY.getType());
|
||||
WSOnlineOfflineNotify onlineOfflineNotify = new WSOnlineOfflineNotify();
|
||||
onlineOfflineNotify.setChangeList(Collections.singletonList(buildOnlineInfo(user)));
|
||||
assembleNum(onlineOfflineNotify);
|
||||
wsBaseResp.setData(onlineOfflineNotify);
|
||||
return wsBaseResp;
|
||||
}
|
||||
|
||||
public WSBaseResp<WSOnlineOfflineNotify> buildOfflineNotifyResp(User user) {
|
||||
WSBaseResp<WSOnlineOfflineNotify> wsBaseResp = new WSBaseResp<>();
|
||||
wsBaseResp.setType(WSRespTypeEnum.ONLINE_OFFLINE_NOTIFY.getType());
|
||||
WSOnlineOfflineNotify onlineOfflineNotify = new WSOnlineOfflineNotify();
|
||||
onlineOfflineNotify.setChangeList(Collections.singletonList(buildOfflineInfo(user)));
|
||||
assembleNum(onlineOfflineNotify);
|
||||
wsBaseResp.setData(onlineOfflineNotify);
|
||||
return wsBaseResp;
|
||||
}
|
||||
|
||||
private void assembleNum(WSOnlineOfflineNotify onlineOfflineNotify) {
|
||||
ChatMemberStatisticResp memberStatistic = chatService.getMemberStatistic();
|
||||
onlineOfflineNotify.setOnlineNum(memberStatistic.getOnlineNum());
|
||||
onlineOfflineNotify.setTotalNum(memberStatistic.getTotalNum());
|
||||
}
|
||||
|
||||
private static ChatMemberResp buildOnlineInfo(User user) {
|
||||
ChatMemberResp info = new ChatMemberResp();
|
||||
BeanUtil.copyProperties(user, info);
|
||||
info.setUid(user.getId());
|
||||
info.setActiveStatus(ChatActiveStatusEnum.ONLINE.getStatus());
|
||||
info.setLastOptTime(user.getLastOptTime());
|
||||
return info;
|
||||
}
|
||||
|
||||
private static ChatMemberResp buildOfflineInfo(User user) {
|
||||
ChatMemberResp info = new ChatMemberResp();
|
||||
BeanUtil.copyProperties(user, info);
|
||||
info.setUid(user.getId());
|
||||
info.setActiveStatus(ChatActiveStatusEnum.OFFLINE.getStatus());
|
||||
info.setLastOptTime(user.getLastOptTime());
|
||||
return info;
|
||||
}
|
||||
|
||||
public static WSBaseResp<WSLoginSuccess> buildInvalidateTokenResp() {
|
||||
WSBaseResp<WSLoginSuccess> wsBaseResp = new WSBaseResp<>();
|
||||
wsBaseResp.setType(WSRespTypeEnum.INVALIDATE_TOKEN.getType());
|
||||
return wsBaseResp;
|
||||
}
|
||||
|
||||
public static WSBaseResp<ChatMessageResp> buildMsgSend(ChatMessageResp msgResp) {
|
||||
WSBaseResp<ChatMessageResp> wsBaseResp = new WSBaseResp<>();
|
||||
wsBaseResp.setType(WSRespTypeEnum.MESSAGE.getType());
|
||||
wsBaseResp.setData(msgResp);
|
||||
return wsBaseResp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.abin.mallchat.custom.user.service.handler;
|
||||
|
||||
import me.chanjar.weixin.mp.api.WxMpMessageHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
public abstract class AbstractHandler implements WxMpMessageHandler {
|
||||
protected Logger logger = LoggerFactory.getLogger(getClass());
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.abin.mallchat.custom.user.service.handler;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.session.WxSessionManager;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class LogHandler extends AbstractHandler {
|
||||
@Override
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
|
||||
Map<String, Object> context, WxMpService wxMpService,
|
||||
WxSessionManager sessionManager) {
|
||||
log.info("\n接收到请求消息,内容:{}", JSONUtil.toJsonStr(wxMessage));
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.abin.mallchat.custom.user.service.handler;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.abin.mallchat.custom.user.service.adapter.TextBuilder;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.session.WxSessionManager;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
@Component
|
||||
public class MsgHandler extends AbstractHandler {
|
||||
|
||||
@Override
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
|
||||
Map<String, Object> context, WxMpService weixinService,
|
||||
WxSessionManager sessionManager) {
|
||||
|
||||
if (!wxMessage.getMsgType().equals(XmlMsgType.EVENT)) {
|
||||
//可以选择将消息保存到本地
|
||||
}
|
||||
|
||||
//当用户输入关键词如“你好”,“客服”等,并且有客服在线时,把消息转发给在线客服
|
||||
try {
|
||||
if (StringUtils.startsWithAny(wxMessage.getContent(), "你好", "客服")
|
||||
&& weixinService.getKefuService().kfOnlineList()
|
||||
.getKfOnlineList().size() > 0) {
|
||||
return WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE()
|
||||
.fromUser(wxMessage.getToUser())
|
||||
.toUser(wxMessage.getFromUser()).build();
|
||||
}
|
||||
} catch (WxErrorException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//组装回复消息
|
||||
String content = "收到信息内容:" + JSONUtil.toJsonStr(wxMessage);
|
||||
|
||||
return new TextBuilder().build(content, wxMessage, weixinService);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.abin.mallchat.custom.user.service.handler;
|
||||
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.user.service.LoginService;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.WxMsgService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.TextBuilder;
|
||||
import com.abin.mallchat.custom.user.service.adapter.UserAdapter;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.session.WxSessionManager;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import me.chanjar.weixin.mp.bean.result.WxMpUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class ScanHandler extends AbstractHandler {
|
||||
|
||||
|
||||
@Autowired
|
||||
private WxMsgService wxMsgService;
|
||||
@Override
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
|
||||
WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
|
||||
// 扫码事件处理
|
||||
return wxMsgService.scan(wxMpService,wxMpXmlMessage);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.abin.mallchat.custom.user.service.handler;
|
||||
|
||||
import com.abin.mallchat.custom.user.service.WxMsgService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.TextBuilder;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.session.WxSessionManager;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import me.chanjar.weixin.mp.bean.result.WxMpUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="https://github.com/binarywang">Binary Wang</a>
|
||||
*/
|
||||
@Component
|
||||
public class SubscribeHandler extends AbstractHandler {
|
||||
@Autowired
|
||||
private WxMsgService wxMsgService;
|
||||
|
||||
@Override
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
|
||||
Map<String, Object> context, WxMpService weixinService,
|
||||
WxSessionManager sessionManager) throws WxErrorException {
|
||||
|
||||
this.logger.info("新关注用户 OPENID: " + wxMessage.getFromUser());
|
||||
|
||||
WxMpXmlOutMessage responseResult = null;
|
||||
try {
|
||||
responseResult = this.handleSpecial(weixinService, wxMessage);
|
||||
} catch (Exception e) {
|
||||
this.logger.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (responseResult != null) {
|
||||
return responseResult;
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextBuilder().build("感谢关注", wxMessage, weixinService);
|
||||
} catch (Exception e) {
|
||||
this.logger.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理特殊请求,比如如果是扫码进来的,可以做相应处理
|
||||
*/
|
||||
private WxMpXmlOutMessage handleSpecial(WxMpService weixinService, WxMpXmlMessage wxMessage)
|
||||
throws Exception {
|
||||
return wxMsgService.scan(weixinService, wxMessage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.abin.mallchat.custom.user.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.common.constant.RedisKey;
|
||||
import com.abin.mallchat.common.common.utils.JwtUtils;
|
||||
import com.abin.mallchat.common.common.utils.RedisUtils;
|
||||
import com.abin.mallchat.custom.user.service.LoginService;
|
||||
import com.auth0.jwt.interfaces.Claim;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Description: 登录相关处理类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LoginServiceImpl implements LoginService {
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
//token过期时间
|
||||
private static final Integer TOKEN_EXPIRE_DAYS = 5;
|
||||
//token续期时间
|
||||
private static final Integer TOKEN_RENEWAL_DAYS = 2;
|
||||
|
||||
/**
|
||||
* 校验token是不是有效
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public boolean verify(String token) {
|
||||
Long uid = jwtUtils.getUidOrNull(token);
|
||||
if (Objects.isNull(uid)) {
|
||||
return false;
|
||||
}
|
||||
String key = RedisKey.getKey(RedisKey.USER_TOKEN_STRING, uid);
|
||||
String realToken = redisUtils.getStr(key);
|
||||
return token.equals(realToken);//有可能token失效了,需要校验是不是和最新token一致
|
||||
}
|
||||
|
||||
@Async
|
||||
public void renewalTokenIfNecessary(String token) {
|
||||
Long uid = jwtUtils.getUidOrNull(token);
|
||||
if (Objects.isNull(uid)) {
|
||||
return;
|
||||
}
|
||||
String key = RedisKey.getKey(RedisKey.USER_TOKEN_STRING, uid);
|
||||
long expireDays = redisUtils.getExpire(key, TimeUnit.DAYS);
|
||||
if (expireDays == -2) {//不存在的key
|
||||
return;
|
||||
}
|
||||
if (expireDays < TOKEN_RENEWAL_DAYS) {//小于一天的token帮忙续期
|
||||
redisUtils.expire(key, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String login(Long uid) {
|
||||
//获取用户token
|
||||
String token = jwtUtils.createToken(uid);
|
||||
//存储到redis
|
||||
String key = RedisKey.getKey(RedisKey.USER_TOKEN_STRING, uid);
|
||||
redisUtils.set(key, token, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);//token过期用redis中心化控制,初期采用5天过期,剩1天自动续期的方案。后续可以用双token实现
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValidUid(String token) {
|
||||
return verify(token) ? jwtUtils.getUidOrNull(token) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.abin.mallchat.custom.user.service.impl;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.enums.IdempotentEnum;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.common.user.dao.ItemConfigDao;
|
||||
import com.abin.mallchat.common.user.dao.UserBackpackDao;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
|
||||
import com.abin.mallchat.common.user.domain.enums.ItemEnum;
|
||||
import com.abin.mallchat.common.user.domain.enums.ItemTypeEnum;
|
||||
import com.abin.mallchat.common.user.service.IUserBackpackService;
|
||||
import com.abin.mallchat.common.user.service.cache.ItemCache;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.common.event.UserRegisterEvent;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.ModifyNameReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.WearingBadgeReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.BadgeResp;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserInfoResp;
|
||||
import com.abin.mallchat.custom.user.service.UserService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.UserAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: 用户基础操作类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
@Autowired
|
||||
private UserBackpackDao userBackpackDao;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
private ItemConfigDao itemConfigDao;
|
||||
@Autowired
|
||||
private IUserBackpackService iUserBackpackService;
|
||||
@Autowired
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@Autowired
|
||||
private ItemCache itemCache;
|
||||
|
||||
@Override
|
||||
public UserInfoResp getUserInfo(Long uid) {
|
||||
User userInfo = userCache.getUserInfo(uid);
|
||||
Integer countByValidItemId = userBackpackDao.getCountByValidItemId(uid, ItemEnum.MODIFY_NAME_CARD.getId());
|
||||
return UserAdapter.buildUserInfoResp(userInfo, countByValidItemId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void modifyName(Long uid, ModifyNameReq req) {
|
||||
//判断改名卡够不够
|
||||
UserBackpack firstValidItem = userBackpackDao.getFirstValidItem(uid, ItemEnum.MODIFY_NAME_CARD.getId());
|
||||
AssertUtil.isNotEmpty(firstValidItem, "改名次数不够了,等后续活动送改名卡哦");
|
||||
//使用改名卡
|
||||
boolean useSuccess = userBackpackDao.invalidItem(firstValidItem.getItemId());
|
||||
if (useSuccess) {//用乐观锁,就不用分布式锁了
|
||||
//改名
|
||||
userDao.modifyName(uid, req.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BadgeResp> badges(Long uid) {
|
||||
//查询所有徽章
|
||||
List<ItemConfig> itemConfigs =itemCache.getByType(ItemTypeEnum.BADGE.getType());
|
||||
//查询用户拥有的徽章
|
||||
List<UserBackpack> backpacks = userBackpackDao.getByItemIds(uid, itemConfigs.stream().map(ItemConfig::getId).collect(Collectors.toList()));
|
||||
//查询用户当前佩戴的标签
|
||||
User user = userDao.getById(uid);
|
||||
return UserAdapter.buildBadgeResp(itemConfigs, backpacks, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void wearingBadge(Long uid, WearingBadgeReq req) {
|
||||
//确保有这个徽章
|
||||
UserBackpack firstValidItem = userBackpackDao.getFirstValidItem(uid, req.getBadgeId());
|
||||
AssertUtil.isNotEmpty(firstValidItem, "您没有这个徽章哦,快去达成条件获取吧");
|
||||
//确保物品类型是徽章
|
||||
ItemConfig itemConfig = itemConfigDao.getById(firstValidItem.getItemId());
|
||||
AssertUtil.equal(itemConfig.getType(), ItemTypeEnum.BADGE.getType(), "该徽章不可佩戴");
|
||||
//佩戴徽章
|
||||
userDao.wearingBadge(uid, req.getBadgeId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void register(String openId) {
|
||||
User insert = User.builder().openId(openId).build();
|
||||
userDao.save(insert);
|
||||
applicationEventPublisher.publishEvent(new UserRegisterEvent(this,insert));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.abin.mallchat.custom.user.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.abin.mallchat.common.common.config.ThreadPoolConfig;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.common.event.UserOfflineEvent;
|
||||
import com.abin.mallchat.custom.user.domain.dto.ws.WSChannelExtraDTO;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.ws.WSAuthorize;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.ws.WSBaseResp;
|
||||
import com.abin.mallchat.custom.common.event.UserOnlineEvent;
|
||||
import com.abin.mallchat.custom.user.service.LoginService;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
import com.abin.mallchat.custom.user.websocket.NettyUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Description: websocket处理类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-03-19 16:21
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class WebSocketServiceImpl implements WebSocketService {
|
||||
|
||||
|
||||
/**
|
||||
* 所有请求登录的code与channel关系
|
||||
* todo 有可能有人请求了二维码,就是不登录,留个坑,之后处理
|
||||
*/
|
||||
private static final ConcurrentHashMap<Integer, Channel> WAIT_LOGIN_MAP = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 所有已连接的websocket连接列表和一些额外参数
|
||||
*/
|
||||
private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
public static final int EXPIRE_SECONDS = 60 * 60;
|
||||
@Autowired
|
||||
private WxMpService wxMpService;
|
||||
@Autowired
|
||||
private LoginService loginService;
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
@Autowired
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@Autowired
|
||||
@Qualifier(ThreadPoolConfig.WS_EXECUTOR)
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
/**
|
||||
* 处理用户登录请求,需要返回一张带code的二维码
|
||||
*
|
||||
* @param channel
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void handleLoginReq(Channel channel) {
|
||||
//生成随机不重复的登录码
|
||||
Integer code = generateLoginCode(channel);
|
||||
//请求微信接口,获取登录码地址
|
||||
WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, EXPIRE_SECONDS);
|
||||
//返回给前端
|
||||
sendMsg(channel, WSAdapter.buildLoginResp(wxMpQrCodeTicket));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取不重复的登录的code,微信要求最大不超过int的存储极限
|
||||
* 防止并发,可以给方法加上synchronize,也可以使用cas乐观锁
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Integer generateLoginCode(Channel channel) {
|
||||
int code;
|
||||
do {
|
||||
code = RandomUtil.randomInt(Integer.MAX_VALUE);
|
||||
} while (WAIT_LOGIN_MAP.contains(code)
|
||||
|| Objects.nonNull(WAIT_LOGIN_MAP.putIfAbsent(code, channel)));
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有ws连接的事件
|
||||
*
|
||||
* @param channel
|
||||
*/
|
||||
@Override
|
||||
public void connect(Channel channel) {
|
||||
ONLINE_WS_MAP.put(channel, new WSChannelExtraDTO());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Channel channel) {
|
||||
WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MAP.get(channel);
|
||||
Optional<Long> uidOptional = Optional.ofNullable(wsChannelExtraDTO)
|
||||
.map(WSChannelExtraDTO::getUid);
|
||||
ONLINE_WS_MAP.remove(channel);
|
||||
if (uidOptional.isPresent()) {//已登录用户断连,需要下线通知
|
||||
User user = new User();
|
||||
user.setId(uidOptional.get());
|
||||
user.setLastOptTime(new Date());
|
||||
applicationEventPublisher.publishEvent(new UserOfflineEvent(this, user));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authorize(Channel channel, WSAuthorize wsAuthorize) {
|
||||
//校验token
|
||||
boolean verifySuccess = loginService.verify(wsAuthorize.getToken());
|
||||
if (verifySuccess) {//用户校验成功给用户登录
|
||||
User user = userDao.getById(loginService.getValidUid(wsAuthorize.getToken()));
|
||||
loginSuccess(channel, user, wsAuthorize.getToken());
|
||||
} else { //让前端的token失效
|
||||
sendMsg(channel, WSAdapter.buildInvalidateTokenResp());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功,并更新状态
|
||||
*/
|
||||
private void loginSuccess(Channel channel, User user, String token) {
|
||||
//更新上线列表
|
||||
online(channel, user.getId());
|
||||
//返回给用户登录成功
|
||||
sendMsg(channel, WSAdapter.buildLoginSuccessResp(user, token));
|
||||
//发送用户上线事件
|
||||
user.setLastOptTime(new Date());
|
||||
user.getIpInfo().refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
|
||||
applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户上线
|
||||
*/
|
||||
private void online(Channel channel, Long uid) {
|
||||
getOrInitChannelExt(channel).setUid(uid);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Boolean scanLoginSuccess(Integer loginCode, User user, String token) {
|
||||
//发送消息
|
||||
Channel channel = WAIT_LOGIN_MAP.get(loginCode);
|
||||
if (Objects.isNull(channel)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
//移除code
|
||||
WAIT_LOGIN_MAP.remove(loginCode);
|
||||
//用户登录
|
||||
loginSuccess(channel, user, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean scanSuccess(Integer loginCode) {
|
||||
Channel channel = WAIT_LOGIN_MAP.get(loginCode);
|
||||
if (Objects.isNull(channel)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
sendMsg(channel, WSAdapter.buildScanSuccessResp());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 如果在线列表不存在,就先把该channel放进在线列表
|
||||
*
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
private WSChannelExtraDTO getOrInitChannelExt(Channel channel) {
|
||||
WSChannelExtraDTO wsChannelExtraDTO =
|
||||
ONLINE_WS_MAP.getOrDefault(channel, new WSChannelExtraDTO());
|
||||
WSChannelExtraDTO old = ONLINE_WS_MAP.putIfAbsent(channel, wsChannelExtraDTO);
|
||||
return ObjectUtil.isNull(old) ? wsChannelExtraDTO : old;
|
||||
}
|
||||
|
||||
//entrySet的值不是快照数据,但是它支持遍历,所以无所谓了,不用快照也行。
|
||||
@Override
|
||||
public void sendToAllOnline(WSBaseResp wsBaseResp, Long skipUid) {
|
||||
ONLINE_WS_MAP.forEach((channel, ext) -> {
|
||||
if (ObjectUtil.equal(ext.getUid(), skipUid)) {
|
||||
return;
|
||||
}
|
||||
threadPoolTaskExecutor.execute(() -> sendMsg(channel, wsBaseResp));
|
||||
});
|
||||
}
|
||||
|
||||
private void sendMsg(Channel channel, WSBaseResp wsBaseResp) {
|
||||
channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 案例证明ConcurrentHashMap#entrySet的值不是快照数据
|
||||
*
|
||||
* @param args
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
ReentrantLock reentrantLock = new ReentrantLock();
|
||||
Condition condition = reentrantLock.newCondition();
|
||||
ConcurrentHashMap<Integer, Integer> a = new ConcurrentHashMap<>();
|
||||
a.put(1, 1);
|
||||
a.put(2, 2);
|
||||
new Thread(() -> {
|
||||
reentrantLock.lock();
|
||||
Set<Map.Entry<Integer, Integer>> entries = a.entrySet();
|
||||
System.out.println(entries);
|
||||
try {
|
||||
condition.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println(entries);
|
||||
reentrantLock.unlock();
|
||||
|
||||
}).start();
|
||||
Thread.sleep(1000);
|
||||
reentrantLock.lock();
|
||||
a.put(3, 3);
|
||||
System.out.println("haha");
|
||||
condition.signalAll();
|
||||
reentrantLock.unlock();
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
|
||||
private AttributeKey<String> key = AttributeKey.valueOf("Id");
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof FullHttpRequest) {
|
||||
HttpHeaders headers = ((FullHttpRequest) msg).headers();
|
||||
String ip = headers.get("X-Real-IP");
|
||||
if (Objects.isNull(ip)) {//如果没经过nginx,就直接获取远端地址
|
||||
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
|
||||
ip = address.getAddress().getHostAddress();
|
||||
}
|
||||
NettyUtil.setAttr(ctx.channel(), NettyUtil.IP, ip);
|
||||
}
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.omg.CORBA.PUBLIC_MEMBER;
|
||||
|
||||
/**
|
||||
* Description: netty工具类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-04-18
|
||||
*/
|
||||
|
||||
public class NettyUtil {
|
||||
|
||||
public static AttributeKey<String> IP = AttributeKey.valueOf("ip");
|
||||
|
||||
public static <T> void setAttr(Channel channel, AttributeKey<T> attributeKey, T data) {
|
||||
Attribute<T> attr = channel.attr(attributeKey);
|
||||
attr.set(data);
|
||||
}
|
||||
|
||||
public static <T> T getAttr(Channel channel, AttributeKey<T> ip) {
|
||||
return channel.attr(ip).get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class NettyWebSocketServer {
|
||||
public static final int WEB_SOCKET_PORT = 8090;
|
||||
// 创建线程池执行器
|
||||
private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
|
||||
private EventLoopGroup workerGroup = new NioEventLoopGroup(8);
|
||||
|
||||
/**
|
||||
* 启动 ws server
|
||||
*
|
||||
* @return
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
@PostConstruct
|
||||
public void start() throws InterruptedException {
|
||||
run();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
@PreDestroy
|
||||
public void destroy() throws InterruptedException {
|
||||
Future<?> future = bossGroup.shutdownGracefully();
|
||||
Future<?> future1 = workerGroup.shutdownGracefully();
|
||||
future.sync();
|
||||
future1.sync();
|
||||
log.info("关闭 ws server 成功");
|
||||
}
|
||||
|
||||
public void run() throws InterruptedException {
|
||||
// 服务器启动引导对象
|
||||
ServerBootstrap serverBootstrap = new ServerBootstrap();
|
||||
serverBootstrap.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 128)
|
||||
.option(ChannelOption.SO_KEEPALIVE, true)
|
||||
.handler(new LoggingHandler(LogLevel.INFO)) // 为 bossGroup 添加 日志处理器
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
ChannelPipeline pipeline = socketChannel.pipeline();
|
||||
//30秒客户端没有向服务器发送心跳则关闭连接
|
||||
pipeline.addLast(new IdleStateHandler(30, 0, 0));
|
||||
// 因为使用http协议,所以需要使用http的编码器,解码器
|
||||
pipeline.addLast(new HttpServerCodec());
|
||||
// 以块方式写,添加 chunkedWriter 处理器
|
||||
pipeline.addLast(new ChunkedWriteHandler());
|
||||
/**
|
||||
* 说明:
|
||||
* 1. http数据在传输过程中是分段的,HttpObjectAggregator可以把多个段聚合起来;
|
||||
* 2. 这就是为什么当浏览器发送大量数据时,就会发出多次 http请求的原因
|
||||
*/
|
||||
pipeline.addLast(new HttpObjectAggregator(8192));
|
||||
pipeline.addLast(new HttpHeadersHandler());
|
||||
/**
|
||||
* 说明:
|
||||
* 1. 对于 WebSocket,它的数据是以帧frame 的形式传递的;
|
||||
* 2. 可以看到 WebSocketFrame 下面有6个子类
|
||||
* 3. 浏览器发送请求时: ws://localhost:7000/hello 表示请求的uri
|
||||
* 4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;
|
||||
* 是通过一个状态码 101 来切换的
|
||||
*/
|
||||
pipeline.addLast(new WebSocketServerProtocolHandler("/"));
|
||||
// 自定义handler ,处理业务逻辑
|
||||
pipeline.addLast(new NettyWebSocketServerHandler());
|
||||
}
|
||||
});
|
||||
// 启动服务器,监听端口,阻塞直到启动成功
|
||||
serverBootstrap.bind(WEB_SOCKET_PORT).sync();
|
||||
System.out.println("启动成功");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.abin.mallchat.custom.user.domain.enums.WSReqTypeEnum;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.ws.WSAuthorize;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.ws.WSBaseReq;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
|
||||
|
||||
// 当web客户端连接后,触发该方法
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
getService().connect(ctx.channel());
|
||||
}
|
||||
|
||||
// 客户端离线
|
||||
@Override
|
||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||
userOffLine(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消绑定
|
||||
*
|
||||
* @param ctx
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
// 可能出现业务判断离线后再次触发 channelInactive
|
||||
log.warn("触发 channelInactive 掉线![{}]", ctx.channel().id());
|
||||
userOffLine(ctx);
|
||||
}
|
||||
|
||||
private void userOffLine(ChannelHandlerContext ctx) {
|
||||
getService().removed(ctx.channel());
|
||||
ctx.channel().close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳检查
|
||||
*
|
||||
* @param ctx
|
||||
* @param evt
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
|
||||
// 读空闲
|
||||
if (idleStateEvent.state() == IdleState.READER_IDLE) {
|
||||
// 关闭用户的连接
|
||||
userOffLine(ctx);
|
||||
}
|
||||
}
|
||||
super.userEventTriggered(ctx, evt);
|
||||
}
|
||||
|
||||
// 处理异常
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
log.warn("异常发生,异常消息 ={}" + cause.getMessage());
|
||||
ctx.channel().close();
|
||||
}
|
||||
|
||||
private WebSocketService getService() {
|
||||
return SpringUtil.getBean(WebSocketService.class);
|
||||
}
|
||||
|
||||
// 读取客户端发送的请求报文
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
|
||||
log.info("服务器端收到消息 = " + msg.text());
|
||||
WSBaseReq wsBaseReq = JSONUtil.toBean(msg.text(), WSBaseReq.class);
|
||||
WSReqTypeEnum wsReqTypeEnum = WSReqTypeEnum.of(wsBaseReq.getType());
|
||||
switch (wsReqTypeEnum) {
|
||||
case LOGIN:
|
||||
getService().handleLoginReq(ctx.channel());
|
||||
break;
|
||||
case HEARTBEAT:
|
||||
break;
|
||||
case AUTHORIZE:
|
||||
getService().authorize(ctx.channel(), JSONUtil.toBean(wsBaseReq.getData(), WSAuthorize.class));
|
||||
default:
|
||||
log.info("未知类型");
|
||||
}
|
||||
}
|
||||
}
|
||||
59
mallchat-custom-server/src/main/resources/logback.xml
Normal file
59
mallchat-custom-server/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="LOG_PATH" value="/data/logs/mallchat-custom-server"/>
|
||||
<property name="LOG_FILE" value="mallchat-custom-server"/>
|
||||
<property name="CONSOLE_LOG_PATTERN"
|
||||
value="|%level|%d{yyyy-MM-dd HH:mm:ss.SSS}|%thread|%X{tid}|uid=%X{uid}|%msg|%X{callChain}%n"/>
|
||||
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder >
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/${LOG_FILE}.log</file>
|
||||
<append>true</append>
|
||||
<encoder >
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_PATH}/archived/${LOG_FILE}.%d{dd-MM-yyyy}.log
|
||||
</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>5GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
|
||||
</appender>
|
||||
|
||||
<appender name="fileError" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<file>${LOG_PATH}/${LOG_FILE}.error.log</file>
|
||||
<append>true</append>
|
||||
<encoder >
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_PATH}/archived/${LOG_FILE}.%d{dd-MM-yyyy}.error.log
|
||||
</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>2GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="fileAppender"/>
|
||||
<appender-ref ref="fileError"/>
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user