Преглед на файлове

feat: 用户忙线状态改为在后端维护

xsx преди 1 година
родител
ревизия
b046eb245a

+ 35 - 0
im-platform/src/main/java/com/bx/implatform/controller/SystemController.java

@@ -0,0 +1,35 @@
+package com.bx.implatform.controller;
+
+import com.bx.implatform.config.WebrtcConfig;
+import com.bx.implatform.dto.PrivateMessageDTO;
+import com.bx.implatform.result.Result;
+import com.bx.implatform.result.ResultUtils;
+import com.bx.implatform.vo.SystemConfigVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+
+
+/**
+ * @author: blue
+ * @date: 2024-06-10
+ * @version: 1.0
+ */
+@Api(tags = "系统相关")
+@RestController
+@RequestMapping("/system")
+@RequiredArgsConstructor
+public class SystemController {
+
+    private final WebrtcConfig webrtcConfig;
+
+    @GetMapping("/config")
+    @ApiOperation(value = "加载系统配置", notes = "加载系统配置")
+    public Result<SystemConfigVO> loadConfig() {
+        return ResultUtils.success(new SystemConfigVO(webrtcConfig));
+    }
+
+
+}

+ 1 - 6
im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java

@@ -123,9 +123,4 @@ public class WebrtcGroupController {
         return ResultUtils.success();
     }
 
-    @GetMapping("/config")
-    @ApiOperation(httpMethod = "GET", value = "获取系统配置")
-    public Result<WebrtcConfig> loadConfig() {
-        return ResultUtils.success(webrtcGroupService.loadConfig());
-    }
-}
+ }

+ 5 - 5
im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java

@@ -70,10 +70,10 @@ public class WebrtcPrivateController {
         return ResultUtils.success();
     }
 
-
-    @GetMapping("/iceservers")
-    @ApiOperation(httpMethod = "GET", value = "获取iceservers")
-    public Result<List<ICEServer>> iceservers() {
-        return ResultUtils.success(webrtcPrivateService.getIceServers());
+    @ApiOperation(httpMethod = "POST", value = "获取通话信息")
+    @PostMapping("/heartbeat")
+    public Result heartbeat(@RequestParam Long uid) {
+        webrtcPrivateService.heartbeat(uid);
+        return ResultUtils.success();
     }
 }

+ 0 - 6
im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java

@@ -76,10 +76,4 @@ public interface IWebrtcGroupService {
      */
     void heartbeat(Long groupId);
 
-
-    /**
-     * 加载配置
-     */
-    WebrtcConfig loadConfig();
-
 }

+ 1 - 2
im-platform/src/main/java/com/bx/implatform/service/IWebrtcPrivateService.java

@@ -25,7 +25,6 @@ public interface IWebrtcPrivateService {
 
     void candidate(Long uid, String candidate);
 
-    List<ICEServer> getIceServers();
-
+    void heartbeat(Long uid);
 
 }

+ 1 - 6
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java

@@ -110,7 +110,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
             sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo));
         }
         // 向被邀请的用户广播消息,发起呼叫
-        List<Long> recvIds = getRecvIds(dto.getUserInfos());
+        List<Long> recvIds = getRecvIds(userInfos);
         sendRtcMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), recvIds, JSON.toJSONString(userInfos));
         // 发送文字提示信息
         WebrtcUserInfo mineInfo = findUserInfo(webrtcSession,userSession.getUserId());
@@ -465,11 +465,6 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
         userStateUtils.expire(userSession.getUserId());
     }
 
