1.记录微信消息

2.重复上线问题解决
This commit is contained in:
zhongzb
2023-05-20 12:56:56 +08:00
parent a35e72ad46
commit fe1bd80082
19 changed files with 194 additions and 23 deletions

View File

@@ -1,12 +1,12 @@
![](https://s1.ax1x.com/2023/05/04/p9NC50f.png)
MallChat的后端项目是一个既能购物又能即时聊天的电商系统。致力于打造互联网企业级项目的最佳实践。电商该有的购物车订单支付推荐搜索拉新促活推送物流客服它都必须有。持续更新ing~~
MallChat的后端项目是一个既能购物又能即时聊天的电商系统。致力于打造互联网企业级项目的最佳实践。电商该有的购物车订单支付推荐搜索拉新促活推送物流客服它都必须有。持续更新ing~~(记得star啊喂)
<p align="center">
<a href="#公众号"><img src="https://img.shields.io/badge/公众号-程序员阿斌-blue.svg?style=plasticr"></a>
<a href="#公众号"><img src="https://img.shields.io/badge/交流群-加入开发-green.svg?style=plasticr"></a>
<a href="https://github.com/zongzibinbin/MallChat"><img src="https://img.shields.io/badge/github-项目地址-yellow.svg?style=plasticr"></a>
<a href="https://github.com/zongzibinbin/MallChat"><img src="https://img.shields.io/badge/码云-项目地址-orange.svg?style=plasticr"></a>
<a href="https://gitee.com/zhongzhibinbin/MallChat"><img src="https://img.shields.io/badge/码云-项目地址-orange.svg?style=plasticr"></a>
<a href="https://github.com/Evansy/MallChatWeb"><img src="https://img.shields.io/badge/前端-项目地址-blueviolet.svg?style=plasticr"></a>
</p>
@@ -16,9 +16,9 @@ MallChat的后端项目是一个既能购物又能即时聊天的电商系统
1. **快速体验地址**[抹茶聊天首页](https://mallchat.cn)
2. **前端项目仓库**[MallChatWeb](https://github.com/Evansy/MallChatWeb)
3. **项目视频记录**[Bilibili地址](https://space.bilibili.com/146719540) 全程分享项目进度,功能选型的思考,同时征集迭代建议。
4. **项目学习文档**10w+字,保姆级教学路线,环境搭建、核心功能、基建轮子、接口压测、问题记录、一个不落。可点击[抹茶项目文档](https://www.yuque.com/snab/mallcaht)查看内含500人交流大群
4. **项目学习文档**10w+字,保姆级教学路线,环境搭建、核心功能、基建轮子、接口压测、问题记录、一个不落。可点击[抹茶项目文档](https://www.yuque.com/snab/planet/cef1mcko4fve0ur3)查看内含500人交流大群
5. **项目交流群**:对抹茶感兴趣的,可以加入[交流群](#公众号)。你的每一个举动都会决定项目未来的方向。无论是提意见做产品经理还是找bug做个测试人员又或者加入开发小模块成为contributer都欢迎你的加入。
6. **码云仓库**[https://github.com/zongzibinbin/MallChat](https://github.com/zongzibinbin/MallChat) (国内访问速度更快)
6. **码云仓库**[https://gitee.com/zhongzhibinbin/MallChat](https://gitee.com/zhongzhibinbin/MallChat) (国内访问速度更快)
## 项目介绍
@@ -59,11 +59,11 @@ MallChat的后端项目是一个既能购物又能即时聊天的电商系统
### 环境搭建
在项目目录下的`application.yml`修改自己的启动环境`spring.profiles.active` = `test`然后找到同级文件`application-test.properties`,填写自己的环境配置。[星球成员](https://www.yuque.com/snab/mallcaht)提供一套测试环境配置,可直连
在项目目录下的`application.yml`修改自己的启动环境`spring.profiles.active` = `test`然后找到同级文件`application-test.properties`,填写自己的环境配置。[星球成员](https://www.yuque.com/snab/planet/cne0nel2hny8eu4i)提供一套测试环境配置,可直连
### 项目文档
保姆级教学路线,环境搭建、核心功能、基建轮子、接口压测、问题记录、项目亮点一个不落。点击[项目文档](https://www.yuque.com/snab/mallcaht)
保姆级教学路线,环境搭建、核心功能、基建轮子、接口压测、问题记录、项目亮点一个不落。点击[项目文档](https://www.yuque.com/snab/planet/cef1mcko4fve0ur3)
更多有趣功能在持续更新中。。。

View File

@@ -145,4 +145,17 @@ CREATE TABLE `user_backpack` (
INDEX `idx_update_time`(`update_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户背包表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `wx_msg`;
CREATE TABLE `wx_msg` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
`open_id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '微信openid用户标识',
`msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户消息',
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_open_id`(`open_id`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE,
INDEX `idx_update_time`(`update_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信消息表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,20 @@
package com.abin.mallchat.common.chat.dao;
import com.abin.mallchat.common.chat.domain.entity.WxMsg;
import com.abin.mallchat.common.chat.mapper.WxMsgMapper;
import com.abin.mallchat.common.chat.service.IWxMsgService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 微信消息表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-05-16
*/
@Service
public class WxMsgDao extends ServiceImpl<WxMsgMapper, WxMsg> {
}

View File

@@ -0,0 +1,60 @@
package com.abin.mallchat.common.chat.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
* 微信消息表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-05-16
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("wx_msg")
public class WxMsg implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 微信openid用户标识
*/
@TableField("open_id")
private String openId;
/**
* 用户消息
*/
@TableField("msg")
private String msg;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField("update_time")
private Date updateTime;
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.mapper;
import com.abin.mallchat.common.chat.domain.entity.WxMsg;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 微信消息表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-05-16
*/
public interface WxMsgMapper extends BaseMapper<WxMsg> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.service;
import com.abin.mallchat.common.chat.domain.entity.WxMsg;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 微信消息表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-05-16
*/
public interface IWxMsgService extends IService<WxMsg> {
}

View File

@@ -754,6 +754,9 @@ public class RedisUtils {
public static Boolean zAdd(String key, Object value, double score) {
return zAdd(key, value.toString(), score);
}
public static Boolean zIsMember(String key, Object value) {
return Objects.nonNull(stringRedisTemplate.opsForZSet().score(key,value.toString()));
}
/**
* @param key

View File

@@ -53,6 +53,11 @@ public class UserCache {
RedisUtils.zAdd(onlineKey, uid, optTime.getTime());
}
public boolean isOnline(Long uid) {
String onlineKey = RedisKey.getKey(RedisKey.ONLINE_UID_ZET);
return RedisUtils.zIsMember(onlineKey, uid);
}
//用户下线
public void offline(Long uid, Date optTime) {
String onlineKey = RedisKey.getKey(RedisKey.ONLINE_UID_ZET);

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.chat.mapper.WxMsgMapper">
</mapper>

View File

@@ -85,7 +85,7 @@ public class ChatMessageResp {
private Integer userDislike;
}
@Data
private static class Badge {
public static class Badge {
@ApiModelProperty("徽章图像")
private String img;
@ApiModelProperty("徽章说明")

View File

@@ -7,6 +7,7 @@ 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.ItemConfig;
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;
@@ -35,11 +36,11 @@ public class MessageAdapter {
}
public static List<ChatMessageResp> buildMsgResp(List<Message> messages, Map<Long, Message> replyMap, Map<Long, User> userMap, List<MessageMark> msgMark, Long receiveUid) {
public static List<ChatMessageResp> buildMsgResp(List<Message> messages, Map<Long, Message> replyMap, Map<Long, User> userMap, List<MessageMark> msgMark, Long receiveUid, Map<Long, ItemConfig> itemMap) {
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.setFromUser(buildFromUser(userMap.get(a.getFromUid()),itemMap));
resp.setMessage(buildMessage(a, replyMap, userMap, markMap.getOrDefault(a.getId(), new ArrayList<>()), receiveUid));
return resp;
})
@@ -80,11 +81,18 @@ public class MessageAdapter {
return mark;
}
private static ChatMessageResp.UserInfo buildFromUser(User fromUser) {
private static ChatMessageResp.UserInfo buildFromUser(User fromUser, Map<Long, ItemConfig> itemMap) {
ChatMessageResp.UserInfo userInfo = new ChatMessageResp.UserInfo();
userInfo.setUsername(fromUser.getName());
userInfo.setAvatar(fromUser.getAvatar());
userInfo.setUid(fromUser.getId());
if(Objects.nonNull(fromUser.getItemId())){
ChatMessageResp.Badge badge =new ChatMessageResp.Badge();
ItemConfig itemConfig = itemMap.get(fromUser.getItemId());
badge.setImg(itemConfig.getImg());
badge.setDescribe(itemConfig.getDescribe());
userInfo.setBadge(badge);
}
return userInfo;
}

View File

@@ -17,8 +17,10 @@ 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.ItemConfig;
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.ItemCache;
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;
@@ -69,6 +71,8 @@ public class ChatServiceImpl implements ChatService {
private RoomDao roomDao;
@Autowired
private MessageMarkDao messageMarkDao;
@Autowired
private ItemCache itemCache;
/**
* 发送消息
@@ -209,7 +213,8 @@ public class ChatServiceImpl implements ChatService {
return new ArrayList<>();
}
Map<Long, Message> replyMap = new HashMap<>();
Map<Long, User> userMap = new HashMap<>();
Map<Long, User> userMap;
Map<Long, ItemConfig> itemMap;
//批量查出回复的消息
List<Long> replyIds = messages.stream().map(Message::getReplyMsgId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(replyIds)) {
@@ -218,9 +223,11 @@ public class ChatServiceImpl implements ChatService {
//批量查询消息关联用户
Set<Long> uidSet = Stream.concat(replyMap.values().stream().map(Message::getFromUid), messages.stream().map(Message::getFromUid)).collect(Collectors.toSet());
userMap = userCache.getUserInfoBatch(uidSet);
//批量查询item信息
itemMap = userMap.values().stream().map(User::getItemId).distinct().filter(Objects::nonNull).map(itemCache::getById).collect(Collectors.toMap(ItemConfig::getId,Function.identity()));
//查询消息标志
List<MessageMark> msgMark = messageMarkDao.getValidMarkByMsgIdBatch(messages.stream().map(Message::getId).collect(Collectors.toList()));
return MessageAdapter.buildMsgResp(messages, replyMap, userMap, msgMark, receiveUid);
return MessageAdapter.buildMsgResp(messages, replyMap, userMap, msgMark, receiveUid,itemMap);
}
}

View File

@@ -38,7 +38,7 @@ public class UserOnlineListener {
User user = event.getUser();
userCache.online(user.getId(), user.getLastOptTime());
//推送给所有在线用户,该用户登录成功
webSocketService.sendToAllOnline(wsAdapter.buildOnlineNotifyResp(event.getUser()), event.getUser().getId());
webSocketService.sendToAllOnline(wsAdapter.buildOnlineNotifyResp(event.getUser()), null);
}
@Async

View File

@@ -59,7 +59,7 @@ public class WxPortalController {
log.error("callBack error",e);
}
RedirectView redirectView = new RedirectView();
redirectView.setUrl("https://mp.weixin.qq.com/s/MKCWzoCIzvh5G_1sK5sLoA");
redirectView.setUrl("https://mp.weixin.qq.com/s/m1SRsBG96kLJW5mPe4AVGA");
return redirectView;
}

View File

@@ -17,7 +17,7 @@ public class BadgeResp {
private Long id;
@ApiModelProperty(value = "徽章图标")
private String image;
private String img;
@ApiModelProperty(value = "徽章描述")
private String describe;

View File

@@ -91,7 +91,9 @@ public class WxMsgService {
public void authorize(WxOAuth2UserInfo userInfo) {
User user = userDao.getByOpenId(userInfo.getOpenid());
//更新用户信息
fillUserInfo(user.getId(), userInfo);
if(Objects.isNull(user.getName())){
fillUserInfo(user.getId(), userInfo);
}
//触发用户登录成功操作
Integer eventKey = OPENID_EVENT_CODE_MAP.get(userInfo.getOpenid());
login(user.getId(), eventKey);

View File

@@ -1,6 +1,8 @@
package com.abin.mallchat.custom.user.service.handler;
import cn.hutool.json.JSONUtil;
import com.abin.mallchat.common.chat.dao.WxMsgDao;
import com.abin.mallchat.common.chat.domain.entity.WxMsg;
import com.abin.mallchat.custom.user.service.adapter.TextBuilder;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.WxSessionManager;
@@ -9,6 +11,7 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@@ -21,12 +24,18 @@ import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
@Component
public class MsgHandler extends AbstractHandler {
@Autowired
private WxMsgDao wxMsgDao;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService weixinService,
WxSessionManager sessionManager) {
if (true) {
return WxMpXmlOutMessage.TEXT().build();
WxMsg msg =new WxMsg();
msg.setOpenId(wxMessage.getFromUser());
msg.setMsg(wxMessage.getContent());
wxMsgDao.save(msg);
return null;
}
if (!wxMessage.getMsgType().equals(XmlMsgType.EVENT)) {
//可以选择将消息保存到本地

View File

@@ -7,6 +7,7 @@ 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.common.common.event.UserOfflineEvent;
import com.abin.mallchat.common.user.service.cache.UserCache;
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;
@@ -51,7 +52,8 @@ public class WebSocketServiceImpl implements WebSocketService {
* 所有已连接的websocket连接列表和一些额外参数
*/
private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Channel, WSChannelExtraDTO> getOnlineMap(){
public static ConcurrentHashMap<Channel, WSChannelExtraDTO> getOnlineMap() {
return ONLINE_WS_MAP;
}
@@ -67,6 +69,8 @@ public class WebSocketServiceImpl implements WebSocketService {
@Autowired
@Qualifier(ThreadPoolConfig.WS_EXECUTOR)
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private UserCache userCache;
/**
* 处理用户登录请求需要返回一张带code的二维码
@@ -144,12 +148,14 @@ public class WebSocketServiceImpl implements WebSocketService {
//返回给用户登录成功
sendMsg(channel, WSAdapter.buildLoginSuccessResp(user, token));
//发送用户上线事件
user.setLastOptTime(new Date());
user.getIpInfo().refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
boolean online = userCache.isOnline(user.getId());
if (!online) {
user.setLastOptTime(new Date());
user.getIpInfo().refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
}
}
/**
* 用户上线
*/

View File

@@ -86,17 +86,18 @@ public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<Tex
// 读取客户端发送的请求报文
@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());
log.info("请求二维码 = " + msg.text());
break;
case HEARTBEAT:
break;
case AUTHORIZE:
getService().authorize(ctx.channel(), JSONUtil.toBean(wsBaseReq.getData(), WSAuthorize.class));
log.info("主动认证 = " + msg.text());
default:
log.info("未知类型");
}