diff --git a/db/im-platform.sql b/db/im-platform.sql
index d90976f..003b6f8 100644
--- a/db/im-platform.sql
+++ b/db/im-platform.sql
@@ -10,6 +10,7 @@ create table `im_user`(
`reason` varchar(255) default '' comment '被封禁原因',
`type` smallint default 1 comment '用户类型 1:普通用户 2:审核账户',
`signature` varchar(1024) default '' comment '个性签名',
+ `cid` varchar(255) default '' comment '客户端id,用于uni-push推送',
`last_login_time` datetime DEFAULT null comment '最后登录时间',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间',
unique key `idx_user_name`(user_name),
diff --git a/im-platform/pom.xml b/im-platform/pom.xml
index 65ef8ab..8aaab0a 100644
--- a/im-platform/pom.xml
+++ b/im-platform/pom.xml
@@ -91,6 +91,11 @@
knife4j-openapi3-jakarta-spring-boot-starter
${knife4j.version}
+
+ com.getui.push
+ restful-sdk
+ 1.0.3.0
+
${project.artifactId}
diff --git a/im-platform/src/main/java/com/bx/implatform/config/UniPushConfig.java b/im-platform/src/main/java/com/bx/implatform/config/UniPushConfig.java
new file mode 100644
index 0000000..641c3f6
--- /dev/null
+++ b/im-platform/src/main/java/com/bx/implatform/config/UniPushConfig.java
@@ -0,0 +1,44 @@
+package com.bx.implatform.config;
+
+import com.getui.push.v2.sdk.ApiHelper;
+import com.getui.push.v2.sdk.GtApiConfiguration;
+import com.getui.push.v2.sdk.api.PushApi;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author: 谢绍许
+ * @date: 2024-07-06
+ * @version: 1.0
+ */
+@Data
+@Component
+public class UniPushConfig {
+
+ @Value("${notify.uniPush.appId}")
+ private String appId;
+ @Value("${notify.uniPush.appKey}")
+ private String appKey;
+ @Value("${notify.uniPush.masterSecret}")
+ private String masterSecret;
+
+ @Bean
+ public GtApiConfiguration uniPushConfiguration(){
+ GtApiConfiguration apiConfiguration = new GtApiConfiguration();
+ apiConfiguration.setAppId(appId);
+ apiConfiguration.setAppKey(appKey);
+ apiConfiguration.setMasterSecret(masterSecret);
+ return apiConfiguration;
+ }
+
+ @Bean
+ public PushApi uniPushApi(GtApiConfiguration configuration){
+ // 实例化ApiHelper对象,用于创建接口对象
+ ApiHelper apiHelper = ApiHelper.build(configuration);
+ // 创建对象,建议复用。目前有PushApi、StatisticApi、UserApi
+ PushApi pushApi = apiHelper.creatApi(PushApi.class);
+ return pushApi;
+ }
+}
diff --git a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java
index 17ddd9f..221ddbf 100644
--- a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java
+++ b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java
@@ -10,6 +10,11 @@ public final class RedisKey {
* 已读群聊消息位置(已读最大id)
*/
public static final String IM_GROUP_READED_POSITION = "im:readed:group:position";
+
+ /**
+ * 私聊离线通知
+ */
+ public static final String IM_OFFLINE_NOTIFY_PRIVATE = "im:notify:private";
/**
* webrtc 单人通话
*/
@@ -52,4 +57,5 @@ public final class RedisKey {
*/
public static final String IM_LOCK_RTC_GROUP = "im:lock:rtc:group";
+
}
diff --git a/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java
index 30b240e..5470fac 100644
--- a/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java
+++ b/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java
@@ -25,4 +25,7 @@ public class LoginDTO {
@Schema(description = "用户密码")
private String password;
+ @ApiModelProperty(value = "用户客户端id")
+ private String cid;
+
}
diff --git a/im-platform/src/main/java/com/bx/implatform/dto/LogoutDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/LogoutDTO.java
new file mode 100644
index 0000000..4b252af
--- /dev/null
+++ b/im-platform/src/main/java/com/bx/implatform/dto/LogoutDTO.java
@@ -0,0 +1,19 @@
+package com.bx.implatform.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Data
+@ApiModel("用户登录DTO")
+public class LogoutDTO {
+
+ @ApiModelProperty(value = "用户客户端id")
+ private String cid;
+
+}
diff --git a/im-platform/src/main/java/com/bx/implatform/entity/User.java b/im-platform/src/main/java/com/bx/implatform/entity/User.java
index ca292e6..3960601 100644
--- a/im-platform/src/main/java/com/bx/implatform/entity/User.java
+++ b/im-platform/src/main/java/com/bx/implatform/entity/User.java
@@ -69,6 +69,11 @@ public class User {
*/
private String reason;
+ /**
+ * 客户端id,用于uni-push推送
+ */
+ @TableField("cid")
+ private String cid;
/**
* 最后登录时间
*/
diff --git a/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java b/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
index 2a2f44f..d60fa46 100644
--- a/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
+++ b/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
@@ -6,10 +6,12 @@ import com.bx.imclient.annotation.IMListener;
import com.bx.imclient.listener.MessageListener;
import com.bx.imcommon.enums.IMListenerType;
import com.bx.imcommon.enums.IMSendCode;
+import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMSendResult;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.service.PrivateMessageService;
+import com.bx.implatform.service.INotifyPrivateService;
import com.bx.implatform.vo.PrivateMessageVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,27 +24,53 @@ import java.util.Set;
@Slf4j
@IMListener(type = IMListenerType.PRIVATE_MESSAGE)
public class PrivateMessageListener implements MessageListener {
+
@Lazy
@Autowired
private PrivateMessageService privateMessageService;
+
+ @Lazy
+ @Autowired
+ private NotifyPrivateService uniPushService;
+
@Override
public void process(List> results) {
+ // 更新消息状态
+ updateMessageStatus(results);
+ // 推送离线通知
+ sendOfflineNotify(results);
+ }
+
+ private void updateMessageStatus(List> results) {
Set messageIds = new HashSet<>();
- for(IMSendResult result : results){
+ for (IMSendResult result : results) {
PrivateMessageVO messageInfo = result.getData();
// 更新消息状态,这里只处理成功消息,失败的消息继续保持未读状态
if (result.getCode().equals(IMSendCode.SUCCESS.code())) {
messageIds.add(messageInfo.getId());
- log.info("消息送达,消息id:{},发送者:{},接收者:{},终端:{}", messageInfo.getId(), result.getSender().getId(), result.getReceiver().getId(), result.getReceiver().getTerminal());
+ log.info("消息送达,消息id:{},发送者:{},接收者:{},终端:{}", messageInfo.getId(),
+ result.getSender().getId(), result.getReceiver().getId(), result.getReceiver().getTerminal());
}
}
- // 批量修改状态
- if(CollUtil.isNotEmpty(messageIds)){
+ // 对发送成功的消息修改状态
+ if (CollUtil.isNotEmpty(messageIds)) {
UpdateWrapper updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda().in(PrivateMessage::getId, messageIds)
- .eq(PrivateMessage::getStatus, MessageStatus.UNSEND.code())
- .set(PrivateMessage::getStatus, MessageStatus.SENDED.code());
+ .eq(PrivateMessage::getStatus, MessageStatus.UNSEND.code())
+ .set(PrivateMessage::getStatus, MessageStatus.SENDED.code());
privateMessageService.update(updateWrapper);
}
}
+
+ private void sendOfflineNotify(List> results) {
+ for (IMSendResult result : results) {
+ PrivateMessageVO messageInfo = result.getData();
+ if (result.getCode().equals(IMSendCode.SUCCESS.code()) && result.getReceiver().getTerminal()
+ .equals(IMTerminalType.APP.code())) {
+ uniPushService.sendMessage(messageInfo.getSendId(), messageInfo.getRecvId(), messageInfo.getContent());
+ log.info("推送离线通知,消息id:{},发送者:{},接收者:{}", messageInfo.getId(), result.getSender().getId(),
+ result.getReceiver().getId());
+ }
+ }
+ }
}
diff --git a/im-platform/src/main/java/com/bx/implatform/service/INotifyPrivateService.java b/im-platform/src/main/java/com/bx/implatform/service/INotifyPrivateService.java
new file mode 100644
index 0000000..323d889
--- /dev/null
+++ b/im-platform/src/main/java/com/bx/implatform/service/INotifyPrivateService.java
@@ -0,0 +1,107 @@
+package com.bx.implatform.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.bx.implatform.contant.RedisKey;
+import com.bx.implatform.entity.User;
+import com.bx.implatform.session.NotifySession;
+import com.bx.implatform.service.thirdparty.UniPushService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 私聊离线通知服务
+ *
+ * @author: blue
+ * @date: 2024-07-06
+ * @version: 1.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class INotifyPrivateService {
+ @Lazy
+ @Autowired
+ private IUserService userService;
+ private final UniPushService uniPushService;
+ private final RedisTemplate redisTemplate;
+ @Value("${notify.enable}")
+ private Boolean enable = false;
+ @Value("${notify.max.private}")
+ private Integer max = -1;
+ public void sendMessage(Long sendId, Long recvId, String content) {
+ if(!enable){
+ return;
+ }
+ NotifySession session = findNotifySession(sendId, recvId);
+ if (Objects.isNull(session)) {
+ session = createNotifySession(sendId, recvId);
+ }
+ // 未上报cid,无法推送
+ if (StrUtil.isEmpty(session.getCid())) {
+ log.info("用户'{}'未上报cid,无法推送离线通知", recvId);
+ return;
+ }
+ // 已达到最大数量
+ if (max > 0 && session.getCount() >= max) {
+ log.info("用户'{}'已到达推送数量上线,不再推送离线通知", recvId);
+ return;
+ }
+ // 消息数量加1
+ session.setCount(session.getCount()+1);
+ String body = String.format("%s:%s", session.getSendNickName(),content);
+ // 大于1条时需要展示数量
+ if (session.getCount() > 1) {
+ body = String.format("[%d条] ", session.getCount()) + body;
+ }
+ uniPushService.pushByCid(session,body);
+ // 保存会话
+ saveNotifySession(session,sendId,recvId);
+ }
+
+ public void removeNotifySession( Long recvId){
+ String key = StrUtil.join(":", RedisKey.IM_OFFLINE_NOTIFY_PRIVATE, "*", recvId);
+ Set keys = redisTemplate.keys(key);
+ redisTemplate.delete(keys);
+ }
+
+ public void removeNotifySession(Long sendId, Long recvId){
+ String key = StrUtil.join(":", RedisKey.IM_OFFLINE_NOTIFY_PRIVATE, sendId, recvId);
+ redisTemplate.delete(key);
+ }
+
+ private NotifySession createNotifySession(Long sendId, Long recvId) {
+ String key = StrUtil.join(":", RedisKey.IM_OFFLINE_NOTIFY_PRIVATE, sendId, recvId);
+ User sendUser = userService.getById(sendId);
+ User recvUser = userService.getById(recvId);
+ NotifySession session = new NotifySession();
+ session.setCount(0);
+ session.setCid(recvUser.getCid());
+ session.setTitle(sendUser.getNickName());
+ session.setSendNickName(sendUser.getNickName());
+ session.setNotifyId(Math.abs(key.hashCode()));
+ session.setLogo(sendUser.getHeadImageThumb());
+ redisTemplate.opsForValue().set(key, session, 30, TimeUnit.DAYS);
+ return session;
+ }
+
+ private NotifySession findNotifySession(Long sendId, Long recvId) {
+ String key = StrUtil.join(":", RedisKey.IM_OFFLINE_NOTIFY_PRIVATE, sendId, recvId);
+ return (NotifySession)redisTemplate.opsForValue().get(key);
+ }
+
+ private void saveNotifySession(NotifySession session, Long sendId, Long recvId) {
+ String key = StrUtil.join(":", RedisKey.IM_OFFLINE_NOTIFY_PRIVATE, sendId, recvId);
+ redisTemplate.opsForValue().set(key, session);
+ }
+
+}
diff --git a/im-platform/src/main/java/com/bx/implatform/service/UserService.java b/im-platform/src/main/java/com/bx/implatform/service/UserService.java
index 37bcc8b..b5f28e5 100644
--- a/im-platform/src/main/java/com/bx/implatform/service/UserService.java
+++ b/im-platform/src/main/java/com/bx/implatform/service/UserService.java
@@ -2,6 +2,7 @@ package com.bx.implatform.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bx.implatform.dto.LoginDTO;
+import com.bx.implatform.dto.LogoutDTO;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.dto.RegisterDTO;
import com.bx.implatform.entity.User;
@@ -21,6 +22,14 @@ public interface UserService extends IService {
*/
LoginVO login(LoginDTO dto);
+ /**
+ * 用户退出登陆
+ *
+ * @param dto 退出登陆dto
+ * @return
+ */
+ void logout(LogoutDTO dto);
+
/**
* 修改用户密码
*
diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
index 995e97d..132d4db 100644
--- a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
+++ b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
@@ -20,6 +20,7 @@ import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.mapper.PrivateMessageMapper;
import com.bx.implatform.service.FriendService;
import com.bx.implatform.service.PrivateMessageService;
+import com.bx.implatform.service.NotifyPrivateService;
import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils;
@@ -43,7 +44,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl implements Us
private final FriendService friendService;
private final JwtProperties jwtProperties;
private final IMClient imClient;
+ private final INotifyPrivateService notifyPrivateService;
@Override
public LoginVO login(LoginDTO dto) {
@@ -60,6 +66,14 @@ public class UserServiceImpl extends ServiceImpl implements Us
if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
throw new GlobalException(ResultCode.PASSWOR_ERROR);
}
+ // 更新用户登陆时间和cid
+ user.setLastLoginTime(new Date());
+ // 用户更换了设备,记录新的cid
+ if(StrUtil.isNotEmpty(dto.getCid()) && dto.getCid().equals(user.getCid())){
+ user.setCid(dto.getCid());
+ notifyPrivateService.removeNotifySession(user.getId());
+ }
+ this.updateById(user);
// 生成token
UserSession session = BeanUtils.copyProperties(user, UserSession.class);
session.setUserId(user.getId());
@@ -77,6 +91,20 @@ public class UserServiceImpl extends ServiceImpl implements Us
return vo;
}
+ @Override
+ public void logout(LogoutDTO dto) {
+ UserSession session = SessionContext.getSession();
+ if(StrUtil.isNotEmpty(dto.getCid())){
+ // 清除cid,不再推送离线通知
+ notifyPrivateService.removeNotifySession(session.getUserId());
+ LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate();
+ wrapper.eq(User::getId,session.getUserId());
+ wrapper.eq(User::getCid,dto.getCid());
+ wrapper.set(User::getCid, Strings.EMPTY);
+ this.update(wrapper);
+ }
+ }
+
@Override
public LoginVO refreshToken(String refreshToken) {
//验证 token
diff --git a/im-platform/src/main/java/com/bx/implatform/service/thirdparty/UniPushService.java b/im-platform/src/main/java/com/bx/implatform/service/thirdparty/UniPushService.java
new file mode 100644
index 0000000..f0c367b
--- /dev/null
+++ b/im-platform/src/main/java/com/bx/implatform/service/thirdparty/UniPushService.java
@@ -0,0 +1,122 @@
+package com.bx.implatform.service.thirdparty;
+
+import com.bx.implatform.session.NotifySession;
+import com.getui.push.v2.sdk.api.PushApi;
+import com.getui.push.v2.sdk.common.ApiResult;
+import com.getui.push.v2.sdk.dto.req.Audience;
+import com.getui.push.v2.sdk.dto.req.Settings;
+import com.getui.push.v2.sdk.dto.req.message.PushChannel;
+import com.getui.push.v2.sdk.dto.req.message.PushDTO;
+import com.getui.push.v2.sdk.dto.req.message.PushMessage;
+import com.getui.push.v2.sdk.dto.req.message.android.AndroidDTO;
+import com.getui.push.v2.sdk.dto.req.message.android.GTNotification;
+import com.getui.push.v2.sdk.dto.req.message.android.ThirdNotification;
+import com.getui.push.v2.sdk.dto.req.message.android.Ups;
+import com.getui.push.v2.sdk.dto.req.message.ios.Alert;
+import com.getui.push.v2.sdk.dto.req.message.ios.Aps;
+import com.getui.push.v2.sdk.dto.req.message.ios.IosDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author: 谢绍许
+ * @date: 2024-07-06
+ * @version: 1.0
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class UniPushService {
+
+ private final PushApi pushApi;
+
+ public void pushByCid(NotifySession session, String body){
+ //根据cid进行单推
+ PushDTO pushDTO = new PushDTO();
+ // 设置推送参数,requestid需要每次变化唯一
+ pushDTO.setRequestId(System.currentTimeMillis()+"");
+ Settings settings = new Settings();
+ pushDTO.setSettings(settings);
+ //消息有效期,走厂商消息必须设置该值
+ settings.setTtl(3600000);
+ //在线走个推通道时推送的消息体
+ PushMessage pushMessage = new PushMessage();
+ pushDTO.setPushMessage(pushMessage);
+ //此格式的透传消息由 unipush 做了特殊处理,会自动展示通知栏。开发者也可自定义其它格式,在客户端自己处理。
+ GTNotification gtNotification = new GTNotification();
+ gtNotification.setTitle(session.getTitle());
+ gtNotification.setBody(body);
+ gtNotification.setClickType("startapp");
+ gtNotification.setNotifyId(session.getNotifyId().toString());
+ gtNotification.setLogoUrl(session.getLogo());
+ gtNotification.setBadgeAddNum("1");
+ pushMessage.setNotification(gtNotification);
+ // 设置接收人信息
+ Audience audience = new Audience();
+ pushDTO.setAudience(audience);
+ audience.addCid(session.getCid());
+ //设置离线推送时的消息体
+ PushChannel pushChannel = new PushChannel();
+ //安卓离线厂商通道推送的消息体
+ AndroidDTO androidDTO = new AndroidDTO();
+ Ups ups = new Ups();
+ ThirdNotification thirdNotification = new ThirdNotification();
+ ups.setNotification(thirdNotification);
+ ups.setOptions(buildOptions(session.getLogo()));
+ thirdNotification.setTitle(session.getTitle());
+ thirdNotification.setBody(body);
+ thirdNotification.setNotifyId(session.getNotifyId().toString());
+ // 打开首页
+ thirdNotification.setClickType("startapp");
+ androidDTO.setUps(ups);
+ pushChannel.setAndroid(androidDTO);
+ // ios离线apn通道推送的消息体
+ Alert alert = new Alert();
+ alert.setTitle(session.getTitle());
+ alert.setBody(body);
+ Aps aps = new Aps();
+ // 0:普通通知消息 1:静默推送(无通知栏消息),静默推送时不需要填写其他参数。苹果建议1小时最多推送3条静默消息
+ aps.setContentAvailable(0);
+ // default: 系统铃声 不填:无声
+ aps.setSound("default");
+ aps.setAlert(alert);
+
+ IosDTO iosDTO = new IosDTO();
+ iosDTO.setAps(aps);
+ iosDTO.setType("notify");
+ pushChannel.setIos(iosDTO);
+ pushDTO.setPushChannel(pushChannel);
+ // 进行cid单推
+ ApiResult