-    @Override
-    public WebrtcConfig loadConfig() {
-        return webrtcConfig;
-    }
-
     private WebrtcGroupSession getWebrtcSession(Long groupId) {
         String key = buildWebrtcSessionKey(groupId);
         WebrtcGroupSession webrtcSession = (WebrtcGroupSession)redisTemplate.opsForValue().get(key);

+ 29 - 4
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java

@@ -12,6 +12,7 @@ import com.bx.implatform.service.IWebrtcPrivateService;
 import com.bx.implatform.session.SessionContext;
 import com.bx.implatform.session.UserSession;
 import com.bx.implatform.session.WebrtcPrivateSession;
+import com.bx.implatform.util.UserStateUtils;
 import com.bx.implatform.vo.PrivateMessageVO;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -31,6 +32,7 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
     private final IMClient imClient;
     private final RedisTemplate<String, Object> redisTemplate;
     private final WebrtcConfig iceServerConfig;
+    private final UserStateUtils userStateUtils;
 
     @Override
     public void call(Long uid, String mode, String offer) {
@@ -38,12 +40,18 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
         if (!imClient.isOnline(uid)) {
             throw new GlobalException("对方目前不在线");
         }
+        if(userStateUtils.isBusy(uid)){
+            throw new GlobalException("对方正忙");
+        }
         // 创建webrtc会话
         WebrtcPrivateSession webrtcSession = new WebrtcPrivateSession();
         webrtcSession.setCallerId(session.getUserId());
         webrtcSession.setCallerTerminal(session.getTerminal());
         String key = getWebRtcSessionKey(session.getUserId(), uid);
-        redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
+        redisTemplate.opsForValue().set(key, webrtcSession, 60, TimeUnit.SECONDS);
+        // 设置用户忙线状态
+        userStateUtils.setBusy(uid);
+        userStateUtils.setBusy(session.getUserId());
         // 向对方所有终端发起呼叫
         PrivateMessageVO messageInfo = new PrivateMessageVO();
         MessageType messageType = mode.equals("video") ? MessageType.RTC_CALL_VIDEO : MessageType.RTC_CALL_VOICE;
@@ -71,7 +79,7 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
         webrtcSession.setAcceptorId(session.getUserId());
         webrtcSession.setAcceptorTerminal(session.getTerminal());
         String key = getWebRtcSessionKey(session.getUserId(), uid);
-        redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
+        redisTemplate.opsForValue().set(key, webrtcSession, 60, TimeUnit.SECONDS);
         // 向发起人推送接受通话信令
         PrivateMessageVO messageInfo = new PrivateMessageVO();
         messageInfo.setType(MessageType.RTC_ACCEPT.code());
@@ -97,6 +105,9 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
         WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
         // 删除会话信息
         removeWebrtcSession(uid, session.getUserId());
+        // 设置用户空闲状态
+        userStateUtils.setFree(uid);
+        userStateUtils.setFree(session.getUserId());
         // 向发起人推送拒绝通话信令
         PrivateMessageVO messageInfo = new PrivateMessageVO();
         messageInfo.setType(MessageType.RTC_REJECT.code());
@@ -119,6 +130,9 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
         UserSession session = SessionContext.getSession();
         // 删除会话信息
         removeWebrtcSession(session.getUserId(), uid);
+        // 设置用户空闲状态
+        userStateUtils.setFree(uid);
+        userStateUtils.setFree(session.getUserId());
         // 向对方所有终端推送取消通话信令
         PrivateMessageVO messageInfo = new PrivateMessageVO();
         messageInfo.setType(MessageType.RTC_CANCEL.code());
@@ -142,6 +156,9 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
         WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
         // 删除会话信息
         removeWebrtcSession(uid, session.getUserId());
+        // 设置用户空闲状态
+        userStateUtils.setFree(uid);
+        userStateUtils.setFree(session.getUserId());
         // 向发起方推送通话失败信令
         PrivateMessageVO messageInfo = new PrivateMessageVO();
         messageInfo.setType(MessageType.RTC_FAILED.code());
@@ -168,6 +185,9 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
         WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
         // 删除会话信息
         removeWebrtcSession(uid, session.getUserId());
+        // 设置用户空闲状态
+        userStateUtils.setFree(uid);
+        userStateUtils.setFree(session.getUserId());
         // 向对方推送挂断通话信令
         PrivateMessageVO messageInfo = new PrivateMessageVO();
         messageInfo.setType(MessageType.RTC_HANDUP.code());
@@ -210,8 +230,13 @@ public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService {
     }
 
     @Override
-    public List<ICEServer> getIceServers() {
-        return iceServerConfig.getIceServers();
+    public void heartbeat(Long uid) {
+        UserSession session = SessionContext.getSession();
+        // 会话续命
+        String key = getWebRtcSessionKey(session.getUserId(), uid);
+        redisTemplate.expire(key,60,TimeUnit.SECONDS);
+        // 用户状态续命
+        userStateUtils.expire(session.getUserId());
     }
 
     private WebrtcPrivateSession getWebrtcSession(Long userId, Long uid) {

+ 22 - 0
im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java

@@ -0,0 +1,22 @@
+package com.bx.implatform.vo;
+
+import com.bx.implatform.config.WebrtcConfig;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * @author: blue
+ * @date: 2024-06-10
+ * @version: 1.0
+ */
+@Data
+@ApiModel("系统配置VO")
+@AllArgsConstructor
+public class SystemConfigVO {
+
+    @ApiModelProperty(value = "webrtc配置")
+    private WebrtcConfig webrtc;
+
+}

+ 16 - 7
im-ui/src/components/chat/ChatPrivateVideo.vue

@@ -49,6 +49,7 @@
 				peerConnection: null,
 				videoTime: 0,
 				videoTimer: null,
+				heartbeatTimer: null,
 				candidates: [],
 				configuration: {
 					iceServers: []
@@ -78,6 +79,8 @@
 						this.accept(this.rtcInfo.offer);
 					}
 				});
+				// 开启心跳
+				this.startHeartBeat();
 			},
 			openCamera(callback) {
 				navigator.getUserMedia({
@@ -275,6 +278,7 @@
 				this.loading = false;
 				this.videoTime = 0;
 				this.videoTimer && clearInterval(this.videoTimer);
+				this.heartbeatTimer && clearInterval(this.heartbeatTimer);
 				this.audio.pause();
 				this.candidates = [];
 				if (this.peerConnection) {
@@ -295,6 +299,16 @@
 					this.videoTime++;
 				}, 1000)
 			},
+			startHeartBeat() {
+				// 每15s推送一次心跳
+				this.heartbeatTimer && clearInterval(this.heartbeatTimer);
+				this.heartbeatTimer = setInterval(() => {
+					this.$http({
+						url: `/webrtc/private/heartbeat?uid=${this.rtcInfo.friend.id}`,
+						method: 'post'
+					})
+				}, 15000)
+			},
 			handleClose() {
 				if (this.isAccepted) {
 					this.handup()
@@ -325,14 +339,9 @@
 				this.audio.loop = true;
 			},
 			initICEServers() {
-				this.$http({
-					url: '/webrtc/private/iceservers',
-					method: 'get'
-				}).then((servers) => {
-					this.configuration.iceServers = servers;
-				})
+				let iceServers = this.$store.state.configStore.webrtc.iceServers;
+				this.configuration.iceServers = iceServers;
 			}
-
 		},
 		watch: {
 			rtcState: {

+ 3 - 1
im-ui/src/store/index.js

@@ -4,12 +4,13 @@ import chatStore from './chatStore.js';
 import friendStore from './friendStore.js';
 import userStore from './userStore.js';
 import groupStore from './groupStore.js';
+import configStore from './configStore.js';
 import uiStore from './uiStore.js';
 
 Vue.use(Vuex)
 
 export default new Vuex.Store({
-	modules: {chatStore,friendStore,userStore,groupStore,uiStore},
+	modules: {chatStore,friendStore,userStore,groupStore,configStore,uiStore},
 	state: {},
 	mutations: {
 	},
@@ -20,6 +21,7 @@ export default new Vuex.Store({
 				promises.push(this.dispatch("loadFriend"));
 				promises.push(this.dispatch("loadGroup"));
 				promises.push(this.dispatch("loadChat"));
+				promises.push(this.dispatch("loadConfig"));
 				return Promise.all(promises);
 			})
 		},

+ 1 - 1
im-uniapp/App.vue

@@ -191,7 +191,7 @@
 						return;
 					// #endif
 					// 被呼叫,弹出视频页面
-					let delayTime = 10;
+					let delayTime = 100;
 					if(msg.type == enums.MESSAGE_TYPE.RTC_GROUP_SETUP){
 						let pages = getCurrentPages();
 						let curPage = pages[pages.length-1].route;

+ 14 - 2
im-uniapp/components/group-member-selector/group-member-selector.vue

@@ -19,7 +19,7 @@
 			</view>
 			<view class="member-items">
 				<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
-					<view v-for="m in members"  v-show="!m.quit && m.aliasName.startsWith(searchText)" :key="m.userId">
+					<view v-for="m in members" v-show="!m.quit && m.aliasName.startsWith(searchText)" :key="m.userId">
 						<view class="member-item" @click="onSwitchChecked(m)">
 							<head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
 								:size="90"></head-image>
@@ -42,6 +42,10 @@
 		props: {
 			members: {
 				type: Array
+			},
+			maxSize: {
+				type: Number,
+				default: -1
 			}
 		},
 		data() {
@@ -60,9 +64,17 @@
 				this.$refs.popup.open();
 			},
 			onSwitchChecked(m) {
-				if(!m.locked){
+				if (!m.locked) {
 					m.checked = !m.checked;
 				}
+				// 达到选择上限
+				if (this.maxSize > 0 && this.checkedIds.length > this.maxSize) {
+					m.checked = false;
+					uni.showToast({
+						title: `最多选择${this.maxSize}位用户`,
+						icon: "none"
+					})
+				}
 			},
 			onClean() {
 				this.members.forEach((m) => {

+ 1 - 0
im-uniapp/pages/chat/chat-box.vue

@@ -109,6 +109,7 @@
 			@complete="onAtComplete"></chat-at-box>
 		<!-- 群语音通话时选择成员 -->
 		<group-member-selector ref="selBox" :members="groupMembers"
+			:maxSize="$store.state.configStore.webrtc.maxChannel"
 			@complete="onSelectMember"></group-member-selector>
 		<group-rtc-join ref="rtcJoin" :groupId="group.id"></group-rtc-join>
 	</view>

+ 1 - 0
im-uniapp/pages/chat/chat-group-video.vue

@@ -76,6 +76,7 @@
 				this.url += "&isHost=" + this.isHost;
 				this.url += "&loginInfo=" + JSON.stringify(uni.getStorageSync("loginInfo"));
 				this.url += "&userInfos=" + JSON.stringify(this.userInfos);
+				this.url += "&config=" + JSON.stringify(this.$store.state.configStore.webrtc);
 			},
 		},
 		onBackPress() {

+ 2 - 1
im-uniapp/pages/chat/chat-private-video.vue

@@ -72,13 +72,14 @@
 				// #endif
 			},
 			initUrl(){
-				this.url = "/hybrid/html/index.html";
+				this.url = "/hybrid/html/rtc-private/index.html";
 				this.url += "?mode="+this.mode;
 				this.url += "&isHost="+this.isHost;
 				this.url += "&baseUrl="+UNI_APP.BASE_URL;
 				this.url += "&loginInfo="+JSON.stringify(uni.getStorageSync("loginInfo"));
 				this.url += "&userInfo="+JSON.stringify(this.$store.state.userStore.userInfo);
 				this.url += "&friend="+JSON.stringify(this.friend);
+				this.url += "&config=" + JSON.stringify(this.$store.state.configStore.webrtc);
 			},
 		},
 		onBackPress() {

+ 6 - 4
im-uniapp/store/index.js

@@ -2,15 +2,16 @@ import chatStore from './chatStore.js';
 import friendStore from './friendStore.js';
 import userStore from './userStore.js';
 import groupStore from './groupStore.js';
-import {
-	createStore
-} from 'vuex';
+import configStore from './configStore.js';
+import { createStore } from 'vuex';
+
 const store = createStore({
 	modules: {
 		chatStore,
 		friendStore,
 		userStore,
-		groupStore
+		groupStore,
+		configStore
 	},
 	state: {},
 	actions: {
@@ -20,6 +21,7 @@ const store = createStore({
 				promises.push(this.dispatch("loadFriend"));
 				promises.push(this.dispatch("loadGroup"));
 				promises.push(this.dispatch("loadChat"));
+				promises.push(this.dispatch("loadConfig"));
 				return Promise.all(promises);
 			})
 		},

+ 3 - 0
im-uniapp/store/userStore.js

@@ -5,6 +5,9 @@ import http from '../common/request'
 export default {
 	state: {
 		userInfo: {},
+		config:{
+			webrtc:{}
+		},
 		state: USER_STATE.FREE 
 	},