Commit dd886e11 by hweeeeeei

Merge branch 'feature-1.4-pushParam' of…

Merge branch 'feature-1.4-pushParam' of https://gitlab.aillo.cc/hewei/wecloud_im_server into feiwa_test

 Conflicts:
	config/src/main/resources/config/application-prod.yml
parents 7b19bae6 a90e5c83
# 四恩慈善 - 后端项目 # IM 后端项目
## 框架介绍 ## 框架介绍
> 本项目使用第三方开源脚手架项目: spring-boot-plus 参考:README-zh.md > 本项目使用第三方开源脚手架项目: spring-boot-plus 参考:README-zh.md
......
package io.geekidea.springbootplus.test;
import cn.hutool.core.codec.Base64;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* 读取系统文件目录 生成apns字符串
*/
public class ApnsTest {
@Test
public void test() throws Exception {
File file = new File("/Users/giaogiao/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/f31cd9e6d7da1d15c57c40575f5c85db/Message/MessageTemp/e1180914825140f051a87348bda5cbb5/File/hipro_test_push.p12");
InputStream certificate = new FileInputStream(file);
String encode = Base64.encode(certificate);
}
}
...@@ -6,6 +6,8 @@ import com.turo.pushy.apns.PushType; ...@@ -6,6 +6,8 @@ import com.turo.pushy.apns.PushType;
import com.wecloud.im.ws.sender.IosPush; import com.wecloud.im.ws.sender.IosPush;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
...@@ -29,17 +31,22 @@ public class IosApnsBase64Test { ...@@ -29,17 +31,22 @@ public class IosApnsBase64Test {
// * @param sound rtc= "call.caf"; 否则为default // * @param sound rtc= "call.caf"; 否则为default
Map<String, Object> customProperty = new HashMap<String, Object>(10); Map<String, Object> customProperty = new HashMap<String, Object>(10);
String apnsCertificatePath = "frogsell_push_dev.p12"; // String apnsCertificatePath = "frogsell_push_dev.p12";
String deviceToken = "27c93ca84bbf17d9ff8eb05df0576ac49822db2ae1c02aa0afea83b5c3861276"; String deviceToken = "5b761f954efe7493de0bc751942e1a8355853771b66a512f5687ca05e7335e99";
String alertTitle = "你好333"; String alertTitle = "你好333";
String alertBody = "hi333"; String alertBody = "hi333";
int badge = 1; int badge = 1;
String topicBundleId = "com.jdw.frogsell"; String topicBundleId = "com.xteng.Hibro";
boolean contentAvailable = false; boolean contentAvailable = false;
InputStream certificate = IosPush.getApnsCertificate(apnsCertificatePath); // InputStream certificate = IosPush.getApnsCertificate(apnsCertificatePath);
String encode = Base64.encode(certificate); // String encode = Base64.encode(certificate);
File file = new File("/Users/giaogiao/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/f31cd9e6d7da1d15c57c40575f5c85db/Message/MessageTemp/e1180914825140f051a87348bda5cbb5/File/hipro_test_push.p12");
InputStream in = new FileInputStream(file);
String encode = Base64.encode(in);
// 解码 // 解码
byte[] decode = Base64.decode(encode); byte[] decode = Base64.decode(encode);
......
package io.geekidea.springbootplus.test; package io.geekidea.springbootplus.test;
import cn.hutool.crypto.digest.MD5;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.ws.model.request.ReceiveModel; import com.wecloud.im.ws.model.request.ReceiveModel;
import org.springframework.util.DigestUtils;
import java.util.Date; import java.util.Date;
...@@ -24,7 +24,10 @@ public class SignTest { ...@@ -24,7 +24,10 @@ public class SignTest {
*/ */
private static void getSign(String timestamp, String clientId, String appKey, String appSecret) { private static void getSign(String timestamp, String clientId, String appKey, String appSecret) {
String sign = new MD5().digestHex(timestamp + clientId + appKey + appSecret); String data = timestamp + clientId + appKey + appSecret;
// String sign = new MD5().digestHex(data);
String sign = DigestUtils.md5DigestAsHex(data.getBytes());
System.out.println("timestamp:" + timestamp); System.out.println("timestamp:" + timestamp);
System.out.println("sign:" + sign); System.out.println("sign:" + sign);
......
package com.wecloud.im.controller; package com.wecloud.im.controller;
import com.wecloud.im.entity.ImApplication; import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.param.GetClientInfoParam;
import com.wecloud.im.param.GetOnlineStatusParam; import com.wecloud.im.param.GetOnlineStatusParam;
import com.wecloud.im.param.add.ImClientDeviceInfoAdd; import com.wecloud.im.param.add.ImClientDeviceInfoAdd;
import com.wecloud.im.param.add.ImClientHeadPortraitAdd;
import com.wecloud.im.param.add.ImClientHeadPortraitAndNicknameUpdate;
import com.wecloud.im.param.add.ImClientNicknameAdd;
import com.wecloud.im.service.ImApplicationService; import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService; import com.wecloud.im.service.ImClientService;
import com.wecloud.im.vo.GetInfoListVo;
import com.wecloud.im.vo.ImOnlineStatusVo; import com.wecloud.im.vo.ImOnlineStatusVo;
import com.wecloud.im.ws.service.MangerChannelService; import com.wecloud.im.ws.service.MangerChannelService;
import io.geekidea.springbootplus.framework.common.api.ApiResult; import io.geekidea.springbootplus.framework.common.api.ApiResult;
...@@ -46,6 +51,14 @@ public class ImClientController extends BaseController { ...@@ -46,6 +51,14 @@ public class ImClientController extends BaseController {
@Autowired @Autowired
private ImApplicationService imApplicationService; private ImApplicationService imApplicationService;
@PostMapping("/infoList")
@ApiOperation(value = "根据id获取Client的头像昵称")
public ApiResult<List<GetInfoListVo>> getInfoList(@Validated(Add.class) @RequestBody GetClientInfoParam getClientInfoParam) throws Exception {
return imClientService.getInfoList(getClientInfoParam);
}
/** /**
* 添加或修改推送设备信息 * 添加或修改推送设备信息
*/ */
...@@ -57,6 +70,29 @@ public class ImClientController extends BaseController { ...@@ -57,6 +70,29 @@ public class ImClientController extends BaseController {
} }
@PostMapping("/updateHeadAndNickname")
@ApiOperation(value = "添加或修改头像和昵称")
public ApiResult<Boolean> updateHeadAndNickname(@Validated(Add.class) @RequestBody ImClientHeadPortraitAndNicknameUpdate imClientHeadPortraitAndNicknameUpdate) throws Exception {
boolean flag = imClientService.updateHeadAndNickname(imClientHeadPortraitAndNicknameUpdate);
return ApiResult.result(flag);
}
@PostMapping("/updateHeadPortrait")
@ApiOperation(value = "添加或修改头像")
public ApiResult<Boolean> updateHeadPortrait(@Validated(Add.class) @RequestBody ImClientHeadPortraitAdd imClientHeadPortraitAdd) throws Exception {
boolean flag = imClientService.updateHeadPortrait(imClientHeadPortraitAdd);
return ApiResult.result(flag);
}
@PostMapping("/updateNickname")
@ApiOperation(value = "添加或修改主昵称")
public ApiResult<Boolean> updateNickname(@Validated(Add.class) @RequestBody ImClientNicknameAdd imClientNicknameAdd) throws Exception {
boolean flag = imClientService.updateNickname(imClientNicknameAdd);
return ApiResult.result(flag);
}
/** /**
* 退出登陆 * 退出登陆
* *
......
package com.wecloud.im.controller; package com.wecloud.im.controller;
import com.wecloud.im.param.add.ImClientLeaveConversation;
import com.wecloud.im.param.add.ImClientToConversation;
import com.wecloud.im.param.add.ImConversationAttrUpdate;
import com.wecloud.im.param.add.ImConversationCreate; import com.wecloud.im.param.add.ImConversationCreate;
import com.wecloud.im.param.add.ImConversationDisplayUpdate; import com.wecloud.im.param.add.ImConversationDisplayUpdate;
import com.wecloud.im.param.add.ImConversationNameUpdate;
import com.wecloud.im.service.ImConversationService; import com.wecloud.im.service.ImConversationService;
import com.wecloud.im.vo.ImConversationCreateVo; import com.wecloud.im.vo.ImConversationCreateVo;
import com.wecloud.im.vo.MyConversationListVo; import com.wecloud.im.vo.MyConversationListVo;
...@@ -33,6 +37,56 @@ public class ImConversationController extends BaseController { ...@@ -33,6 +37,56 @@ public class ImConversationController extends BaseController {
@Autowired @Autowired
private ImConversationService imConversationService; private ImConversationService imConversationService;
/**
* 添加或修改会话名称
*/
@PostMapping("/saveOrUpdateName")
@ApiOperation(value = "添加或修改会话名称", notes = "权限:目前只有创建者有权限操作")
public ApiResult<Boolean> saveOrUpdateName(@RequestBody ImConversationNameUpdate imConversationNameUpdate) throws Exception {
return imConversationService.saveOrUpdateName(imConversationNameUpdate);
}
/**
* 添加或修改会话拓展字段
*/
@PostMapping("/saveOrUpdateAttr")
@ApiOperation(value = "添加或修改会话拓展字段", notes = "权限:所有client都权限操作")
public ApiResult<Boolean> saveOrUpdateAttr(@RequestBody ImConversationAttrUpdate imConversationAttrUpdate) throws Exception {
return imConversationService.saveOrUpdateAttr(imConversationAttrUpdate);
}
/**
* client退出会话
*/
@PostMapping("/leave")
@ApiOperation(value = "client退出会话", notes = "若是创建者退出,[创建者]权限将会转移给按加入会话时间排序的下一个client")
public ApiResult<Boolean> leaveConversation(@RequestBody ImClientLeaveConversation imClientToConversation) throws Exception {
return imConversationService.leaveConversation(imClientToConversation);
}
/**
* 将client从会话移除
*/
@PostMapping("/delClient")
@ApiOperation(value = "将client从会话移除", notes = "权限:目前只有创建者有权限操作")
public ApiResult<Boolean> delClientToConversation(@RequestBody ImClientToConversation imClientToConversation) throws Exception {
return imConversationService.delClientToConversation(imClientToConversation);
}
/**
* 将用户添加进会话
*/
@PostMapping("/addClient")
@ApiOperation(value = "将用户添加进会话", notes = "权限:会话中所有client都有权限操作")
public ApiResult<Boolean> addClientToConversation(@RequestBody ImClientToConversation imClientToConversation) throws Exception {
return imConversationService.addClientToConversation(imClientToConversation);
}
/** /**
* 创建会话 * 创建会话
*/ */
......
package com.wecloud.im.controller; package com.wecloud.im.controller;
import com.wecloud.im.param.ImConvMemeClientRemarkNameParam;
import com.wecloud.im.param.ImConversationMembersListParam;
import com.wecloud.im.param.add.ImConversationMemAttrUpdate;
import com.wecloud.im.service.ImConversationMembersService; import com.wecloud.im.service.ImConversationMembersService;
import com.wecloud.im.vo.ImConversationMemberListVo;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.controller.BaseController; import io.geekidea.springbootplus.framework.common.controller.BaseController;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* 会话成员表 控制器 * 会话成员表 控制器
* *
...@@ -23,6 +34,37 @@ public class ImConversationMembersController extends BaseController { ...@@ -23,6 +34,37 @@ public class ImConversationMembersController extends BaseController {
@Autowired @Autowired
private ImConversationMembersService imConversationMembersService; private ImConversationMembersService imConversationMembersService;
/**
* 添加或修改会话成员备注
*/
@PostMapping("/updateClientRemarkName")
@ApiOperation(value = "添加或修改会话成员备注", notes = "权限:目前只有client成员自己有权限操作")
public ApiResult<Boolean> saveOrUpdateClientRemarkName(@RequestBody ImConvMemeClientRemarkNameParam imConvMemeClientRemarkNameParam) throws Exception {
return imConversationMembersService.saveOrUpdateClientRemarkName(imConvMemeClientRemarkNameParam);
}
/**
* 添加或修改会话成员拓展字段
*/
@PostMapping("/updateAttr")
@ApiOperation(value = "添加或修改会话成员拓展字段", notes = "权限:目前只有client成员自己有权限操作")
public ApiResult<Boolean> saveOrUpdateAttr(@RequestBody ImConversationMemAttrUpdate imConversationMemAttrUpdate) throws Exception {
return imConversationMembersService.saveOrUpdateAttr(imConversationMemAttrUpdate);
}
/**
* 会话中成员表列表
*/
@PostMapping("/getList")
// @OperationLog(name = "会话中成员表列表", type = OperationLogType.PAGE)
@ApiOperation(value = "获取会话中成员表列表")
public ApiResult<List<ImConversationMemberListVo>> getImConversationMembersList(@Validated @RequestBody ImConversationMembersListParam imConversationMembersListParam) throws Exception {
// Paging<ImConversationMembersQueryVo> paging = imConversationMembersService.getImConversationMembersPageList(imConversationMembersPageParam);
// return ApiResult.ok(paging);
return ApiResult.ok(imConversationMembersService.getImConversationMembersList(imConversationMembersListParam));
}
// /** // /**
// * 添加会话成员表 // * 添加会话成员表
......
package com.wecloud.im.controller;
import com.wecloud.im.param.rtc.CandidateForwardParam;
import com.wecloud.im.param.rtc.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CreateRtcChannelResult;
import com.wecloud.im.param.rtc.JoinRtcChannelParam;
import com.wecloud.im.param.rtc.LeaveRtcChannelParam;
import com.wecloud.im.param.rtc.RejectRtcChannelParam;
import com.wecloud.im.param.rtc.SdpForwardParam;
import com.wecloud.rtc.service.RtcService;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 音视频通话 控制器
*
* @author wei
* @since 2021-10-18
*/
@Slf4j
@RestController
@RequestMapping("/rtc")
@Api(value = "音视频通话", tags = {"音视频通话"})
public class ImRtcController extends BaseController {
@Autowired
private RtcService rtcService;
/**
* 创建频道,并邀请客户端加入
*/
@PostMapping("/createAndCall")
@ApiOperation(value = "创建频道,并邀请客户端加入", notes = "创建频道,并邀请客户端加入")
public ApiResult<CreateRtcChannelResult> createAndCall(@RequestBody CreateRtcChannelParam createRtcChannelParam) throws Exception {
return rtcService.createAndCall(createRtcChannelParam);
}
@PostMapping("/join")
@ApiOperation(value = "同意进入频道", notes = "")
public ApiResult<Boolean> join(@RequestBody JoinRtcChannelParam joinRtcChannelParam) {
return rtcService.join(joinRtcChannelParam);
}
@PostMapping("/reject")
@ApiOperation(value = "拒接进入频道", notes = "")
public ApiResult<Boolean> reject(@RequestBody RejectRtcChannelParam rejectRtcChannelParam) {
return rtcService.reject(rejectRtcChannelParam);
}
@PostMapping("/leave")
@ApiOperation(value = "主动挂断(离开频道)", notes = "")
public ApiResult<Boolean> leave(@RequestBody LeaveRtcChannelParam leaveRtcChannelParam) {
return rtcService.leave(leaveRtcChannelParam);
}
@PostMapping("/sdpForward")
@ApiOperation(value = "SDP数据转发", notes = "")
public ApiResult<Boolean> sdpForward(@RequestBody SdpForwardParam sdpForwardParam) {
return rtcService.sdpForward(sdpForwardParam);
}
@PostMapping("/candidateForward")
@ApiOperation(value = "candidate候选者数据转发", notes = "")
public ApiResult<Boolean> candidateForward(@RequestBody CandidateForwardParam candidateForwardParam) {
return rtcService.candidateForward(candidateForwardParam);
}
}
...@@ -8,7 +8,7 @@ import io.swagger.annotations.Api; ...@@ -8,7 +8,7 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
...@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/signDemo") @RequestMapping("/signDemo")
@Api(value = "sign API", tags = {"获取sign(Demo)"}) @Api(value = "signAPI", tags = {"获取sign(Demo)"})
public class SignController extends BaseController { public class SignController extends BaseController {
@Autowired @Autowired
...@@ -31,9 +31,9 @@ public class SignController extends BaseController { ...@@ -31,9 +31,9 @@ public class SignController extends BaseController {
/** /**
* 根据客户方生成签名字符串 验证通过则下发token * 根据客户方生成签名字符串 验证通过则下发token
*/ */
@GetMapping("/get") @PostMapping("/get")
@ApiOperation(value = "获取sign(仅测试使用)", notes = "生成签名") @ApiOperation(value = "获取sign(仅提供测试调试使用)", notes = "生成签名测试,在生产环境中,此步骤需要第三方应用的服务端进行生成")
public String verify(@RequestBody GetSignParam getSignParam) throws Exception { public String get(@RequestBody GetSignParam getSignParam) throws Exception {
return new MD5().digestHex(getSignParam.getTimestamp() + getSignParam.getClientId() + getSignParam.getAppKey() + getSignParam.getAppSecret()); return new MD5().digestHex(getSignParam.getTimestamp() + getSignParam.getClientId() + getSignParam.getAppKey() + getSignParam.getAppSecret());
} }
......
...@@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/token") @RequestMapping("/token")
@Api(value = "token API", tags = {"token相关"}) @Api(value = "token API", tags = {"申请token相关"})
public class TokenController extends BaseController { public class TokenController extends BaseController {
@Autowired @Autowired
...@@ -33,8 +33,8 @@ public class TokenController extends BaseController { ...@@ -33,8 +33,8 @@ public class TokenController extends BaseController {
* 根据客户方生成签名字符串 验证通过则下发token * 根据客户方生成签名字符串 验证通过则下发token
*/ */
@PostMapping("/verify") @PostMapping("/verify")
@ApiOperation(value = "验证sign,并返回token", notes = "根据客户方生成签名字符串 验证通过则下发token") @ApiOperation(value = "根据sign申请token", notes = "校验客户方生成的签名字符串,验证通过则下发token")
public ApiResult<TokenVo> verify(@RequestBody ImTokenVerify imTokenVerify) throws Exception { public ApiResult<TokenVo> verify(@RequestBody ImTokenVerify imTokenVerify) {
return imClientLoginService.verifySign(imTokenVerify); return imClientLoginService.verifySign(imTokenVerify);
} }
......
package com.wecloud.im.controller.serverapi;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.param.ApiImConversationMembersPageParam;
import com.wecloud.im.param.ApiImConversationMembersQueryVo;
import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImConversationMembersService;
import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.controller.BaseController;
import io.geekidea.springbootplus.framework.log.annotation.OperationLog;
import io.geekidea.springbootplus.framework.log.enums.OperationLogType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 会话成员表 控制器
*
* @author wei
* @since 2021-05-07
*/
@Slf4j
@RestController
@RequestMapping("/server/imConversationMembers")
@Api(value = "服务端rest-API-会话成员表", tags = {"服务端API-会话成员表"})
public class ApiImConversationMembersController extends BaseController {
@Autowired
private ImConversationMembersService imConversationMembersService;
@Autowired
private ImApplicationService imApplicationService;
/**
* 会话成员表分页列表
*/
@PostMapping("/findList")
@OperationLog(name = "rest-api-会话成员表分页列表", type = OperationLogType.PAGE)
@ApiOperation(value = "rest-api-会话成员表分页列表")
public ApiResult<List<ApiImConversationMembersQueryVo>> getApiImConversationMembersList(@Validated @RequestBody ApiImConversationMembersPageParam apiImConversationMembersPageParam, @RequestHeader String appkey, @RequestHeader String appSecret) throws Exception {
// 根据appKey从数据库查询密钥
ImApplication imApplication = imApplicationService.getOneByAppKey(appkey);
if (imApplication == null) {
return ApiResult.result(ApiCode.FAIL, null);
}
// 校验appkey 和appSecret
if (!imApplication.getAppSecret().equals(appSecret)) {
return ApiResult.result(ApiCode.FAIL, null);
}
return imConversationMembersService.getRestApiImConversationMembersList(apiImConversationMembersPageParam, imApplication);
}
// /**
// * 添加会话成员表
// */
// @PostMapping("/add")
// @OperationLog(name = "添加会话成员表", type = OperationLogType.ADD)
// @ApiOperation(value = "添加会话成员表")
// public ApiResult<Boolean> addImConversationMembers(@Validated(Add.class) @RequestBody ImConversationMembers imConversationMembers) throws Exception {
// boolean flag = imConversationMembersService.saveImConversationMembers(imConversationMembers);
// return ApiResult.result(flag);
// }
//
// /**
// * 修改会话成员表
// */
// @PostMapping("/update")
// @OperationLog(name = "修改会话成员表", type = OperationLogType.UPDATE)
// @ApiOperation(value = "修改会话成员表")
// public ApiResult<Boolean> updateImConversationMembers(@Validated(Update.class) @RequestBody ImConversationMembers imConversationMembers) throws Exception {
// boolean flag = imConversationMembersService.updateImConversationMembers(imConversationMembers);
// return ApiResult.result(flag);
// }
//
// /**
// * 删除会话成员表
// */
// @PostMapping("/delete/{id}")
// @OperationLog(name = "删除会话成员表", type = OperationLogType.DELETE)
// @ApiOperation(value = "删除会话成员表")
// public ApiResult<Boolean> deleteImConversationMembers(@PathVariable("id") Long id) throws Exception {
// boolean flag = imConversationMembersService.deleteImConversationMembers(id);
// return ApiResult.result(flag);
// }
//
// /**
// * 获取会话成员表详情
// */
// @GetMapping("/info/{id}")
// @OperationLog(name = "会话成员表详情", type = OperationLogType.INFO)
// @ApiOperation(value = "会话成员表详情")
// public ApiResult<ImConversationMembersQueryVo> getImConversationMembers(@PathVariable("id") Long id) throws Exception {
// ImConversationMembersQueryVo imConversationMembersQueryVo = imConversationMembersService.getImConversationMembersById(id);
// return ApiResult.ok(imConversationMembersQueryVo);
// }
//
}
package com.wecloud.im.controller.serverapi;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.param.add.ImMsgSendToOnlineClient;
import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImMessageService;
import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 消息存储表 控制器
*
* @author wei
* @since 2021-04-29
*/
@Slf4j
@RestController
@RequestMapping("/server/imMessage")
@Api(value = "服务端rest-API-消息", tags = {"服务端rest-API-消息"})
public class ApiImMessageController extends BaseController {
@Autowired
private ImApplicationService imApplicationService;
@Autowired
private ImMessageService imMessageService;
/**
* 向会话中在线client,下发透传消息
*/
@PostMapping("/sendToOnlineClient")
@ApiOperation(value = "向会话中在线client,下发透传消息", notes = "应用服务端向某会话中所有client下发透传消息, 不会保存进离线消息, 仅在线client能收到")
public ApiResult<Boolean> restApiImMessageSend(@RequestBody ImMsgSendToOnlineClient imMsgSendToOnlineClient, @RequestHeader String appkey, @RequestHeader String appSecret) throws Exception {
// return imMessageService.updateMsgWithdrawById(imMsgRecall);
// 根据appKey从数据库查询密钥
ImApplication imApplication = imApplicationService.getOneByAppKey(appkey);
if (imApplication == null) {
return ApiResult.result(ApiCode.FAIL, null);
}
// 校验appkey 和appSecret
if (!imApplication.getAppSecret().equals(appSecret)) {
return ApiResult.result(ApiCode.FAIL, null);
}
return imMessageService.restApiImMessageSend(imMsgSendToOnlineClient, imApplication);
}
}
package com.wecloud.im.entity;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 消息在线推送
*
* @author wei
* @since 2021-04-29
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImApiMessageOnlineSend ")
public class ImApiMessageOnlineSend extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("创建时间")
private Date createTime;
@ApiModelProperty("内容")
private String content;
@ApiModelProperty("会话id")
private Long conversationId;
}
...@@ -56,4 +56,9 @@ public class ImClient extends BaseEntity { ...@@ -56,4 +56,9 @@ public class ImClient extends BaseEntity {
@ApiModelProperty("设备推送token") @ApiModelProperty("设备推送token")
private String deviceToken; private String deviceToken;
@ApiModelProperty("头像")
private String headPortrait;
@ApiModelProperty("主昵称")
private String nickname;
} }
...@@ -48,7 +48,13 @@ public class ImConversationMembers extends BaseEntity { ...@@ -48,7 +48,13 @@ public class ImConversationMembers extends BaseEntity {
@ApiModelProperty("客户端id") @ApiModelProperty("客户端id")
private Long fkClientId; private Long fkClientId;
@ApiModelProperty("可选 自定义属性,供开发者扩展使用。")
private String attributes;
@NotNull(message = "单向删除(隐藏)会话, 0不显示, 1显示不能为空") @NotNull(message = "单向删除(隐藏)会话, 0不显示, 1显示不能为空")
@ApiModelProperty("单向删除(隐藏)会话, 0不显示, 1显示") @ApiModelProperty("单向删除(隐藏)会话, 0不显示, 1显示")
private Long displayStatus; private Long displayStatus;
@ApiModelProperty("会话中client的备注名")
private String clientRemarkName;
} }
package com.wecloud.im.executor;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.wecloud.im.ws.model.WsConstants;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 业务处理线程池
*/
public class BusinessThreadPool {
private final static ThreadFactory BUSINESS_THREAD_FACTORY = new ThreadFactoryBuilder()
.setNamePrefix("-business-").build();
/**
* 业务处理线程池
*/
public final static ExecutorService BUSINESS_TASK_THREAD_POOL_EXECUTOR =
new ThreadPoolExecutor(WsConstants.CPU_PROCESSORS, WsConstants.CPU_PROCESSORS * 2,
60L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024 * 2), BUSINESS_THREAD_FACTORY, new ThreadPoolExecutor.CallerRunsPolicy());
}
...@@ -6,10 +6,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; ...@@ -6,10 +6,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wecloud.im.entity.ImClient; import com.wecloud.im.entity.ImClient;
import com.wecloud.im.param.ImClientPageParam; import com.wecloud.im.param.ImClientPageParam;
import com.wecloud.im.param.ImClientQueryVo; import com.wecloud.im.param.ImClientQueryVo;
import com.wecloud.im.vo.GetInfoListVo;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* 终端表 Mapper 接口 * 终端表 Mapper 接口
...@@ -41,4 +43,6 @@ public interface ImClientMapper extends BaseMapper<ImClient> { ...@@ -41,4 +43,6 @@ public interface ImClientMapper extends BaseMapper<ImClient> {
int removeOldToken(@Param("appId") Long appId, @Param("deviceToken") String deviceToken); int removeOldToken(@Param("appId") Long appId, @Param("deviceToken") String deviceToken);
List<GetInfoListVo> getInfoList(@Param("appId") Long appId, @Param("conversationId") Long conversationId, @Param("clientIds") List<String> clientIds);
} }
...@@ -77,7 +77,7 @@ public interface ImConversationMapper extends BaseMapper<ImConversation> { ...@@ -77,7 +77,7 @@ public interface ImConversationMapper extends BaseMapper<ImConversation> {
/** /**
* 查询已经存在的会话信息 * 查询已经存在的一对一会话信息
* *
* @param clientId1 * @param clientId1
* @param clientId2 * @param clientId2
......
...@@ -4,12 +4,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; ...@@ -4,12 +4,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wecloud.im.entity.ImConversationMembers; import com.wecloud.im.entity.ImConversationMembers;
import com.wecloud.im.param.ApiImConversationMembersQueryVo;
import com.wecloud.im.param.ImConversationMembersPageParam; import com.wecloud.im.param.ImConversationMembersPageParam;
import com.wecloud.im.param.ImConversationMembersQueryVo; import com.wecloud.im.param.ImConversationMembersQueryVo;
import com.wecloud.im.vo.ImConversationMemberListVo;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* 会话成员表 Mapper 接口 * 会话成员表 Mapper 接口
...@@ -28,6 +31,12 @@ public interface ImConversationMembersMapper extends BaseMapper<ImConversationMe ...@@ -28,6 +31,12 @@ public interface ImConversationMembersMapper extends BaseMapper<ImConversationMe
*/ */
ImConversationMembersQueryVo getImConversationMembersById(Serializable id); ImConversationMembersQueryVo getImConversationMembersById(Serializable id);
List<ApiImConversationMembersQueryVo> getRestApiImConversationMembersList(@Param("conversationId") Long conversationId);
List<ImConversationMemberListVo> getImConversationMembersList(@Param("conversationId") Long conversationId);
/** /**
* 获取分页对象 * 获取分页对象
* *
......
...@@ -8,10 +8,11 @@ import io.netty.handler.codec.http.HttpObjectAggregator; ...@@ -8,10 +8,11 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component @Component
public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> { public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
...@@ -30,10 +31,20 @@ public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> { ...@@ -30,10 +31,20 @@ public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
pipeline.addLast("SingleHttpRequestHandler", channelInboundHandler); pipeline.addLast("SingleHttpRequestHandler", channelInboundHandler);
// 连接超时管理 (判断通道是否有数据写入) // 连接超时管理 (判断通道是否有数据写入)
pipeline.addLast("readTimeoutHandler", new ReadTimeoutHandler(60)); // pipeline.addLast("readTimeoutHandler", new ReadTimeoutHandler(60));
// "/appws"路径 升级长连接 // "/appws"路径 升级长连接
pipeline.addLast("appWebSocketServerotocolHandler", new WebSocketServerProtocolHandler(WsConstants.WS_URL)); pipeline.addLast("appWebSocketServerotocolHandler", new WebSocketServerProtocolHandler(WsConstants.WS_URL));
/*
* 心跳机制
* observeOutput -当评估写空闲时是否应该考虑字节的消耗。默认为false。
* readerIdleTime—状态为IdleState的IdleStateEvent。当在指定的时间内没有执行读操作时,将触发READER_IDLE。指定0禁用。
* writerIdleTime—状态为IdleState的IdleStateEvent。当在指定的时间内没有执行写操作时,会触发WRITER_IDLE。指定0禁用。
* allIdleTime—状态为IdleState的IdleStateEvent。在一定时间内不读不写会触发ALL_IDLE。指定0禁用。
* unit—readerIdleTime、writeIdleTime和allIdleTime的时间单位
*/
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
} }
} }
package com.wecloud.im.netty.core; package com.wecloud.im.netty.core;
import cn.hutool.core.thread.ThreadFactoryBuilder; import com.wecloud.im.executor.BusinessThreadPool;
import com.wecloud.im.ws.model.WsConstants;
import com.wecloud.im.ws.receive.ReadWsData; import com.wecloud.im.ws.receive.ReadWsData;
import com.wecloud.im.ws.service.MangerChannelService; import com.wecloud.im.ws.service.MangerChannelService;
import com.wecloud.rtc.service.RtcService;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** /**
* @Description app端 长连接事件处理 * @Description app端 长连接事件处理
...@@ -28,26 +25,23 @@ import java.util.concurrent.TimeUnit; ...@@ -28,26 +25,23 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final String PING = "ping";
private static final String PONG = "pong";
@Resource @Resource
private ReadWsData readWsData; private ReadWsData readWsData;
@Autowired
private RtcService rtcService;
@Resource @Resource
private MangerChannelService mangerChannelService; private MangerChannelService mangerChannelService;
private final static ThreadFactory NAMED_THREAD_FACTORY = new ThreadFactoryBuilder()
.setNamePrefix("WS-business-").build();
/**
* 耗时核心业务处理线程池
* 属于io密集型业务
* io密集型任务配置尽可能多的线程数量
*/
private final static ExecutorService TASK_THREAD_POOL_EXECUTOR =
new ThreadPoolExecutor(WsConstants.CPU_PROCESSORS * 5, WsConstants.CPU_PROCESSORS * 50,
3L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1), NAMED_THREAD_FACTORY, new ThreadPoolExecutor.CallerRunsPolicy());
@Override @Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) { protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
ctx.channel().attr(MangerChannelService.READ_IDLE_TIMES).set(0);// 读空闲的计数清零
String data = msg.text(); String data = msg.text();
try { try {
if (data.isEmpty()) { if (data.isEmpty()) {
...@@ -56,13 +50,13 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram ...@@ -56,13 +50,13 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
/* /*
* 在此进入耗时业务线程池, 将不再阻塞netty的I/O线程,提高网络吞吐 * 在此进入耗时业务线程池, 将不再阻塞netty的I/O线程,提高网络吞吐
*/ */
TASK_THREAD_POOL_EXECUTOR.execute(() -> BusinessThreadPool.BUSINESS_TASK_THREAD_POOL_EXECUTOR.execute(() ->
execute(ctx, data) execute(ctx, data)
); );
} catch (Exception e) { } catch (Exception e) {
//返回错误 //返回错误
ctx.channel().writeAndFlush(new TextWebSocketFrame("error=" + e.toString() + ",data=" + data)); ctx.channel().writeAndFlush(new TextWebSocketFrame("error=" + e + ",data=" + data));
log.error(e.getMessage() + data, e); log.error(e.getMessage() + data, e);
} }
} }
...@@ -71,12 +65,59 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram ...@@ -71,12 +65,59 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
String appKey = ctx.channel().attr(MangerChannelService.APP_KEY).get(); String appKey = ctx.channel().attr(MangerChannelService.APP_KEY).get();
String clientId = ctx.channel().attr(MangerChannelService.CLIENT_ID).get(); String clientId = ctx.channel().attr(MangerChannelService.CLIENT_ID).get();
try { try {
if (PING.equals(data)) {
log.info("收到心跳clientId:" + clientId);
ctx.channel().writeAndFlush(new TextWebSocketFrame(PONG));
return;
}
if (PONG.equals(data)) {
log.info("收到心跳应用Pong,clientId:" + clientId);
return;
}
readWsData.convertModel(data, ctx, appKey, clientId); readWsData.convertModel(data, ctx, appKey, clientId);
} catch (Exception e) { } catch (Exception e) {
log.error("系统繁忙data:" + data + ",appKey:" + appKey + ",clientId:" + clientId + log.error("系统繁忙data:" + data + ",appKey:" + appKey + ",clientId:" + clientId +
",channelId:" + ctx.channel().id().asShortText(), e); ",channelId:" + ctx.channel().id().asLongText(), e);
} }
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
String clientId = ctx.channel().attr(MangerChannelService.CLIENT_ID).get();
//读超时计时器
Integer readIdleTimes = ctx.channel().attr(MangerChannelService.READ_IDLE_TIMES).get();
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲:readIdleTimes=" + readIdleTimes;
ctx.channel().attr(MangerChannelService.READ_IDLE_TIMES).set(readIdleTimes + 1);// 读空闲的计数加1
// 发ping
ctx.channel().writeAndFlush(new TextWebSocketFrame(PING));
break;
case WRITER_IDLE:
eventType = "写空闲";
// 不处理
break;
case ALL_IDLE:
eventType = "读写空闲";
// 不处理
break;
}
log.info(clientId + "超时事件:" + eventType);
if (readIdleTimes >= 5) {
log.info(clientId + ".读空闲超过5次关闭连接");
ctx.channel().close();
}
} }
...@@ -90,13 +131,13 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram ...@@ -90,13 +131,13 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// @Override // @Override
// public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// String userIdByChannel = mangerChannelService.getInfoByChannel(ctx); // String userIdByChannel = mangerChannelService.getInfoByChannel(ctx);
// log.info("uid:" + userIdByChannel + ",ws异常,channelId:" + ctx.channel().id().asShortText(), cause); // log.info("uid:" + userIdByChannel + ",ws异常,channelId:" + ctx.channel().id().asLongText(), cause);
// } // }
@Override @Override
public void handlerAdded(ChannelHandlerContext ctx) { public void handlerAdded(ChannelHandlerContext ctx) {
String userIdByChannel = mangerChannelService.getInfoByChannel(ctx); String userIdByChannel = mangerChannelService.getInfoByChannel(ctx);
log.info("连接WS成功handlerAdded,uid:" + userIdByChannel + "," + ",channelId:" + ctx.channel().id().asShortText()); log.info("连接WS成功handlerAdded,uid:" + userIdByChannel + "," + ",channelId:" + ctx.channel().id().asLongText());
} }
...@@ -109,7 +150,7 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram ...@@ -109,7 +150,7 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// @Override // @Override
// public void channelInactive(ChannelHandlerContext ctx) { // public void channelInactive(ChannelHandlerContext ctx) {
// String userIdByChannel = mangerChannelService.getInfoByChannel(ctx); // String userIdByChannel = mangerChannelService.getInfoByChannel(ctx);
// log.info("uid:" + userIdByChannel + "," + "channelInactive" + ",channelId:" + ctx.channel().id().asShortText()); // log.info("uid:" + userIdByChannel + "," + "channelInactive" + ",channelId:" + ctx.channel().id().asLongText());
// } // }
/** /**
...@@ -117,10 +158,16 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram ...@@ -117,10 +158,16 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
*/ */
@Override @Override
public void handlerRemoved(ChannelHandlerContext ctx) { public void handlerRemoved(ChannelHandlerContext ctx) {
String appKey = ctx.channel().attr(MangerChannelService.APP_KEY).get();
String clientId = ctx.channel().attr(MangerChannelService.CLIENT_ID).get();
String userIdByChannel = mangerChannelService.getInfoByChannel(ctx); String userIdByChannel = mangerChannelService.getInfoByChannel(ctx);
log.info("uid:" + userIdByChannel + "," + "handlerRemoved" + ",channelId:" + ctx.channel().id().asShortText()); log.info("uid:" + userIdByChannel + "," + "handlerRemoved" + ",channelId:" + ctx.channel().id().asLongText());
// 关掉连接 // 关掉连接
ctx.close(); ctx.close();
// rtc清空缓存
rtcService.clientOffline(appKey, clientId);
} }
} }
...@@ -128,6 +128,7 @@ public class NettyApiRequest { ...@@ -128,6 +128,7 @@ public class NettyApiRequest {
// 设置属性值 userid - channel // 设置属性值 userid - channel
ctx.channel().attr(MangerChannelService.CLIENT_ID).set(clientId); ctx.channel().attr(MangerChannelService.CLIENT_ID).set(clientId);
ctx.channel().attr(MangerChannelService.APP_KEY).set(appKey); ctx.channel().attr(MangerChannelService.APP_KEY).set(appKey);
ctx.channel().attr(MangerChannelService.READ_IDLE_TIMES).set(0);// 读空闲的计数=0
// 添加长连接handler // 添加长连接handler
ctx.pipeline().addLast("appImHandler", appImReadHandler); ctx.pipeline().addLast("appImHandler", appImReadHandler);
......
package com.wecloud.im.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <pre>
* 会话成员表 分页参数对象
* </pre>
*
* @author wei
* @date 2021-05-07
*/
@Data
@Accessors(chain = true)
@ApiModel(value = "ApiImConversationMembersPageParam")
public class ApiImConversationMembersPageParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "会话表id", required = true)
private Long conversationId;
}
package com.wecloud.im.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <pre>
* 会话成员表 查询结果对象
* </pre>
*
* @author wei
* @date 2021-05-07
*/
@Data
@Accessors(chain = true)
@ApiModel(value = "ApiImConversationMembersQueryVo")
public class ApiImConversationMembersQueryVo implements Serializable {
private static final long serialVersionUID = 1L;
// @ApiModelProperty("唯一id")
// private Long id;
// @ApiModelProperty("加入时间")
// private Date createTime;
@ApiModelProperty("客户端id")
private String clientId;
// @ApiModelProperty("修改时间")
// private Date updateTime;
//
// @ApiModelProperty("应用appid")
// private Long fkAppid;
// @ApiModelProperty("会话表id")
// private Long fkConversationId;
}
\ No newline at end of file
package com.wecloud.im.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* sign
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "GetClientInfoParam")
public class GetClientInfoParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("client客户端id")
private List<String> clientIds;
@ApiModelProperty("会话id")
private Long conversationId;
}
...@@ -4,6 +4,8 @@ import io.swagger.annotations.ApiModel; ...@@ -4,6 +4,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
/** /**
* sign * sign
* *
...@@ -12,7 +14,7 @@ import lombok.Data; ...@@ -12,7 +14,7 @@ import lombok.Data;
*/ */
@Data @Data
@ApiModel(value = "GetSignParam") @ApiModel(value = "GetSignParam")
public class GetSignParam { public class GetSignParam implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ApiModelProperty("时间戳") @ApiModelProperty("时间戳")
......
package com.wecloud.im.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <pre>
* 会话成员表 分页参数对象
* </pre>
*
* @author wei
* @date 2021-05-07
*/
@Data
@Accessors(chain = true)
@ApiModel(value = "ImConvMemeClientRemarkNameParam")
public class ImConvMemeClientRemarkNameParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "会话中的client备注,展示给会话中其他client查看的", required = true)
private String clientRemarkName;
@ApiModelProperty("会话表id")
private Long conversationId;
}
package com.wecloud.im.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <pre>
* 会话成员表 分页参数对象
* </pre>
*
* @author wei
* @date 2021-05-07
*/
@Data
@Accessors(chain = true)
@ApiModel(value = "ImConversationMembersListParam")
public class ImConversationMembersListParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "会话表id", required = true)
private Long conversationId;
}
...@@ -15,13 +15,13 @@ import lombok.Data; ...@@ -15,13 +15,13 @@ import lombok.Data;
public class ImTokenVerify { public class ImTokenVerify {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ApiModelProperty("时间戳") @ApiModelProperty("时间戳,需与生成sign时的值一致")
private String timestamp; private String timestamp;
@ApiModelProperty("client客户端id") @ApiModelProperty("client客户端id,需与生成sign时的值一致")
private String clientId; private String clientId;
@ApiModelProperty("appkey") @ApiModelProperty("appkey,需与生成sign时的值一致")
private String appKey; private String appKey;
@ApiModelProperty("签名sign") @ApiModelProperty("签名sign")
......
...@@ -23,7 +23,7 @@ public class ImClientDeviceInfoAdd extends BaseEntity { ...@@ -23,7 +23,7 @@ public class ImClientDeviceInfoAdd extends BaseEntity {
@ApiModelProperty("设备不想收到推送提醒, 1想, 0不想") @ApiModelProperty("设备不想收到推送提醒, 1想, 0不想")
private Integer valid; private Integer valid;
@ApiModelProperty("设备类型1:ios; 2:android") @ApiModelProperty("设备类型1:ios; 2:android; 3:web")
private Integer deviceType; private Integer deviceType;
@ApiModelProperty("设备推送token") @ApiModelProperty("设备推送token")
......
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 添加或修改推送设备信息
*
* @author wei
* @since 2021-04-27
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImClientHeadPortraitAdd")
public class ImClientHeadPortraitAdd extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("头像,uri地址")
private String headPortrait;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 添加或修改推送设备信息
*
* @author wei
* @since 2021-04-27
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImClientHeadPortraitAndNicknameUpdate")
public class ImClientHeadPortraitAndNicknameUpdate extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("头像,uri地址")
private String headPortrait;
@ApiModelProperty("主昵称")
private String nickname;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @author wei
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImClientLeaveConversation")
public class ImClientLeaveConversation extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("会话表id")
private Long conversationId;
@ApiModelProperty("会话的创建者退出时,是否需要转移给下一个client, true为转移, false为不转移直接解散")
private Boolean transfer;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 添加或修改推送设备信息
*
* @author wei
* @since 2021-04-27
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImClientNicknameAdd")
public class ImClientNicknameAdd extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主昵称")
private String nickname;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.List;
/**
* @author wei
* @since 2021年11月30日16:57:03
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImClientToConversation")
public class ImClientToConversation extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("会话表id")
private Long conversationId;
@ApiModelProperty("要操作的clientId")
private List<String> clientIds;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.HashMap;
/**
* 会话表
*
* @author wei
* @since 2021-05-07
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImConversationAttrUpdate")
public class ImConversationAttrUpdate extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("json格式,自定义属性,供开发者扩展使用。")
private HashMap attributes;
@ApiModelProperty("会话表id")
private Long conversationId;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.HashMap;
/**
* 会话表
*
* @author wei
* @since 2021-05-07
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImConversationMemAttrUpdate")
public class ImConversationMemAttrUpdate extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("json格式,自定义属性,供开发者扩展使用。")
private HashMap attributes;
@ApiModelProperty("会话表id")
private Long conversationId;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 会话表
*
* @author wei
* @since 2021-05-07
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImConversationNameUpdate")
public class ImConversationNameUpdate extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("对话的名字,可为群组命名。")
private String name;
@ApiModelProperty("会话表id")
private Long conversationId;
}
package com.wecloud.im.param.add;
import io.geekidea.springbootplus.framework.common.entity.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.HashMap;
/**
* 自定义透传内容
*
* @author wei
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ImMsgSendToOnlineClient")
public class ImMsgSendToOnlineClient extends BaseEntity {
private static final long serialVersionUID = 1L;
// @ApiModelProperty(value = "自定义透传内容", required = true)
// private String content;
@ApiModelProperty(value = "自定义透传内容 ,为任意参数名称和类型的对象,供开发者扩展使用。", required = true)
private HashMap content;
@ApiModelProperty(value = "会话id", required = true)
private Long conversationId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "CandidateForwardParam")
public class CandidateForwardParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
/**
* 转发的候选者数据
*/
private String candidateData;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 创建频道请求参数
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "CreateRtcChannelParam")
public class CreateRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "被邀请的客户端ID", required = true)
private String toClient;
@ApiModelProperty(value = "客户端自定义数据", required = false)
private String attrs;
@ApiModelProperty(value = "类型: video或voice", required = true)
private String type;
@ApiModelProperty(value = "绑定的会话id,可选", required = false)
private Long conversationId;
@ApiModelProperty(value = "接收方展示的系统推送内容,可", required = false)
private String push;
@ApiModelProperty(value = "是否需要给对方发系统通知", required = true)
private Boolean pushCall;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 创建频道返回参数
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "CreateRtcChannelResult")
public class CreateRtcChannelResult implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 加入频道请求参数
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "JoinRtcChannelParam")
public class JoinRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 退出频道 请求
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "LeaveRtcChannelParam")
public class LeaveRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 拒接加入频道 请求
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "RejectRtcChannelParam")
public class RejectRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "SdpForwardParam")
public class SdpForwardParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
@ApiModelProperty("sdp转发的数据")
private String sdpData;
@ApiModelProperty("sdp类型: Offer或Answer")
private String sdpType;
}
...@@ -34,7 +34,7 @@ public class PushUtils { ...@@ -34,7 +34,7 @@ public class PushUtils {
} }
public static void main(String[] args) { public static void main(String[] args) {
// TODO set your appkey and master secret here // set your appkey and master secret here
PushUtils demo = new PushUtils("your appkey", "your master secret"); PushUtils demo = new PushUtils("your appkey", "your master secret");
try { try {
...@@ -52,7 +52,7 @@ public class PushUtils { ...@@ -52,7 +52,7 @@ public class PushUtils {
String body = "今日可能下雨🌂"; String body = "今日可能下雨🌂";
demo.sendIOSUnicast(deviceTokenIOS, titleIOS, subtitle, body); demo.sendIOSUnicast(deviceTokenIOS, titleIOS, subtitle, body);
/* TODO these methods are all available, just fill in some fields and do the test /* these methods are all available, just fill in some fields and do the test
* demo.sendAndroidCustomizedcastFile(); * demo.sendAndroidCustomizedcastFile();
* demo.sendAndroidBroadcast(); * demo.sendAndroidBroadcast();
* demo.sendAndroidGroupcast(); * demo.sendAndroidGroupcast();
...@@ -76,7 +76,7 @@ public class PushUtils { ...@@ -76,7 +76,7 @@ public class PushUtils {
broadcast.setText("Android broadcast text"); broadcast.setText("Android broadcast text");
broadcast.goAppAfterOpen(); broadcast.goAppAfterOpen();
broadcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION); broadcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION);
// TODO Set 'production_mode' to 'false' if it's a test device. // Set 'production_mode' to 'false' if it's a test device.
// For how to register a test device, please see the developer doc. // For how to register a test device, please see the developer doc.
broadcast.setProductionMode(); broadcast.setProductionMode();
// Set customized fields // Set customized fields
...@@ -98,7 +98,7 @@ public class PushUtils { ...@@ -98,7 +98,7 @@ public class PushUtils {
*/ */
public void sendAndroidUnicast(String deviceToken, String unicastText, String unicastTicker, String title) throws Exception { public void sendAndroidUnicast(String deviceToken, String unicastText, String unicastTicker, String title) throws Exception {
AndroidUnicast unicast = new AndroidUnicast(appkey, appMasterSecret); AndroidUnicast unicast = new AndroidUnicast(appkey, appMasterSecret);
// TODO Set your device token // Set your device token
unicast.setDeviceToken(deviceToken); unicast.setDeviceToken(deviceToken);
...@@ -108,7 +108,7 @@ public class PushUtils { ...@@ -108,7 +108,7 @@ public class PushUtils {
unicast.setText(unicastText); unicast.setText(unicastText);
unicast.goAppAfterOpen(); unicast.goAppAfterOpen();
unicast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION); unicast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION);
// TODO Set 'production_mode' to 'false' if it's a test device. // Set 'production_mode' to 'false' if it's a test device.
// For how to register a test device, please see the developer doc. // For how to register a test device, please see the developer doc.
unicast.setProductionMode(); unicast.setProductionMode();
// Set customized fields // Set customized fields
...@@ -120,7 +120,7 @@ public class PushUtils { ...@@ -120,7 +120,7 @@ public class PushUtils {
public void sendAndroidGroupcast() throws Exception { public void sendAndroidGroupcast() throws Exception {
AndroidGroupcast groupcast = new AndroidGroupcast(appkey, appMasterSecret); AndroidGroupcast groupcast = new AndroidGroupcast(appkey, appMasterSecret);
/* TODO /*
* Construct the filter condition: * Construct the filter condition:
* "where": * "where":
* { * {
...@@ -150,7 +150,7 @@ public class PushUtils { ...@@ -150,7 +150,7 @@ public class PushUtils {
groupcast.goAppAfterOpen(); groupcast.goAppAfterOpen();
groupcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION); groupcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION);
groupcast.setChannelActivity("your channel activity"); groupcast.setChannelActivity("your channel activity");
// TODO Set 'production_mode' to 'false' if it's a test device. // Set 'production_mode' to 'false' if it's a test device.
// For how to register a test device, please see the developer doc. // For how to register a test device, please see the developer doc.
groupcast.setProductionMode(); groupcast.setProductionMode();
//厂商通道相关参数 //厂商通道相关参数
...@@ -161,7 +161,7 @@ public class PushUtils { ...@@ -161,7 +161,7 @@ public class PushUtils {
public void sendAndroidCustomizedcast() throws Exception { public void sendAndroidCustomizedcast() throws Exception {
AndroidCustomizedcast customizedcast = new AndroidCustomizedcast(appkey, appMasterSecret); AndroidCustomizedcast customizedcast = new AndroidCustomizedcast(appkey, appMasterSecret);
// TODO Set your alias here, and use comma to split them if there are multiple alias. // Set your alias here, and use comma to split them if there are multiple alias.
// And if you have many alias, you can also upload a file containing these alias, then // And if you have many alias, you can also upload a file containing these alias, then
// use file_id to send customized notification. // use file_id to send customized notification.
customizedcast.setAlias("alias", "alias_type"); customizedcast.setAlias("alias", "alias_type");
...@@ -170,7 +170,7 @@ public class PushUtils { ...@@ -170,7 +170,7 @@ public class PushUtils {
customizedcast.setText("Android customizedcast text"); customizedcast.setText("Android customizedcast text");
customizedcast.goAppAfterOpen(); customizedcast.goAppAfterOpen();
customizedcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION); customizedcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION);
// TODO Set 'production_mode' to 'false' if it's a test device. // Set 'production_mode' to 'false' if it's a test device.
// For how to register a test device, please see the developer doc. // For how to register a test device, please see the developer doc.
customizedcast.setProductionMode(); customizedcast.setProductionMode();
//厂商通道相关参数 //厂商通道相关参数
...@@ -181,7 +181,7 @@ public class PushUtils { ...@@ -181,7 +181,7 @@ public class PushUtils {
public void sendAndroidCustomizedcastFile() throws Exception { public void sendAndroidCustomizedcastFile() throws Exception {
AndroidCustomizedcast customizedcast = new AndroidCustomizedcast(appkey, appMasterSecret); AndroidCustomizedcast customizedcast = new AndroidCustomizedcast(appkey, appMasterSecret);
// TODO Set your alias here, and use comma to split them if there are multiple alias. // Set your alias here, and use comma to split them if there are multiple alias.
// And if you have many alias, you can also upload a file containing these alias, then // And if you have many alias, you can also upload a file containing these alias, then
// use file_id to send customized notification. // use file_id to send customized notification.
String fileId = client.uploadContents(appkey, appMasterSecret, "aa" + "\n" + "bb" + "\n" + "alias"); String fileId = client.uploadContents(appkey, appMasterSecret, "aa" + "\n" + "bb" + "\n" + "alias");
...@@ -191,7 +191,7 @@ public class PushUtils { ...@@ -191,7 +191,7 @@ public class PushUtils {
customizedcast.setText("Android customizedcast text"); customizedcast.setText("Android customizedcast text");
customizedcast.goAppAfterOpen(); customizedcast.goAppAfterOpen();
customizedcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION); customizedcast.setDisplayType(AndroidNotification.DisplayType.NOTIFICATION);
// TODO Set 'production_mode' to 'false' if it's a test device. // Set 'production_mode' to 'false' if it's a test device.
// For how to register a test device, please see the developer doc. // For how to register a test device, please see the developer doc.
customizedcast.setProductionMode(); customizedcast.setProductionMode();
//厂商通道相关参数 //厂商通道相关参数
...@@ -202,7 +202,7 @@ public class PushUtils { ...@@ -202,7 +202,7 @@ public class PushUtils {
public void sendAndroidFilecast() throws Exception { public void sendAndroidFilecast() throws Exception {
AndroidFilecast filecast = new AndroidFilecast(appkey, appMasterSecret); AndroidFilecast filecast = new AndroidFilecast(appkey, appMasterSecret);
// TODO upload your device tokens, and use '\n' to split them if there are multiple tokens // upload your device tokens, and use '\n' to split them if there are multiple tokens
String fileId = client.uploadContents(appkey, appMasterSecret, "aa" + "\n" + "bb"); String fileId = client.uploadContents(appkey, appMasterSecret, "aa" + "\n" + "bb");
filecast.setFileId(fileId); filecast.setFileId(fileId);
filecast.setTicker("Android filecast ticker"); filecast.setTicker("Android filecast ticker");
...@@ -224,7 +224,7 @@ public class PushUtils { ...@@ -224,7 +224,7 @@ public class PushUtils {
broadcast.setAlert("今日天气", "", "今日可能下雨🌂"); broadcast.setAlert("今日天气", "", "今日可能下雨🌂");
broadcast.setBadge(0); broadcast.setBadge(0);
broadcast.setSound("default"); broadcast.setSound("default");
// TODO set 'production_mode' to 'true' if your app is under production mode // set 'production_mode' to 'true' if your app is under production mode
broadcast.setTestMode(); broadcast.setTestMode();
// Set customized fields // Set customized fields
broadcast.setCustomizedField("test", "helloworld"); broadcast.setCustomizedField("test", "helloworld");
...@@ -243,7 +243,7 @@ public class PushUtils { ...@@ -243,7 +243,7 @@ public class PushUtils {
*/ */
public void sendIOSUnicast(String deviceToken, String title, String subtitle, String body) throws Exception { public void sendIOSUnicast(String deviceToken, String title, String subtitle, String body) throws Exception {
IOSUnicast unicast = new IOSUnicast(appkey, appMasterSecret); IOSUnicast unicast = new IOSUnicast(appkey, appMasterSecret);
// TODO Set your device token // Set your device token
unicast.setDeviceToken(deviceToken); unicast.setDeviceToken(deviceToken);
//alert值设置为字符串 //alert值设置为字符串
...@@ -253,7 +253,7 @@ public class PushUtils { ...@@ -253,7 +253,7 @@ public class PushUtils {
unicast.setAlert(title, subtitle, body); unicast.setAlert(title, subtitle, body);
unicast.setBadge(0); unicast.setBadge(0);
unicast.setSound("default"); unicast.setSound("default");
// TODO set 'production_mode' to 'true' if your app is under production mode // set 'production_mode' to 'true' if your app is under production mode
unicast.setTestMode(); unicast.setTestMode();
// Set customized fields // Set customized fields
unicast.setCustomizedField("test", "helloworld"); unicast.setCustomizedField("test", "helloworld");
...@@ -262,7 +262,7 @@ public class PushUtils { ...@@ -262,7 +262,7 @@ public class PushUtils {
public void sendIOSGroupcast() throws Exception { public void sendIOSGroupcast() throws Exception {
IOSGroupcast groupcast = new IOSGroupcast(appkey, appMasterSecret); IOSGroupcast groupcast = new IOSGroupcast(appkey, appMasterSecret);
/* TODO /*
* Construct the filter condition: * Construct the filter condition:
* "where": * "where":
* { * {
...@@ -289,14 +289,14 @@ public class PushUtils { ...@@ -289,14 +289,14 @@ public class PushUtils {
groupcast.setAlert("今日天气", "subtitle", "今日可能下雨🌂"); groupcast.setAlert("今日天气", "subtitle", "今日可能下雨🌂");
groupcast.setBadge(0); groupcast.setBadge(0);
groupcast.setSound("default"); groupcast.setSound("default");
// TODO set 'production_mode' to 'true' if your app is under production mode // set 'production_mode' to 'true' if your app is under production mode
groupcast.setTestMode(); groupcast.setTestMode();
client.send(groupcast); client.send(groupcast);
} }
public void sendIOSCustomizedcast() throws Exception { public void sendIOSCustomizedcast() throws Exception {
IOSCustomizedcast customizedcast = new IOSCustomizedcast(appkey, appMasterSecret); IOSCustomizedcast customizedcast = new IOSCustomizedcast(appkey, appMasterSecret);
// TODO Set your alias and alias_type here, and use comma to split them if there are multiple alias. // Set your alias and alias_type here, and use comma to split them if there are multiple alias.
// And if you have many alias, you can also upload a file containing these alias, then // And if you have many alias, you can also upload a file containing these alias, then
// use file_id to send customized notification. // use file_id to send customized notification.
customizedcast.setAlias("alias", "alias_type"); customizedcast.setAlias("alias", "alias_type");
...@@ -305,14 +305,14 @@ public class PushUtils { ...@@ -305,14 +305,14 @@ public class PushUtils {
customizedcast.setAlert("今日天气", "", "今日可能下雨🌂"); customizedcast.setAlert("今日天气", "", "今日可能下雨🌂");
customizedcast.setBadge(0); customizedcast.setBadge(0);
customizedcast.setSound("default"); customizedcast.setSound("default");
// TODO set 'production_mode' to 'true' if your app is under production mode // set 'production_mode' to 'true' if your app is under production mode
customizedcast.setTestMode(); customizedcast.setTestMode();
client.send(customizedcast); client.send(customizedcast);
} }
public void sendIOSFilecast() throws Exception { public void sendIOSFilecast() throws Exception {
IOSFilecast filecast = new IOSFilecast(appkey, appMasterSecret); IOSFilecast filecast = new IOSFilecast(appkey, appMasterSecret);
// TODO upload your device tokens, and use '\n' to split them if there are multiple tokens // upload your device tokens, and use '\n' to split them if there are multiple tokens
String fileId = client.uploadContents(appkey, appMasterSecret, "aa" + "\n" + "bb"); String fileId = client.uploadContents(appkey, appMasterSecret, "aa" + "\n" + "bb");
filecast.setFileId(fileId); filecast.setFileId(fileId);
//filecast.setAlert("IOS 文件播测试"); //filecast.setAlert("IOS 文件播测试");
...@@ -320,7 +320,7 @@ public class PushUtils { ...@@ -320,7 +320,7 @@ public class PushUtils {
filecast.setAlert("今日天气", "", "今日可能下雨🌂"); filecast.setAlert("今日天气", "", "今日可能下雨🌂");
filecast.setBadge(0); filecast.setBadge(0);
filecast.setSound("default"); filecast.setSound("default");
// TODO set 'production_mode' to 'true' if your app is under production mode // set 'production_mode' to 'true' if your app is under production mode
filecast.setTestMode(); filecast.setTestMode();
client.send(filecast); client.send(filecast);
} }
......
package com.wecloud.im.service; package com.wecloud.im.service;
import com.wecloud.im.entity.ImClient; import com.wecloud.im.entity.ImClient;
import com.wecloud.im.param.GetClientInfoParam;
import com.wecloud.im.param.ImClientPageParam; import com.wecloud.im.param.ImClientPageParam;
import com.wecloud.im.param.ImClientQueryVo; import com.wecloud.im.param.ImClientQueryVo;
import com.wecloud.im.param.add.ImClientDeviceInfoAdd; import com.wecloud.im.param.add.ImClientDeviceInfoAdd;
import com.wecloud.im.param.add.ImClientHeadPortraitAdd;
import com.wecloud.im.param.add.ImClientHeadPortraitAndNicknameUpdate;
import com.wecloud.im.param.add.ImClientNicknameAdd;
import com.wecloud.im.vo.GetInfoListVo;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.service.BaseService; import io.geekidea.springbootplus.framework.common.service.BaseService;
import io.geekidea.springbootplus.framework.core.pagination.Paging; import io.geekidea.springbootplus.framework.core.pagination.Paging;
import java.util.List;
/** /**
* 终端表 服务类 * 终端表 服务类
* *
...@@ -15,6 +23,23 @@ import io.geekidea.springbootplus.framework.core.pagination.Paging; ...@@ -15,6 +23,23 @@ import io.geekidea.springbootplus.framework.core.pagination.Paging;
*/ */
public interface ImClientService extends BaseService<ImClient> { public interface ImClientService extends BaseService<ImClient> {
boolean updateHeadPortrait(ImClientHeadPortraitAdd imClientHeadPortraitAdd) throws Exception;
boolean updateHeadAndNickname(ImClientHeadPortraitAndNicknameUpdate imClientHeadPortraitAndNicknameUpdate);
/**
* 根据ids获取Client的头像昵称
*
* @param getClientInfoParam
* @return
* @throws Exception
*/
ApiResult<List<GetInfoListVo>> getInfoList(GetClientInfoParam getClientInfoParam) throws Exception;
boolean updateNickname(ImClientNicknameAdd imClientNicknameAdd) throws Exception;
/** /**
* 保存 * 保存
* *
......
package com.wecloud.im.service; package com.wecloud.im.service;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImConversationMembers; import com.wecloud.im.entity.ImConversationMembers;
import com.wecloud.im.param.ApiImConversationMembersPageParam;
import com.wecloud.im.param.ApiImConversationMembersQueryVo;
import com.wecloud.im.param.ImConvMemeClientRemarkNameParam;
import com.wecloud.im.param.ImConversationMembersListParam;
import com.wecloud.im.param.ImConversationMembersPageParam; import com.wecloud.im.param.ImConversationMembersPageParam;
import com.wecloud.im.param.ImConversationMembersQueryVo; import com.wecloud.im.param.ImConversationMembersQueryVo;
import com.wecloud.im.param.add.ImConversationMemAttrUpdate;
import com.wecloud.im.vo.ImConversationMemberListVo;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.service.BaseService; import io.geekidea.springbootplus.framework.common.service.BaseService;
import io.geekidea.springbootplus.framework.core.pagination.Paging; import io.geekidea.springbootplus.framework.core.pagination.Paging;
import java.util.List;
/** /**
* 会话成员表 服务类 * 会话成员表 服务类
* *
...@@ -14,6 +24,32 @@ import io.geekidea.springbootplus.framework.core.pagination.Paging; ...@@ -14,6 +24,32 @@ import io.geekidea.springbootplus.framework.core.pagination.Paging;
*/ */
public interface ImConversationMembersService extends BaseService<ImConversationMembers> { public interface ImConversationMembersService extends BaseService<ImConversationMembers> {
/**
* 服务端api-会话成员表分页列表
*
* @param apiImConversationMembersPageParam
* @param imApplication
* @return
*/
ApiResult<List<ApiImConversationMembersQueryVo>> getRestApiImConversationMembersList(ApiImConversationMembersPageParam apiImConversationMembersPageParam, ImApplication imApplication);
ApiResult<Boolean> saveOrUpdateClientRemarkName(ImConvMemeClientRemarkNameParam imConvMemeClientRemarkNameParam);
/**
* 会话成员表分页列表
*
* @param imConversationMembersListParam
* @return
* @throws Exception
*/
List<ImConversationMemberListVo> getImConversationMembersList(ImConversationMembersListParam imConversationMembersListParam) throws Exception;
ApiResult<Boolean> saveOrUpdateAttr(ImConversationMemAttrUpdate imConversationMemAttrUpdate);
/** /**
* 保存 * 保存
* *
......
...@@ -4,8 +4,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; ...@@ -4,8 +4,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.im.entity.ImConversation; import com.wecloud.im.entity.ImConversation;
import com.wecloud.im.param.ImConversationPageParam; import com.wecloud.im.param.ImConversationPageParam;
import com.wecloud.im.param.ImConversationQueryVo; import com.wecloud.im.param.ImConversationQueryVo;
import com.wecloud.im.param.add.ImClientLeaveConversation;
import com.wecloud.im.param.add.ImClientToConversation;
import com.wecloud.im.param.add.ImConversationAttrUpdate;
import com.wecloud.im.param.add.ImConversationCreate; import com.wecloud.im.param.add.ImConversationCreate;
import com.wecloud.im.param.add.ImConversationDisplayUpdate; import com.wecloud.im.param.add.ImConversationDisplayUpdate;
import com.wecloud.im.param.add.ImConversationNameUpdate;
import com.wecloud.im.vo.ImConversationCreateVo; import com.wecloud.im.vo.ImConversationCreateVo;
import com.wecloud.im.vo.MyConversationListVo; import com.wecloud.im.vo.MyConversationListVo;
import io.geekidea.springbootplus.framework.common.api.ApiResult; import io.geekidea.springbootplus.framework.common.api.ApiResult;
...@@ -40,6 +44,39 @@ public interface ImConversationService extends BaseService<ImConversation> { ...@@ -40,6 +44,39 @@ public interface ImConversationService extends BaseService<ImConversation> {
*/ */
ApiResult<ImConversationCreateVo> createImConversation(ImConversationCreate imConversationCreate) throws JsonProcessingException; ApiResult<ImConversationCreateVo> createImConversation(ImConversationCreate imConversationCreate) throws JsonProcessingException;
/**
* 将用户添加进会话
*
* @param imClientToConversation
* @return
* @throws JsonProcessingException
*/
ApiResult<Boolean> addClientToConversation(ImClientToConversation imClientToConversation);
/**
* 将client从会话移除
*
* @param imClientToConversation
* @return
*/
ApiResult<Boolean> delClientToConversation(ImClientToConversation imClientToConversation) throws Exception;
/**
* client退出会话
*/
ApiResult<Boolean> leaveConversation(ImClientLeaveConversation imClientToConversation) throws Exception;
/**
* 添加或修改会话名称
*/
ApiResult<Boolean> saveOrUpdateName(ImConversationNameUpdate imConversationNameUpdate) throws Exception;
/**
* 添加或修改会话拓展字段
*/
ApiResult<Boolean> saveOrUpdateAttr(ImConversationAttrUpdate imConversationAttrUpdate) throws Exception;
/** /**
* 修改 * 修改
* *
......
package com.wecloud.im.service; package com.wecloud.im.service;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient;
import com.wecloud.im.entity.ImMessage; import com.wecloud.im.entity.ImMessage;
import com.wecloud.im.param.ImHistoryMessagePageParam; import com.wecloud.im.param.ImHistoryMessagePageParam;
import com.wecloud.im.param.add.ImMsgRecall; import com.wecloud.im.param.add.ImMsgRecall;
import com.wecloud.im.param.add.ImMsgSendToOnlineClient;
import com.wecloud.im.param.add.ImMsgUpdate; import com.wecloud.im.param.add.ImMsgUpdate;
import com.wecloud.im.vo.ImMessageOfflineListVo; import com.wecloud.im.vo.ImMessageOfflineListVo;
import com.wecloud.im.vo.OfflineMsgDto; import com.wecloud.im.vo.OfflineMsgDto;
...@@ -22,6 +25,18 @@ public interface ImMessageService extends BaseService<ImMessage> { ...@@ -22,6 +25,18 @@ public interface ImMessageService extends BaseService<ImMessage> {
/** /**
* 下发透传消息
*
* @param imMsgSendToOnlineClient
* @return
*/
ApiResult<Boolean> restApiImMessageSend(ImMsgSendToOnlineClient imMsgSendToOnlineClient, ImApplication imApplication);
ImMessage saveImMessage(ImApplication imApplication, ImClient imClientSender, Long toConversationId, long messageId, String content);
/**
* 消息撤回 只能撤回客户端自己发送的消息 * 消息撤回 只能撤回客户端自己发送的消息
* *
* @return * @return
......
...@@ -8,11 +8,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; ...@@ -8,11 +8,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wecloud.im.entity.ImApplication; import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient; import com.wecloud.im.entity.ImClient;
import com.wecloud.im.mapper.ImClientMapper; import com.wecloud.im.mapper.ImClientMapper;
import com.wecloud.im.param.GetClientInfoParam;
import com.wecloud.im.param.ImClientPageParam; import com.wecloud.im.param.ImClientPageParam;
import com.wecloud.im.param.ImClientQueryVo; import com.wecloud.im.param.ImClientQueryVo;
import com.wecloud.im.param.add.ImClientDeviceInfoAdd; import com.wecloud.im.param.add.ImClientDeviceInfoAdd;
import com.wecloud.im.param.add.ImClientHeadPortraitAdd;
import com.wecloud.im.param.add.ImClientHeadPortraitAndNicknameUpdate;
import com.wecloud.im.param.add.ImClientNicknameAdd;
import com.wecloud.im.service.ImApplicationService; import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService; import com.wecloud.im.service.ImClientService;
import com.wecloud.im.vo.GetInfoListVo;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.service.impl.BaseServiceImpl; import io.geekidea.springbootplus.framework.common.service.impl.BaseServiceImpl;
import io.geekidea.springbootplus.framework.core.pagination.PageInfo; import io.geekidea.springbootplus.framework.core.pagination.PageInfo;
import io.geekidea.springbootplus.framework.core.pagination.Paging; import io.geekidea.springbootplus.framework.core.pagination.Paging;
...@@ -27,6 +33,8 @@ import org.springframework.cache.annotation.Cacheable; ...@@ -27,6 +33,8 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/** /**
* 终端表 服务实现类 * 终端表 服务实现类
* *
...@@ -43,6 +51,92 @@ public class ImClientServiceImpl extends BaseServiceImpl<ImClientMapper, ImClien ...@@ -43,6 +51,92 @@ public class ImClientServiceImpl extends BaseServiceImpl<ImClientMapper, ImClien
@Autowired @Autowired
private ImApplicationService imApplicationService; private ImApplicationService imApplicationService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateHeadPortrait(ImClientHeadPortraitAdd imClientHeadPortraitAdd) throws Exception {
ImClient curentClient = getCurentClient();
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询appid
// ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
curentClient.setHeadPortrait(imClientHeadPortraitAdd.getHeadPortrait());
imClientMapper.updateById(curentClient);
// 清除client的redis缓存
deleteCacheImClient(curentClient.getFkAppid(), curentClient.getClientId());
return true;
}
@Override
public boolean updateHeadAndNickname(ImClientHeadPortraitAndNicknameUpdate imClientHeadPortraitAndNicknameUpdate) {
ImClient curentClient = getCurentClient();
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询appid
// ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
curentClient.setHeadPortrait(imClientHeadPortraitAndNicknameUpdate.getHeadPortrait());
curentClient.setNickname(imClientHeadPortraitAndNicknameUpdate.getNickname());
imClientMapper.updateById(curentClient);
// 清除client的redis缓存
deleteCacheImClient(curentClient.getFkAppid(), curentClient.getClientId());
return true;
}
@Override
public ApiResult<List<GetInfoListVo>> getInfoList(GetClientInfoParam getClientInfoParam) throws Exception {
ImClient curentClient = getCurentClient();
// List<ImClient> imClients = this.list(new QueryWrapper<ImClient>().lambda()
// .eq(ImClient::getFkAppid, curentClient.getFkAppid())
// .in(ImClient::getClientId, getClientInfoParam.getClientId())
// );
//
// List<GetInfoListVo> getInfoListVos = new ArrayList<>();
//
// for (ImClient imClient : imClients) {
//
// GetInfoListVo getInfoListVo = new GetInfoListVo();
// getInfoListVo.setHeadPortrait(imClient.getHeadPortrait());
// getInfoListVo.setNickname(imClient.getNickname());
// getInfoListVo.setClientId(imClient.getClientId());
// getInfoListVos.add(getInfoListVo);
// }
List<GetInfoListVo> infoList = imClientMapper.getInfoList(curentClient.getFkAppid(), getClientInfoParam.getConversationId(), getClientInfoParam.getClientIds());
return ApiResult.ok(infoList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateNickname(ImClientNicknameAdd imClientNicknameAdd) throws Exception {
ImClient curentClient = getCurentClient();
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询appid
// ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
curentClient.setNickname(imClientNicknameAdd.getNickname());
imClientMapper.updateById(curentClient);
// 清除client的redis缓存
deleteCacheImClient(curentClient.getFkAppid(), curentClient.getClientId());
return true;
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public boolean saveImClient(ImClient imClient) throws Exception { public boolean saveImClient(ImClient imClient) throws Exception {
......
package com.wecloud.im.service.impl; package com.wecloud.im.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient;
import com.wecloud.im.entity.ImConversationMembers; import com.wecloud.im.entity.ImConversationMembers;
import com.wecloud.im.mapper.ImConversationMapper;
import com.wecloud.im.mapper.ImConversationMembersMapper; import com.wecloud.im.mapper.ImConversationMembersMapper;
import com.wecloud.im.param.ApiImConversationMembersPageParam;
import com.wecloud.im.param.ApiImConversationMembersQueryVo;
import com.wecloud.im.param.ImConvMemeClientRemarkNameParam;
import com.wecloud.im.param.ImConversationMembersListParam;
import com.wecloud.im.param.ImConversationMembersPageParam; import com.wecloud.im.param.ImConversationMembersPageParam;
import com.wecloud.im.param.ImConversationMembersQueryVo; import com.wecloud.im.param.ImConversationMembersQueryVo;
import com.wecloud.im.param.add.ImConversationMemAttrUpdate;
import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService;
import com.wecloud.im.service.ImConversationMembersService; import com.wecloud.im.service.ImConversationMembersService;
import com.wecloud.im.service.ImConversationService;
import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.vo.ImConversationMemberListVo;
import com.wecloud.im.ws.service.WriteDataService;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.service.impl.BaseServiceImpl; import io.geekidea.springbootplus.framework.common.service.impl.BaseServiceImpl;
import io.geekidea.springbootplus.framework.core.pagination.PageInfo; import io.geekidea.springbootplus.framework.core.pagination.PageInfo;
import io.geekidea.springbootplus.framework.core.pagination.Paging; import io.geekidea.springbootplus.framework.core.pagination.Paging;
import io.geekidea.springbootplus.framework.shiro.jwt.JwtToken;
import io.geekidea.springbootplus.framework.shiro.util.JwtUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/** /**
* 会话成员表 服务实现类 * 会话成员表 服务实现类
* *
...@@ -26,9 +48,154 @@ import org.springframework.transaction.annotation.Transactional; ...@@ -26,9 +48,154 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class ImConversationMembersServiceImpl extends BaseServiceImpl<ImConversationMembersMapper, ImConversationMembers> implements ImConversationMembersService { public class ImConversationMembersServiceImpl extends BaseServiceImpl<ImConversationMembersMapper, ImConversationMembers> implements ImConversationMembersService {
// private static final JsonMapper JSON_MAPPER = new JsonMapper();
@Autowired
private WriteDataService writeDataService;
@Autowired
private ImConversationMapper imConversationMapper;
@Autowired
private ImConversationMembersService imConversationMembersService;
@Autowired
private ImConversationService imConversationService;
@Autowired
private ImClientService imClientService;
@Autowired
private ImApplicationService imApplicationService;
@Autowired
private ImMessageService imMessageService;
@Autowired @Autowired
private ImConversationMembersMapper imConversationMembersMapper; private ImConversationMembersMapper imConversationMembersMapper;
@Override
public ApiResult<List<ApiImConversationMembersQueryVo>> getRestApiImConversationMembersList(ApiImConversationMembersPageParam apiImConversationMembersPageParam, ImApplication imApplication) {
List<ApiImConversationMembersQueryVo> restApiImConversationMembersList = imConversationMembersMapper.getRestApiImConversationMembersList(apiImConversationMembersPageParam.getConversationId());
return ApiResult.ok(restApiImConversationMembersList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> saveOrUpdateClientRemarkName(ImConvMemeClientRemarkNameParam imConvMemeClientRemarkNameParam) {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
// 查询该成员
ImConversationMembers imConversationMember = imConversationMembersService.getOne(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imConvMemeClientRemarkNameParam.getConversationId())
.eq(ImConversationMembers::getFkClientId, imClientSender.getId())
);
if (imConversationMember == null) {
return ApiResult.fail();
}
// // 查询该会话所有成员
// List<ImConversationMembers> membersList = imConversationMembersService.list(
// new QueryWrapper<ImConversationMembers>().lambda()
// .eq(ImConversationMembers::getFkAppid, imApplication.getId())
// .eq(ImConversationMembers::getFkConversationId, imConversationAttrUpdate.getConversationId())
// .notIn(ImConversationMembers::getId, imClientSender.getId())
// );
imConversationMember.setClientRemarkName(imConvMemeClientRemarkNameParam.getClientRemarkName());
boolean b = imConversationMembersService.updateById(imConversationMember);
if (b) {
//TODO ws下发群成员备注变动事件
return ApiResult.ok();
} else {
return ApiResult.fail();
}
}
@Override
public List<ImConversationMemberListVo> getImConversationMembersList(ImConversationMembersListParam imConversationMembersListParam) throws Exception {
return imConversationMembersMapper.getImConversationMembersList(imConversationMembersListParam.getConversationId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> saveOrUpdateAttr(ImConversationMemAttrUpdate imConversationMemAttrUpdate) {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
// 查询该成员
ImConversationMembers imConversationMember = imConversationMembersService.getOne(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imConversationMemAttrUpdate.getConversationId())
.eq(ImConversationMembers::getId, imClientSender.getId())
);
if (imConversationMember == null) {
return ApiResult.fail();
}
// // 查询该会话所有成员
// List<ImConversationMembers> membersList = imConversationMembersService.list(
// new QueryWrapper<ImConversationMembers>().lambda()
// .eq(ImConversationMembers::getFkAppid, imApplication.getId())
// .eq(ImConversationMembers::getFkConversationId, imConversationAttrUpdate.getConversationId())
// .notIn(ImConversationMembers::getId, imClientSender.getId())
// );
JsonMapper jsonMapper = new JsonMapper();
try {
String attributes = jsonMapper.writeValueAsString(imConversationMemAttrUpdate.getAttributes());
imConversationMember.setAttributes(attributes);
} catch (JsonProcessingException e) {
e.printStackTrace();
return ApiResult.fail();
}
boolean b = imConversationMembersService.updateById(imConversationMember);
if (b) {
//TODO ws下发群成员属性变动事件
return ApiResult.ok();
} else {
return ApiResult.fail();
}
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public boolean saveImConversationMembers(ImConversationMembers imConversationMembers) throws Exception { public boolean saveImConversationMembers(ImConversationMembers imConversationMembers) throws Exception {
......
...@@ -11,11 +11,17 @@ import com.wecloud.im.entity.ImApplication; ...@@ -11,11 +11,17 @@ import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient; import com.wecloud.im.entity.ImClient;
import com.wecloud.im.entity.ImConversation; import com.wecloud.im.entity.ImConversation;
import com.wecloud.im.entity.ImConversationMembers; import com.wecloud.im.entity.ImConversationMembers;
import com.wecloud.im.entity.ImMessage;
import com.wecloud.im.entity.ImMessageOnlineSend;
import com.wecloud.im.mapper.ImConversationMapper; import com.wecloud.im.mapper.ImConversationMapper;
import com.wecloud.im.param.ImConversationPageParam; import com.wecloud.im.param.ImConversationPageParam;
import com.wecloud.im.param.ImConversationQueryVo; import com.wecloud.im.param.ImConversationQueryVo;
import com.wecloud.im.param.add.ImClientLeaveConversation;
import com.wecloud.im.param.add.ImClientToConversation;
import com.wecloud.im.param.add.ImConversationAttrUpdate;
import com.wecloud.im.param.add.ImConversationCreate; import com.wecloud.im.param.add.ImConversationCreate;
import com.wecloud.im.param.add.ImConversationDisplayUpdate; import com.wecloud.im.param.add.ImConversationDisplayUpdate;
import com.wecloud.im.param.add.ImConversationNameUpdate;
import com.wecloud.im.service.ImApplicationService; import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService; import com.wecloud.im.service.ImClientService;
import com.wecloud.im.service.ImConversationMembersService; import com.wecloud.im.service.ImConversationMembersService;
...@@ -24,6 +30,10 @@ import com.wecloud.im.service.ImMessageService; ...@@ -24,6 +30,10 @@ import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.vo.ImConversationCreateVo; import com.wecloud.im.vo.ImConversationCreateVo;
import com.wecloud.im.vo.MyConversationListVo; import com.wecloud.im.vo.MyConversationListVo;
import com.wecloud.im.vo.OfflineMsgDto; import com.wecloud.im.vo.OfflineMsgDto;
import com.wecloud.im.ws.enums.MsgTypeEnum;
import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.service.WriteDataService;
import io.geekidea.springbootplus.framework.common.api.ApiCode; import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult; import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.service.impl.BaseServiceImpl; import io.geekidea.springbootplus.framework.common.service.impl.BaseServiceImpl;
...@@ -41,6 +51,7 @@ import java.util.ArrayList; ...@@ -41,6 +51,7 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 会话表 服务实现类 * 会话表 服务实现类
...@@ -52,6 +63,12 @@ import java.util.List; ...@@ -52,6 +63,12 @@ import java.util.List;
@Service @Service
public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMapper, ImConversation> implements ImConversationService { public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMapper, ImConversation> implements ImConversationService {
private static final JsonMapper JSON_MAPPER = new JsonMapper();
@Autowired
private WriteDataService writeDataService;
@Autowired @Autowired
private ImConversationMapper imConversationMapper; private ImConversationMapper imConversationMapper;
...@@ -82,6 +99,11 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap ...@@ -82,6 +99,11 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap
ImClient createClient = imClientService.getCurentClient(); ImClient createClient = imClientService.getCurentClient();
if (imConversationCreate.getClientIds().size() == 0) {
log.info("没有成员");
return ApiResult.result(ApiCode.CLIENT_NOT_FOUNT, null);
}
// 成员不存在,不能创建会话 // 成员不存在,不能创建会话
for (String id : imConversationCreate.getClientIds()) { for (String id : imConversationCreate.getClientIds()) {
ImClient imClient = imClientService.getOne(new QueryWrapper<ImClient>().lambda() ImClient imClient = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
...@@ -99,21 +121,21 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap ...@@ -99,21 +121,21 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap
// 根据appKey查询application // 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey()); ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
// 该应用 是否允许创建重复会话 0不允许 1允许 // 该应用 是否允许创建重复一对一会话 0不允许 1允许
if (imApplication.getRepeatSessionStatus() != null && imApplication.getRepeatSessionStatus() == 0) { if (imApplication.getRepeatSessionStatus() != null && imApplication.getRepeatSessionStatus() == 0) {
// 判断是否已经存在会话 // 判断是否已经存在一对一会话
// size() == 1 为单聊不允许重复创建 两个用户如果已经创建过会话,不能重复创建会话 // size() == 1 为单聊不允许重复创建 两个用户如果已经创建过会话,不能重复创建会话
if (imConversationCreate.getClientIds().size() == 1) { if (imConversationCreate.getClientIds().size() == 1) {
ImClient client2 = imClientService.getOne(new QueryWrapper<ImClient>().lambda() ImClient client2 = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, createClient.getFkAppid()) .eq(ImClient::getFkAppid, createClient.getFkAppid())
.eq(ImClient::getClientId, imConversationCreate.getClientIds().get(0))); .eq(ImClient::getClientId, imConversationCreate.getClientIds().get(0)));
// 是否存在重复会话 // 是否存在重复一对一会话
Integer repetitionConversation = getRepetitionConversation(createClient.getId(), client2.getId()); Integer repetitionConversation = getRepetitionConversation(createClient.getId(), client2.getId());
if (repetitionConversation != 0) { if (repetitionConversation != 0) {
log.info("repetitionConversation != 0"); log.info("repetitionConversation != 0");
// 返回已存在的会话id // 返回已存在的一对一会话id
ImConversation repetitionConversationInfo = imConversationMapper.getRepetitionConversationInfo(createClient.getId(), client2.getId()); ImConversation repetitionConversationInfo = imConversationMapper.getRepetitionConversationInfo(createClient.getId(), client2.getId());
ImConversationCreateVo imConversationCreateVo = new ImConversationCreateVo(); ImConversationCreateVo imConversationCreateVo = new ImConversationCreateVo();
imConversationCreateVo.setId(repetitionConversationInfo.getId()); imConversationCreateVo.setId(repetitionConversationInfo.getId());
...@@ -123,7 +145,7 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap ...@@ -123,7 +145,7 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap
} }
} else { } else {
//创建重复会话时对比扩展字段 1是 //创建重复一对一会话时对比扩展字段 1是
if (imApplication.getContrastExtendedFieldStatus() == 1) { if (imApplication.getContrastExtendedFieldStatus() == 1) {
// 被邀请client // 被邀请client
...@@ -201,6 +223,535 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap ...@@ -201,6 +223,535 @@ public class ImConversationServiceImpl extends BaseServiceImpl<ImConversationMap
return ApiResult.ok(imConversationCreateVo); return ApiResult.ok(imConversationCreateVo);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> addClientToConversation(ImClientToConversation imClientToConversation) {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
// 查询该会话所有成员
List<ImConversationMembers> membersList = imConversationMembersService.list(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imClientToConversation.getConversationId())
.notIn(ImConversationMembers::getId, imClientSender.getId())
);
if (membersList.isEmpty()) {
log.info("membersList为空,toConversationId:" + imClientToConversation.getConversationId());
return ApiResult.fail();
}
// 将他人添加到会话
for (String id : imClientToConversation.getClientIds()) {
ImClient client2 = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getClientId, id));
// 判断用户是否已经在该会话
ImConversationMembers members = imConversationMembersService.getOne(new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imClientToConversation.getConversationId())
.eq(ImConversationMembers::getFkClientId, client2.getId())
);
// 已经在该会话 则跳过
if (members != null) {
continue;
}
Long imConversationMembersId2 = SnowflakeUtil.getId();
ImConversationMembers imConversationMembers2 = new ImConversationMembers();
imConversationMembers2.setId(imConversationMembersId2);
imConversationMembers2.setCreateTime(new Date());
imConversationMembers2.setFkAppid(imApplication.getId());
imConversationMembers2.setFkConversationId(imClientToConversation.getConversationId());
imConversationMembers2.setFkClientId(client2.getId());
imConversationMembersService.save(imConversationMembers2);
// ws邀请事件通知给群内其他人 ----------
// 生成消息id
long messageId = SnowflakeUtil.getId();
ImMessage imMessage = new ImMessage();
Map<String, Object> content = new HashMap<>();
content.put("operator", imClientSender.getClientId()); //操作的client ID
content.put("passivityOperator", client2.getClientId()); //被操作的client ID
content.put("type", MsgTypeEnum.INVITE_CLIENT_JOIN_CONVERSATION.getUriCode()); //xx邀请xx加入会话 -1007
try {
String contentString = JSON_MAPPER.writeValueAsString(content);
imMessage.setContent(contentString);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 保存消息至消息表
imMessage.setId(messageId);
imMessage.setCreateTime(new Date());
imMessage.setFkAppid(imApplication.getId());
imMessage.setSender(imClientSender.getId());
imMessage.setWithdraw(false);
imMessage.setEvent(true);
imMessage.setSystem(false);
imMessage.setSendStatus(2);
imMessage.setFkConversationId(imClientToConversation.getConversationId());
boolean save = imMessageService.save(imMessage);
// 遍历发送
for (ImConversationMembers conversationMembers : membersList) {
// 查询接收方
ImClient imClientReceiver = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getId, conversationMembers.getFkClientId()));
if (imClientReceiver == null) {
continue;
}
// 封装响应的实体
ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend();
imMessageOnlineSend.setMsgId(messageId);
imMessageOnlineSend.setSender(imClientSender.getClientId());
imMessageOnlineSend.setContent((HashMap) content);
imMessageOnlineSend.setConversationId(conversationMembers.getFkConversationId());
imMessageOnlineSend.setWithdraw(Boolean.FALSE);
imMessageOnlineSend.setEvent(Boolean.TRUE);
// 向接收方推送
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.CONVERSATION_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(imMessageOnlineSend);
responseModel.setReqId(null);
writeDataService.write(responseModel, imApplication.getAppKey(), imClientReceiver.getClientId());
}
}
return ApiResult.ok();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> delClientToConversation(ImClientToConversation imClientToConversation) throws Exception {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
// 判断是否为群主
ImConversation imConversationById = imConversationService.getById(imClientToConversation.getConversationId());
if (imConversationById == null || !imConversationById.getCreator().equals(imClientSender.getId())) {
return ApiResult.result(false);
}
// 查询该会话所有成员
List<ImConversationMembers> membersList = imConversationMembersService.list(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imClientToConversation.getConversationId())
.notIn(ImConversationMembers::getId, imClientSender.getId())
);
if (membersList.isEmpty()) {
log.info("membersList为空,toConversationId:" + imClientToConversation.getConversationId());
return ApiResult.fail();
}
// 将client从会话移除
for (String id : imClientToConversation.getClientIds()) {
ImClient client2 = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getClientId, id));
// 判断用户是否已经在该会话
ImConversationMembers members = imConversationMembersService.getOne(new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imClientToConversation.getConversationId())
.eq(ImConversationMembers::getFkClientId, client2.getId())
);
// 则跳过
imConversationMembersService.deleteImConversationMembers(members.getId());
// ws移除事件通知给群内其他人 ----------
// 生成消息id
long messageId = SnowflakeUtil.getId();
ImMessage imMessage = new ImMessage();
Map<String, Object> content = new HashMap<>();
content.put("operator", imClientSender.getClientId()); //操作的client ID
content.put("passivityOperator", client2.getClientId()); //被操作的client ID
content.put("type", MsgTypeEnum.REMOVE_CLIENT_CONVERSATION.getUriCode()); //xx被xx移出会话 -1008
try {
String asString = JSON_MAPPER.writeValueAsString(content);
imMessage.setContent(asString);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 保存消息至消息表
imMessage.setId(messageId);
imMessage.setCreateTime(new Date());
imMessage.setFkAppid(imApplication.getId());
imMessage.setSender(imClientSender.getId());
imMessage.setWithdraw(false);
imMessage.setEvent(true);
imMessage.setSystem(false);
imMessage.setSendStatus(2);
imMessage.setFkConversationId(imClientToConversation.getConversationId());
boolean save = imMessageService.save(imMessage);
// 遍历发送
for (ImConversationMembers conversationMembers : membersList) {
// 查询接收方
ImClient imClientReceiver = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getId, conversationMembers.getFkClientId()));
if (imClientReceiver == null) {
continue;
}
// 封装响应的实体
ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend();
imMessageOnlineSend.setMsgId(messageId);
imMessageOnlineSend.setSender(imClientSender.getClientId());
imMessageOnlineSend.setContent((HashMap) content);
imMessageOnlineSend.setConversationId(conversationMembers.getFkConversationId());
imMessageOnlineSend.setWithdraw(Boolean.FALSE);
imMessageOnlineSend.setEvent(Boolean.TRUE);
// 向接收方推送
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.CONVERSATION_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(imMessageOnlineSend);
responseModel.setReqId(null);
writeDataService.write(responseModel, imApplication.getAppKey(), imClientReceiver.getClientId());
}
}
return ApiResult.ok();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> leaveConversation(ImClientLeaveConversation imClientToConversation) throws Exception {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
ImConversation imConversationById = imConversationService.getById(imClientToConversation.getConversationId());
if (imConversationById == null) {
throw new Exception("imConversationById == null");
}
// 查询该会话所有成员
List<ImConversationMembers> membersList = imConversationMembersService.list(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkConversationId, imClientToConversation.getConversationId())
.orderByAsc(ImConversationMembers::getCreateTime)
.notIn(ImConversationMembers::getId, imClientSender.getId())
);
if (membersList.isEmpty()) {
log.info("membersList为空,toConversationId:" + imClientToConversation.getConversationId());
throw new Exception("deleteImConversationMembers");
}
// 判断用户是否在该会话
ImConversationMembers members = imConversationMembersService.getOne(new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imClientToConversation.getConversationId())
.eq(ImConversationMembers::getFkClientId, imClientSender.getId())
);
// 将client从会话移除
boolean b = imConversationMembersService.deleteImConversationMembers(members.getId());
if (!b) {
throw new Exception("deleteImConversationMembers");
}
// ws 退出事件通知给群内其他人 ----------
// 生成消息id
long messageId = SnowflakeUtil.getId();
ImMessage imMessage = new ImMessage();
Map<String, Object> content = new HashMap<>();
content.put("type", MsgTypeEnum.LEAVE_CONVERSATION.getUriCode()); // xx主动退出会话
try {
String asString = JSON_MAPPER.writeValueAsString(content);
imMessage.setContent(asString);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 保存消息至消息表
imMessage.setId(messageId);
imMessage.setCreateTime(new Date());
imMessage.setFkAppid(imApplication.getId());
imMessage.setSender(imClientSender.getId());
imMessage.setWithdraw(false);
imMessage.setEvent(true);
imMessage.setSystem(false);
imMessage.setSendStatus(2);
imMessage.setFkConversationId(imClientToConversation.getConversationId());
boolean save = imMessageService.save(imMessage);
if (!save) {
throw new Exception("deleteImConversationMembers");
}
// 遍历发送
for (ImConversationMembers conversationMembers : membersList) {
// 查询接收方
ImClient imClientReceiver = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getId, conversationMembers.getFkClientId()));
if (imClientReceiver == null) {
continue;
}
// 封装响应的实体
ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend();
imMessageOnlineSend.setMsgId(messageId);
imMessageOnlineSend.setSender(imClientSender.getClientId());
imMessageOnlineSend.setContent((HashMap) content);
imMessageOnlineSend.setConversationId(conversationMembers.getFkConversationId());
imMessageOnlineSend.setWithdraw(Boolean.FALSE);
imMessageOnlineSend.setEvent(Boolean.TRUE);
// 向接收方推送
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.CONVERSATION_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(imMessageOnlineSend);
responseModel.setReqId(null);
writeDataService.write(responseModel, imApplication.getAppKey(), imClientReceiver.getClientId());
}
// 群主退出 是否需要转移给下一个人
if (imConversationById.getCreator().equals(imClientSender.getId()) && imClientToConversation.getTransfer()) {
ImConversationMembers conversationMember = membersList.get(0);
imConversationById.setCreator(conversationMember.getFkClientId());
// 创建者权限转移给下一个人
imConversationService.updateById(imConversationById);
}
return ApiResult.ok();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> saveOrUpdateName(ImConversationNameUpdate imConversationNameUpdate) throws Exception {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
ImConversation imConversationById = imConversationService.getById(imConversationNameUpdate.getConversationId());
// 判断是否为群主
if (imConversationById == null || !imConversationById.getCreator().equals(imClientSender.getId())) {
return ApiResult.result(false);
}
// 查询该会话所有成员
List<ImConversationMembers> membersList = imConversationMembersService.list(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkConversationId, imConversationNameUpdate.getConversationId())
.notIn(ImConversationMembers::getId, imClientSender.getId())
);
imConversationById.setName(imConversationNameUpdate.getName());
boolean b = imConversationService.updateById(imConversationById);
if (b) {
// 下发群名称变动事件
// ws下发群属性变动事件
// 内容
HashMap<String, String> content = new HashMap<>();
content.put("type", "-1015");
content.put("name", imConversationById.getName());
// 遍历发送
for (ImConversationMembers conversationMembers : membersList) {
// 查询接收方
ImClient imClientReceiver = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getId, conversationMembers.getFkClientId()));
if (imClientReceiver == null) {
continue;
}
// 封装响应的实体
ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend();
// imMessageOnlineSend.setMsgId(null);
// imMessageOnlineSend.setSender(imClientSender.getClientId());
imMessageOnlineSend.setContent(content);
imMessageOnlineSend.setConversationId(conversationMembers.getFkConversationId());
// imMessageOnlineSend.setWithdraw(Boolean.FALSE);
imMessageOnlineSend.setEvent(Boolean.TRUE);
// 向接收方推送
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.CONVERSATION_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(imMessageOnlineSend);
responseModel.setReqId(null);
writeDataService.write(responseModel, imApplication.getAppKey(), imClientReceiver.getClientId());
}
return ApiResult.ok();
} else {
return ApiResult.fail();
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> saveOrUpdateAttr(ImConversationAttrUpdate imConversationAttrUpdate) throws Exception {
// shiro线程中获取当前token
JwtToken curentJwtToken = JwtUtil.getCurentJwtToken();
// 根据appKey查询application
ImApplication imApplication = imApplicationService.getOneByAppKey(curentJwtToken.getAppKey());
ImClient imClientSender = imClientService.getCurentClient();
ImConversation imConversationById = imConversationService.getById(imConversationAttrUpdate.getConversationId());
// 判断是否为群主
// if (imConversationById == null || !imConversationById.getCreator().equals(imClientSender.getId())) {
// return ApiResult.result(false);
// }
// 查询该会话所有成员
List<ImConversationMembers> membersList = imConversationMembersService.list(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkAppid, imApplication.getId())
.eq(ImConversationMembers::getFkConversationId, imConversationAttrUpdate.getConversationId())
.notIn(ImConversationMembers::getId, imClientSender.getId())
);
JsonMapper jsonMapper = new JsonMapper();
try {
String attributes = jsonMapper.writeValueAsString(imConversationAttrUpdate.getAttributes());
imConversationById.setAttributes(attributes);
} catch (JsonProcessingException e) {
e.printStackTrace();
return ApiResult.fail();
}
boolean b = imConversationService.updateById(imConversationById);
if (b) {
// ws下发拓展字段变动事件
// 内容
HashMap<String, String> content = new HashMap<>();
content.put("type", "-1014");
content.put("attributes", imConversationById.getAttributes());
// 遍历发送
for (ImConversationMembers conversationMembers : membersList) {
// 查询接收方
ImClient imClientReceiver = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getId, conversationMembers.getFkClientId()));
if (imClientReceiver == null) {
continue;
}
// 封装响应的实体
ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend();
// imMessageOnlineSend.setMsgId(null);
// imMessageOnlineSend.setSender(imClientSender.getClientId());
imMessageOnlineSend.setContent(content);
imMessageOnlineSend.setConversationId(conversationMembers.getFkConversationId());
// imMessageOnlineSend.setWithdraw(Boolean.FALSE);
imMessageOnlineSend.setEvent(Boolean.TRUE);
// 向接收方推送
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.CONVERSATION_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(imMessageOnlineSend);
responseModel.setReqId(null);
writeDataService.write(responseModel, imApplication.getAppKey(), imClientReceiver.getClientId());
}
return ApiResult.ok();
} else {
return ApiResult.fail();
}
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public boolean updateImConversation(ImConversation imConversation) throws Exception { public boolean updateImConversation(ImConversation imConversation) throws Exception {
......
package com.wecloud.im.service.impl; package com.wecloud.im.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wecloud.im.entity.ImApplication; import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient; import com.wecloud.im.entity.ImClient;
...@@ -14,7 +15,9 @@ import com.wecloud.im.service.ImClientService; ...@@ -14,7 +15,9 @@ import com.wecloud.im.service.ImClientService;
import com.wecloud.im.service.ImConversationMembersService; import com.wecloud.im.service.ImConversationMembersService;
import com.wecloud.im.service.ImInboxService; import com.wecloud.im.service.ImInboxService;
import com.wecloud.im.service.ImMessageService; import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.ws.model.ResponseModel; import com.wecloud.im.ws.enums.MsgTypeEnum;
import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.service.WriteDataService; import com.wecloud.im.ws.service.WriteDataService;
import io.geekidea.springbootplus.framework.common.api.ApiCode; import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult; import io.geekidea.springbootplus.framework.common.api.ApiResult;
...@@ -99,7 +102,7 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox> ...@@ -99,7 +102,7 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox>
// 内容 // 内容
HashMap<String, String> stringStringHashMap = new HashMap<>(); HashMap<String, String> stringStringHashMap = new HashMap<>();
stringStringHashMap.put("type", "-1009"); stringStringHashMap.put("type", MsgTypeEnum.CLIENT_RECEIVED_MSG.getUriCode() + "");
stringStringHashMap.put("receiverId", curentClient.getClientId()); stringStringHashMap.put("receiverId", curentClient.getClientId());
// 推送给接收方 // 推送给接收方
...@@ -142,7 +145,7 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox> ...@@ -142,7 +145,7 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox>
// 内容 // 内容
HashMap<String, String> stringStringHashMap = new HashMap<>(); HashMap<String, String> stringStringHashMap = new HashMap<>();
stringStringHashMap.put("type", "-1010"); stringStringHashMap.put("type", MsgTypeEnum.CLIENT_READ_MSG + "");
stringStringHashMap.put("receiverId", curentClient.getClientId()); stringStringHashMap.put("receiverId", curentClient.getClientId());
sendMsgStatus(curentClient, application, stringStringHashMap, imMsgReadStatusUpdate.getMsgIds()); sendMsgStatus(curentClient, application, stringStringHashMap, imMsgReadStatusUpdate.getMsgIds());
...@@ -203,8 +206,8 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox> ...@@ -203,8 +206,8 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox>
continue; continue;
} }
// 向接收方推送 // 向接收方推送
ResponseModel<ImMessageOnlineSend> responseModel = new ResponseModel<>(); WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(ResponseModel.ONLINE_EVENT_MSG); responseModel.setCmd(WsResponseCmdEnum.ONLINE_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS); ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode()); responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage()); responseModel.setMsg(result.getMessage());
......
...@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.OrderItem; ...@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.entity.ImApiMessageOnlineSend;
import com.wecloud.im.entity.ImApplication; import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient; import com.wecloud.im.entity.ImClient;
import com.wecloud.im.entity.ImConversation; import com.wecloud.im.entity.ImConversation;
...@@ -15,6 +16,7 @@ import com.wecloud.im.entity.ImMessageOnlineSend; ...@@ -15,6 +16,7 @@ import com.wecloud.im.entity.ImMessageOnlineSend;
import com.wecloud.im.mapper.ImMessageMapper; import com.wecloud.im.mapper.ImMessageMapper;
import com.wecloud.im.param.ImHistoryMessagePageParam; import com.wecloud.im.param.ImHistoryMessagePageParam;
import com.wecloud.im.param.add.ImMsgRecall; import com.wecloud.im.param.add.ImMsgRecall;
import com.wecloud.im.param.add.ImMsgSendToOnlineClient;
import com.wecloud.im.param.add.ImMsgUpdate; import com.wecloud.im.param.add.ImMsgUpdate;
import com.wecloud.im.service.ImApplicationService; import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService; import com.wecloud.im.service.ImClientService;
...@@ -23,7 +25,8 @@ import com.wecloud.im.service.ImConversationService; ...@@ -23,7 +25,8 @@ import com.wecloud.im.service.ImConversationService;
import com.wecloud.im.service.ImMessageService; import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.vo.ImMessageOfflineListVo; import com.wecloud.im.vo.ImMessageOfflineListVo;
import com.wecloud.im.vo.OfflineMsgDto; import com.wecloud.im.vo.OfflineMsgDto;
import com.wecloud.im.ws.model.ResponseModel; import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.request.PushModel; import com.wecloud.im.ws.model.request.PushModel;
import com.wecloud.im.ws.sender.PushTask; import com.wecloud.im.ws.sender.PushTask;
import com.wecloud.im.ws.service.WriteDataService; import com.wecloud.im.ws.service.WriteDataService;
...@@ -52,6 +55,8 @@ import java.util.List; ...@@ -52,6 +55,8 @@ import java.util.List;
@Service @Service
public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMessage> implements ImMessageService { public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMessage> implements ImMessageService {
// private static final JsonMapper JSON_MAPPER = new JsonMapper();
@Autowired @Autowired
private ImMessageMapper imMessageMapper; private ImMessageMapper imMessageMapper;
...@@ -76,6 +81,86 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes ...@@ -76,6 +81,86 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> restApiImMessageSend(ImMsgSendToOnlineClient imMsgSendToOnlineClient, ImApplication imApplication) {
// 查询该会话所有成员
List<ImConversationMembers> membersList = imConversationMembersService.list(
new QueryWrapper<ImConversationMembers>().lambda()
.eq(ImConversationMembers::getFkConversationId, imMsgSendToOnlineClient.getConversationId())
);
if (membersList.isEmpty()) {
log.info("membersList为空,toConversationId:" + imMsgSendToOnlineClient.getConversationId());
return ApiResult.fail();
}
ImApiMessageOnlineSend imApiMessageOnlineSend = new ImApiMessageOnlineSend();
imApiMessageOnlineSend.setCreateTime(new Date());
JsonMapper jsonMapper = new JsonMapper();
try {
String attributes = jsonMapper.writeValueAsString(imMsgSendToOnlineClient.getContent());
imApiMessageOnlineSend.setContent(attributes);
} catch (JsonProcessingException e) {
e.printStackTrace();
return ApiResult.fail();
}
imApiMessageOnlineSend.setConversationId(imMsgSendToOnlineClient.getConversationId());
// 遍历发送
for (ImConversationMembers conversationMembers : membersList) {
// 查询接收方
ImClient imClientReceiver = imClientService.getOne(new QueryWrapper<ImClient>().lambda()
.eq(ImClient::getFkAppid, imApplication.getId())
.eq(ImClient::getId, conversationMembers.getFkClientId()));
if (imClientReceiver == null) {
continue;
}
WsResponseModel<ImApiMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.REST_API_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(imApiMessageOnlineSend);
responseModel.setReqId(null);
// 向接收方推送
writeDataService.write(responseModel, imApplication.getAppKey(), imClientReceiver.getClientId());
}
return null;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ImMessage saveImMessage(ImApplication imApplication, ImClient imClientSender, Long toConversationId, long messageId, String content) {
ImMessage imMessage = new ImMessage();
imMessage.setId(messageId);
imMessage.setCreateTime(new Date());
imMessage.setFkAppid(imApplication.getId());
imMessage.setSender(imClientSender.getId());
imMessage.setContent(content);
imMessage.setWithdraw(false);
imMessage.setEvent(false);
imMessage.setSystem(false);
imMessage.setSendStatus(2);
imMessage.setFkConversationId(toConversationId);
this.save(imMessage);
return imMessage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> updateMsgWithdrawById(ImMsgRecall imMsgRecall) { public ApiResult<Boolean> updateMsgWithdrawById(ImMsgRecall imMsgRecall) {
ImClient imClientSender = imClientService.getCurentClient(); ImClient imClientSender = imClientService.getCurentClient();
...@@ -99,6 +184,7 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes ...@@ -99,6 +184,7 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes
// 修改消息体 // 修改消息体
messageById.setWithdraw(Boolean.TRUE); messageById.setWithdraw(Boolean.TRUE);
messageById.setWithdrawTime(new Date()); messageById.setWithdrawTime(new Date());
// 清空消息
messageById.setContent("{}"); messageById.setContent("{}");
boolean saveOk = this.updateById(messageById); boolean saveOk = this.updateById(messageById);
...@@ -150,8 +236,8 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes ...@@ -150,8 +236,8 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes
imMessageOnlineSend.setEvent(Boolean.TRUE); imMessageOnlineSend.setEvent(Boolean.TRUE);
// 向接收方推送 // 向接收方推送
ResponseModel<ImMessageOnlineSend> responseModel = new ResponseModel<>(); WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(ResponseModel.ONLINE_EVENT_MSG); responseModel.setCmd(WsResponseCmdEnum.ONLINE_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS); ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode()); responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage()); responseModel.setMsg(result.getMessage());
...@@ -177,7 +263,7 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes ...@@ -177,7 +263,7 @@ public class ImMessageServiceImpl extends BaseServiceImpl<ImMessageMapper, ImMes
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public ApiResult<Boolean> updateMsgById(ImMsgUpdate imMsgUpdate) { public ApiResult<Boolean> updateMsgById(ImMsgUpdate imMsgUpdate) {
ImClient client = imClientService.getCurentClient(); // ImClient client = imClientService.getCurentClient();
/* // 判断该消息是否是该客户端发送 /* // 判断该消息是否是该客户端发送
ImMessage messageById = this.getById(imMsgUpdate.getId()); ImMessage messageById = this.getById(imMsgUpdate.getId());
......
package com.wecloud.im.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@ApiModel(value = "GetInfoListVo")
public class GetInfoListVo implements Serializable {
@ApiModelProperty("会话中client的备注名")
private String clientRemarkName;
@ApiModelProperty("头像")
private String headPortrait;
@ApiModelProperty("主昵称")
private String nickname;
@ApiModelProperty("clientId")
private String clientId;
@ApiModelProperty("client自己的自定义扩展属性")
private String clientAttributes;
@ApiModelProperty("会话成员列表的自定义扩展属性")
private String memberAttributes;
}
package com.wecloud.im.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@ApiModel(value = "ImConversationMemberListVo")
public class ImConversationMemberListVo implements Serializable {
@ApiModelProperty("会话中client的备注名")
private String clientRemarkName;
@ApiModelProperty("头像")
private String headPortrait;
@ApiModelProperty("主昵称")
private String nickname;
@ApiModelProperty("clientId")
private String clientId;
@ApiModelProperty("client自己的自定义扩展属性")
private String clientAttributes;
@ApiModelProperty("会话成员列表的自定义扩展属性")
private String memberAttributes;
}
...@@ -17,7 +17,7 @@ import java.lang.annotation.Target; ...@@ -17,7 +17,7 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//RUNTIME运行时保留 @Retention(RetentionPolicy.RUNTIME)//RUNTIME运行时保留
@Target(ElementType.TYPE) //type描述类、接口 @Target(ElementType.TYPE) //type描述类、接口
@Documented @Documented
public @interface ReceiveTypeAnnotation { public @interface CmdTypeAnnotation {
WsRequestCmdEnum type(); WsRequestCmdEnum type();
} }
package com.wecloud.im.ws.enums;
/**
* @Description ws响应类型
* @Author xxxname xxxemail.com
* @Date 2019-12-05
*/
public enum MsgTypeEnum {
// xx邀请xx加入会话 -1007
INVITE_CLIENT_JOIN_CONVERSATION(-1007),
// xx被xx移出会话 -1008
REMOVE_CLIENT_CONVERSATION(-1008),
// xx已接收某消息 -1009
CLIENT_RECEIVED_MSG(-1009),
// xx已读某条消息 -1010
CLIENT_READ_MSG(-1010),
// 你被xx拉入新会话 -1011
CLIENT_JOIN_NEW_CONVERSATION(-1011),
// 主动退出会话 -1012
LEAVE_CONVERSATION(-1012),
// 成为新群主 -1013
CONVERSATION_NEW_CREATOR(-1013),
;
private final int uriCode;
MsgTypeEnum(int uriCode) {
this.uriCode = uriCode;
}
/**
* 根据uriCode获取
*
* @param uriCode
* @return
*/
public static MsgTypeEnum getByCode(int uriCode) {
for (MsgTypeEnum wsResponsePathEnum : values()) {
if (wsResponsePathEnum.getUriCode() == uriCode) {
return wsResponsePathEnum;
}
}
return null;
}
public int getUriCode() {
return uriCode;
}
}
...@@ -15,7 +15,12 @@ public enum WsRequestCmdEnum { ...@@ -15,7 +15,12 @@ public enum WsRequestCmdEnum {
/** /**
* ping * ping
*/ */
PING(2); PING(2),
/**
* 单人WebRTC音视频通话
*/
SINGLE_RTC(3);
private final int cmdCode; private final int cmdCode;
......
...@@ -8,14 +8,35 @@ package com.wecloud.im.ws.enums; ...@@ -8,14 +8,35 @@ package com.wecloud.im.ws.enums;
public enum WsResponseCmdEnum { public enum WsResponseCmdEnum {
/** /**
* 离线消息下发完成指令 * 服务端下发透传消息
*/ */
OFFLINE_MSG_SUC(100), REST_API_MSG(6),
/** /**
* 主动下发消息指令 * 会话中的事件
*/ */
WRITE_MSG(101); CONVERSATION_EVENT_MSG(5),
/**
* 下发在线RTC事件
*/
SINGLE_RTC_MSG(4),
/**
* 下发在线事件消息
*/
ONLINE_EVENT_MSG(3),
/**
* 下发在线基本类型消息
*/
ONLINE_MSG(2),
/**
* 响应数据类型
*/
RES(1);
private final int cmdCode; private final int cmdCode;
...@@ -23,20 +44,6 @@ public enum WsResponseCmdEnum { ...@@ -23,20 +44,6 @@ public enum WsResponseCmdEnum {
this.cmdCode = uriCode; this.cmdCode = uriCode;
} }
/**
* 根据uriCode获取
*
* @param uriCode
* @return
*/
public static WsResponseCmdEnum getByCode(int uriCode) {
for (WsResponseCmdEnum wsResponsePathEnum : values()) {
if (wsResponsePathEnum.getCmdCode() == uriCode) {
return wsResponsePathEnum;
}
}
return null;
}
public int getCmdCode() { public int getCmdCode() {
return cmdCode; return cmdCode;
......
package com.wecloud.im.ws.enums;
/**
* @Description webrtc响应类型
* @Author hewei hwei1233@163.com
* @Date 2019-12-05
*/
public enum WsRtcResponseSubCmdEnum {
// --- 服务端响应
/**
* 接收到RTC邀请
*/
RTC_CALL(1),
/**
* 用户状态更新事件(用户加入频道)
*/
CLIENT_JOIN(2),
/**
* 用户状态更新事件(用户退出频道)
*/
CLIENT_LEAVE(3),
/**
* 用户状态更新事件(用户拒接邀请,不同意进入频道)
*/
CLIENT_REJECT(4),
/**
* SDP数据转发
*/
SDP_FORWARD(5),
/**
* candidate候选者数据转发
*/
CANDIDATE_FORWARD(6);
private final int cmdCode;
WsRtcResponseSubCmdEnum(int cmdCode) {
this.cmdCode = cmdCode;
}
public int getCmdCode() {
return cmdCode;
}
}
...@@ -12,26 +12,11 @@ import java.io.Serializable; ...@@ -12,26 +12,11 @@ import java.io.Serializable;
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class ResponseModel<T> implements Serializable { public class WsResponseModel<T> implements Serializable {
/** /**
* 下发在线事件消息 * 枚举类WsResponseCmdEnum 请求uri的编码
*/ * 由于webSocket使用同一个通道发送数据,需要区分不同类型请求
public static final Integer ONLINE_EVENT_MSG = 3;
/**
* 下发在线基本类型消息
*/
public static final Integer ONLINE_MSG = 2;
/**
* 响应数据类型
*/
public static final Integer RES = 1;
/**
* 枚举类UriPathEnum 请求uri的编码
* 由于websocket使用同一个通道发送数据,需要区分不同类型请求
*/ */
private Integer cmd; private Integer cmd;
......
...@@ -22,4 +22,10 @@ public class PushModel implements Serializable { ...@@ -22,4 +22,10 @@ public class PushModel implements Serializable {
*/ */
private String subTitle; private String subTitle;
/**
* 自定义系统推送内容
*/
private String data;
} }
...@@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.json.JsonMapper; ...@@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.ws.enums.WsRequestCmdEnum; import com.wecloud.im.ws.enums.WsRequestCmdEnum;
import com.wecloud.im.ws.model.request.ReceiveModel; import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.service.WriteDataService; import com.wecloud.im.ws.service.WriteDataService;
import com.wecloud.im.ws.strategy.AbstractReceiveStrategy; import com.wecloud.im.ws.strategy.ImCmdAbstract;
import com.wecloud.im.ws.strategy.ReceiveStrategyContext; import com.wecloud.im.ws.strategy.ImCmdContext;
import io.geekidea.springbootplus.framework.common.exception.BusinessException; import io.geekidea.springbootplus.framework.common.exception.BusinessException;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import org.slf4j.Logger; import org.slf4j.Logger;
...@@ -23,15 +23,12 @@ import javax.annotation.Resource; ...@@ -23,15 +23,12 @@ import javax.annotation.Resource;
public class ReadWsData { public class ReadWsData {
private static final Logger log = LoggerFactory.getLogger(ReadWsData.class); private static final Logger log = LoggerFactory.getLogger(ReadWsData.class);
// idea此处报红 属于正常
// @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Resource @Resource
private ReceiveStrategyContext receiveStrategyContext; private ImCmdContext receiveStrategyContext;
@Resource @Resource
private WriteDataService writeDataService; private WriteDataService writeDataService;
private static final String PING = "ping";
/** /**
* 在此开始进入业务流程子线程,将不占netty的io线程 * 在此开始进入业务流程子线程,将不占netty的io线程
...@@ -42,11 +39,8 @@ public class ReadWsData { ...@@ -42,11 +39,8 @@ public class ReadWsData {
public void convertModel(String data, ChannelHandlerContext ctx, String appKey, String clientId) throws Exception { public void convertModel(String data, ChannelHandlerContext ctx, String appKey, String clientId) throws Exception {
log.info("appWS收到data:" + data + "\nappKey+clientId:" + appKey + ":" + clientId + log.info("appWS收到data:" + data + "\nappKey+clientId:" + appKey + ":" + clientId +
",channelId:" + ctx.channel().id().asShortText()); ",channelId:" + ctx.channel().id().asLongText());
if (PING.equals(data)) {
log.info("收到心跳clientId:" + clientId);
return;
}
// 解析json // 解析json
JsonMapper jsonMapper = new JsonMapper(); JsonMapper jsonMapper = new JsonMapper();
...@@ -57,8 +51,8 @@ public class ReadWsData { ...@@ -57,8 +51,8 @@ public class ReadWsData {
WsRequestCmdEnum wsRequestUriPathEnum = WsRequestCmdEnum.getByCode(receiveModel.getCmd()); WsRequestCmdEnum wsRequestUriPathEnum = WsRequestCmdEnum.getByCode(receiveModel.getCmd());
// 使用策略模式, 根据不同类型请求调用不同实现类 // 使用策略模式, 根据不同类型请求调用不同实现类
AbstractReceiveStrategy receiveStrategy = receiveStrategyContext.getStrategy(wsRequestUriPathEnum); ImCmdAbstract cmdStrategy = receiveStrategyContext.getStrategy(wsRequestUriPathEnum);
receiveStrategy.process(receiveModel, ctx, data, appKey, clientId); cmdStrategy.process(receiveModel, ctx, data, appKey, clientId);
} }
......
...@@ -57,6 +57,7 @@ public class PushTask { ...@@ -57,6 +57,7 @@ public class PushTask {
private static final String title = "title"; private static final String title = "title";
private static final String subTitle = "subTitle"; private static final String subTitle = "subTitle";
private static final String DATA = "data";
/** /**
...@@ -112,6 +113,7 @@ public class PushTask { ...@@ -112,6 +113,7 @@ public class PushTask {
} else { } else {
pushModel.setTitle(pushMap.get(title)); pushModel.setTitle(pushMap.get(title));
pushModel.setSubTitle(pushMap.get(subTitle)); pushModel.setSubTitle(pushMap.get(subTitle));
pushModel.setData(pushMap.get(DATA));
} }
this.push(pushModel, imClientReceiver, imApplication); this.push(pushModel, imClientReceiver, imApplication);
...@@ -119,6 +121,11 @@ public class PushTask { ...@@ -119,6 +121,11 @@ public class PushTask {
} }
private void android(PushModel pushModel, ImClient imClientReceiver, ImApplication imApplication) { private void android(PushModel pushModel, ImClient imClientReceiver, ImApplication imApplication) {
if (imApplication.getAndroidPushChannel() == null) {
return;
}
// 安卓推送通道,友盟:1;firebase:2; 信鸽3 // 安卓推送通道,友盟:1;firebase:2; 信鸽3
if (imApplication.getAndroidPushChannel() == 1) { if (imApplication.getAndroidPushChannel() == 1) {
log.info("友盟"); log.info("友盟");
...@@ -152,6 +159,11 @@ public class PushTask { ...@@ -152,6 +159,11 @@ public class PushTask {
} }
private void ios(PushModel pushModel, ImClient imClientReceiver, ImApplication imApplication) { private void ios(PushModel pushModel, ImClient imClientReceiver, ImApplication imApplication) {
if (imApplication.getIosPushChannel() == null) {
return;
}
// ios推送通道,友盟:1;firebase:2; apns原生:3 // ios推送通道,友盟:1;firebase:2; apns原生:3
if (imApplication.getIosPushChannel() == 1) { if (imApplication.getIosPushChannel() == 1) {
log.info("友盟"); log.info("友盟");
...@@ -202,7 +214,7 @@ public class PushTask { ...@@ -202,7 +214,7 @@ public class PushTask {
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "key=" + imApplication.getFirebaseSecret()); conn.setRequestProperty("Authorization", "key=" + imApplication.getFirebaseSecret());
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
//推送到哪台客户端机器 //推送到哪台客户端机器 设备token
json.put("to", imClientReceiver.getDeviceToken()); json.put("to", imClientReceiver.getDeviceToken());
JSONObject info = new JSONObject(); JSONObject info = new JSONObject();
info.put("title", pushModel.getTitle()); info.put("title", pushModel.getTitle());
...@@ -210,6 +222,9 @@ public class PushTask { ...@@ -210,6 +222,9 @@ public class PushTask {
//数据消息data 通知消息 notification //数据消息data 通知消息 notification
json.put("notification", info); json.put("notification", info);
// 自定义推送内容
json.put("data", pushModel.getData());
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
jsonStr = json.toString(); jsonStr = json.toString();
wr.write(jsonStr); wr.write(jsonStr);
...@@ -232,7 +247,9 @@ public class PushTask { ...@@ -232,7 +247,9 @@ public class PushTask {
private void apnsPush(PushModel pushModel, ImClient imClientReceiver, ImApplication imApplication) { private void apnsPush(PushModel pushModel, ImClient imClientReceiver, ImApplication imApplication) {
// 查询apns证书 // 查询apns证书
ImIosApns apns = imIosApnsService.getImIosApnsByAppId(imApplication.getId()); ImIosApns apns = imIosApnsService.getImIosApnsByAppId(imApplication.getId());
Map<String, Object> customProperty = new HashMap<String, Object>(1); Map<String, Object> customProperty = new HashMap<String, Object>(3);
// 自定义推送内容
customProperty.put("data", pushModel.getData());
String deviceToken = imClientReceiver.getDeviceToken(); String deviceToken = imClientReceiver.getDeviceToken();
String alertTitle = pushModel.getTitle(); String alertTitle = pushModel.getTitle();
String alertBody = pushModel.getSubTitle(); String alertBody = pushModel.getSubTitle();
......
...@@ -34,15 +34,17 @@ public interface MangerChannelService { ...@@ -34,15 +34,17 @@ public interface MangerChannelService {
/** /**
* LANGUAGE * LANGUAGE
*/ */
AttributeKey<String> LANGUAGE = AttributeKey.valueOf("lan"); AttributeKey<String> LANGUAGE = AttributeKey.valueOf("la");
/** /**
* APP_VERSION * APP_VERSION
*/ */
// AttributeKey<String> APP_VERSION = AttributeKey.valueOf("appVersion"); AttributeKey<String> APP_VERSION = AttributeKey.valueOf("av");
// AttributeKey<String> TOKEN = AttributeKey.valueOf("TOKEN"); AttributeKey<String> TOKEN = AttributeKey.valueOf("to");
// AttributeKey<String> DEVICEID = AttributeKey.valueOf("DEVICEID"); AttributeKey<String> DEVICEID = AttributeKey.valueOf("dc");
// AttributeKey<String> PLATFORM = AttributeKey.valueOf("PLATFORM"); AttributeKey<String> PLATFORM = AttributeKey.valueOf("pt");
AttributeKey<Integer> READ_IDLE_TIMES = AttributeKey.valueOf("readIdleTimes");
/** /**
* 根据userID获取channel * 根据userID获取channel
......
package com.wecloud.im.ws.service; package com.wecloud.im.ws.service;
import com.wecloud.im.ws.model.ResponseModel; import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.request.ReceiveModel; import com.wecloud.im.ws.model.request.ReceiveModel;
import io.geekidea.springbootplus.framework.common.api.ApiCode; import io.geekidea.springbootplus.framework.common.api.ApiCode;
...@@ -47,7 +47,7 @@ public interface WriteDataService { ...@@ -47,7 +47,7 @@ public interface WriteDataService {
* *
* @param responseModel * @param responseModel
*/ */
void write(ResponseModel responseModel, String toAppKey, String toClientId); void write(WsResponseModel responseModel, String toAppKey, String toClientId);
} }
...@@ -32,7 +32,7 @@ public class MangerChannelServiceImpl implements MangerChannelService { ...@@ -32,7 +32,7 @@ public class MangerChannelServiceImpl implements MangerChannelService {
* <p> * <p>
* 由于来自远程端调用下发数据 如果是群聊1000人群则调用1000次 为不要占用太多资源 需要排队下发 * 由于来自远程端调用下发数据 如果是群聊1000人群则调用1000次 为不要占用太多资源 需要排队下发
* 经过并发测试 200并发1000人群消息 需要调用200x1000=20w次 考虑单机cpu性能还要顾及本机api业务 设置阻塞队列 * 经过并发测试 200并发1000人群消息 需要调用200x1000=20w次 考虑单机cpu性能还要顾及本机api业务 设置阻塞队列
* 为避免过多占用本地io线程导致response慢,设置LinkedBlockingQueue数量多可以避免抢占,TODO (队列数量需要测试调试到最优数量 ) * 为避免过多占用本地io线程导致response慢,设置LinkedBlockingQueue数量多可以避免抢占, (队列数量需要测试调试到最优数量 )
* 最大线程数量不要设置太多 数量、优先级一定要比本地io线程低级 * 最大线程数量不要设置太多 数量、优先级一定要比本地io线程低级
* *
* *
...@@ -108,8 +108,8 @@ public class MangerChannelServiceImpl implements MangerChannelService { ...@@ -108,8 +108,8 @@ public class MangerChannelServiceImpl implements MangerChannelService {
// //
// //
// /** // /**
// * TODO 待完成: 根据ACK回执 以及线程等待超时机制来判断客户端是否离线和超时; // * 待完成: 根据ACK回执 以及线程等待超时机制来判断客户端是否离线和超时;
// * TODO 待完成: 发送后阻塞当前子线程2秒后获取ack回执 如客户端发起ack回执则需要主动唤醒当前子线程 立马唤醒当前子线程, 判断如果已回执则返回发送成功, 如果未回执则判断客户端是否断线或发送错误 // * 待完成: 发送后阻塞当前子线程2秒后获取ack回执 如客户端发起ack回执则需要主动唤醒当前子线程 立马唤醒当前子线程, 判断如果已回执则返回发送成功, 如果未回执则判断客户端是否断线或发送错误
// * // *
// * @param msg // * @param msg
// * @param userId // * @param userId
...@@ -310,22 +310,22 @@ public class MangerChannelServiceImpl implements MangerChannelService { ...@@ -310,22 +310,22 @@ public class MangerChannelServiceImpl implements MangerChannelService {
NioSocketChannel nioSocketChannel = get(toAppKey, toClientId); NioSocketChannel nioSocketChannel = get(toAppKey, toClientId);
if (null == nioSocketChannel) { if (null == nioSocketChannel) {
// userCache.offline(toAppKey + toClientId); // userCache.offline(toAppKey + toClientId);
if (log.isDebugEnabled()) { // if (log.isDebugEnabled()) {
log.info("writeData连接为空:" + toAppKey + toClientId + "," + msg); log.info("writeData连接为空:" + toAppKey + toClientId + "," + msg);
} // }
return false; return false;
} }
// 判断连接是否断开 // 判断连接是否断开
if (nioSocketChannel.isShutdown()) { if (nioSocketChannel.isShutdown()) {
if (log.isDebugEnabled()) { // if (log.isDebugEnabled()) {
log.info("writeData连接断开:" + toAppKey + toClientId + "," + msg + ",\nchannelId:" + nioSocketChannel.id().asLongText()); log.info("writeData连接断开:" + toAppKey + toClientId + "," + msg + ",\nchannelId:" + nioSocketChannel.id().asLongText());
} // }
return false; return false;
} }
if (log.isDebugEnabled()) { // if (log.isDebugEnabled()) {
log.info("writeData:" + toAppKey + "," + toClientId + "," + msg + ",\nchannelId:" + nioSocketChannel.id().asLongText()); log.info("writeData:" + toAppKey + "," + toClientId + "," + msg + ",\nchannelId:" + nioSocketChannel.id().asLongText());
} // }
ChannelFuture channelFuture = nioSocketChannel.writeAndFlush(new TextWebSocketFrame(msg)); ChannelFuture channelFuture = nioSocketChannel.writeAndFlush(new TextWebSocketFrame(msg));
channelFuture.addListener( channelFuture.addListener(
......
package com.wecloud.im.ws.service.impl; package com.wecloud.im.ws.service.impl;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.ws.model.ResponseModel; import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.WsConstants;
import com.wecloud.im.ws.model.request.ReceiveModel; import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.service.MangerChannelService; import com.wecloud.im.ws.service.MangerChannelService;
import com.wecloud.im.ws.service.WriteDataService; import com.wecloud.im.ws.service.WriteDataService;
...@@ -14,11 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired; ...@@ -14,11 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** /**
* @Description 下发数据 * @Description 下发数据
...@@ -28,17 +21,17 @@ import java.util.concurrent.TimeUnit; ...@@ -28,17 +21,17 @@ import java.util.concurrent.TimeUnit;
@Component @Component
public class WriteDataServiceImpl implements WriteDataService { public class WriteDataServiceImpl implements WriteDataService {
private final static ThreadFactory WRITE_NAMED_THREAD_FACTORY = new ThreadFactoryBuilder() // private final static ThreadFactory WRITE_NAMED_THREAD_FACTORY = new ThreadFactoryBuilder()
.setNamePrefix("ws-WRITE-").build(); // .setNamePrefix("ws-WRITE-").build();
/** // /**
* 耗时核心业务处理线程池 // * 耗时核心业务处理线程池
* 属于io密集型业务 // * 属于io密集型业务
* io密集型任务配置尽可能多的线程数量 // * io密集型任务配置尽可能多的线程数量
*/ // */
private final static ExecutorService WRITE_TASK_THREAD_POOL_EXECUTOR = // private final static ExecutorService WRITE_TASK_THREAD_POOL_EXECUTOR =
new ThreadPoolExecutor(WsConstants.CPU_PROCESSORS * 2, WsConstants.CPU_PROCESSORS * 3, // new ThreadPoolExecutor(WsConstants.CPU_PROCESSORS * 2, WsConstants.CPU_PROCESSORS * 3,
1L, TimeUnit.MILLISECONDS, // 1L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), WRITE_NAMED_THREAD_FACTORY, new ThreadPoolExecutor.CallerRunsPolicy()); // new LinkedBlockingQueue<Runnable>(), WRITE_NAMED_THREAD_FACTORY, new ThreadPoolExecutor.CallerRunsPolicy());
@Autowired @Autowired
private MangerChannelService mangerChannelService; private MangerChannelService mangerChannelService;
...@@ -62,7 +55,7 @@ public class WriteDataServiceImpl implements WriteDataService { ...@@ -62,7 +55,7 @@ public class WriteDataServiceImpl implements WriteDataService {
@Override @Override
public void dataAndStatus(ReceiveModel receiveModel, ApiCode apiCode, Object data, String toAppKey, String toClientId) { public void dataAndStatus(ReceiveModel receiveModel, ApiCode apiCode, Object data, String toAppKey, String toClientId) {
ApiResult<Boolean> apiResult = ApiResult.result(apiCode); ApiResult<Boolean> apiResult = ApiResult.result(apiCode);
ResponseModel responseModel = new ResponseModel(); WsResponseModel responseModel = new WsResponseModel();
responseModel.setMsg(apiResult.getMessage()); responseModel.setMsg(apiResult.getMessage());
responseModel.setCmd(receiveModel.getCmd()); responseModel.setCmd(receiveModel.getCmd());
responseModel.setReqId(receiveModel.getReqId()); responseModel.setReqId(receiveModel.getReqId());
...@@ -72,25 +65,25 @@ public class WriteDataServiceImpl implements WriteDataService { ...@@ -72,25 +65,25 @@ public class WriteDataServiceImpl implements WriteDataService {
} }
@Override @Override
public void write(ResponseModel responseModel, String toAppKey, String toClientId) { public void write(WsResponseModel responseModel, String toAppKey, String toClientId) {
WRITE_TASK_THREAD_POOL_EXECUTOR.execute( // WRITE_TASK_THREAD_POOL_EXECUTOR.execute(
() -> { // () -> {
JsonMapper jsonMapper = new JsonMapper(); JsonMapper jsonMapper = new JsonMapper();
String json = null; String json = null;
try { try {
json = jsonMapper.writeValueAsString(responseModel); json = jsonMapper.writeValueAsString(responseModel);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
e.printStackTrace(); e.printStackTrace();
} }
mangerChannelService.writeData(json, toAppKey, toClientId); mangerChannelService.writeData(json, toAppKey, toClientId);
} }
); // );
} //}
} }
...@@ -6,12 +6,12 @@ import com.wecloud.im.ws.model.request.ReceiveModel; ...@@ -6,12 +6,12 @@ import com.wecloud.im.ws.model.request.ReceiveModel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
/** /**
* @Description 接收netty不同类型请求 * @Description 处理Cmd请求
* 抽象类 策略设计模式 * 抽象类 策略设计模式
* @Author hewei hwei1233@163.com * @Author hewei hwei1233@163.com
* @Date 2020-01-02 * @Date 2020-01-02
*/ */
public abstract class AbstractReceiveStrategy { public abstract class ImCmdAbstract {
/** /**
* 处理业务流程 * 处理业务流程
......
...@@ -11,15 +11,15 @@ import java.util.Map; ...@@ -11,15 +11,15 @@ import java.util.Map;
* 维护指令码与策略实现的对应 * 维护指令码与策略实现的对应
* @Author hewei hwei1233@163.com * @Author hewei hwei1233@163.com
*/ */
public class ReceiveStrategyContext { public class ImCmdContext {
private final Map<WsRequestCmdEnum, Class> strategyMap; private final Map<WsRequestCmdEnum, Class> strategyMap;
public ReceiveStrategyContext(Map<WsRequestCmdEnum, Class> strategyMap) { public ImCmdContext(Map<WsRequestCmdEnum, Class> strategyMap) {
this.strategyMap = strategyMap; this.strategyMap = strategyMap;
} }
public AbstractReceiveStrategy getStrategy(WsRequestCmdEnum wsRequestPathEnum) { public ImCmdAbstract getStrategy(WsRequestCmdEnum wsRequestPathEnum) {
if (wsRequestPathEnum == null) { if (wsRequestPathEnum == null) {
throw new IllegalArgumentException("not fond enum"); throw new IllegalArgumentException("not fond enum");
...@@ -35,6 +35,6 @@ public class ReceiveStrategyContext { ...@@ -35,6 +35,6 @@ public class ReceiveStrategyContext {
throw new IllegalArgumentException("not fond strategy for type:" + wsRequestPathEnum.getCmdCode()); throw new IllegalArgumentException("not fond strategy for type:" + wsRequestPathEnum.getCmdCode());
} }
return (AbstractReceiveStrategy) SpringBeanUtils.getBean(aClass); return (ImCmdAbstract) SpringBeanUtils.getBean(aClass);
} }
} }
package com.wecloud.im.ws.strategy; package com.wecloud.im.ws.strategy;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.wecloud.im.ws.annotation.ReceiveTypeAnnotation; import com.wecloud.im.ws.annotation.CmdTypeAnnotation;
import com.wecloud.im.ws.enums.WsRequestCmdEnum; import com.wecloud.im.ws.enums.WsRequestCmdEnum;
import com.wecloud.im.ws.utils.ClassScanner; import com.wecloud.im.ws.utils.ClassScanner;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
...@@ -22,7 +22,7 @@ import java.util.Set; ...@@ -22,7 +22,7 @@ import java.util.Set;
* @Date 2020-01-02 * @Date 2020-01-02
*/ */
@Component @Component
public class ReceiveStrategyProcessor implements BeanFactoryPostProcessor { public class ImCmdStrategyProcessor implements BeanFactoryPostProcessor {
// 扫码注解的包路径 // 扫码注解的包路径
private static final String STRATEGY_PACK = "com.wecloud.im.ws.strategy.concrete"; private static final String STRATEGY_PACK = "com.wecloud.im.ws.strategy.concrete";
...@@ -32,20 +32,20 @@ public class ReceiveStrategyProcessor implements BeanFactoryPostProcessor { ...@@ -32,20 +32,20 @@ public class ReceiveStrategyProcessor implements BeanFactoryPostProcessor {
Map<WsRequestCmdEnum, Class> handlerMap = Maps.newHashMapWithExpectedSize(5); Map<WsRequestCmdEnum, Class> handlerMap = Maps.newHashMapWithExpectedSize(5);
// 扫码ReceiveTypeAnnotation注解的类 // 扫码ReceiveTypeAnnotation注解的类
Set<Class<?>> classSet = ClassScanner.scan(STRATEGY_PACK, ReceiveTypeAnnotation.class); Set<Class<?>> classSet = ClassScanner.scan(STRATEGY_PACK, CmdTypeAnnotation.class);
classSet.forEach(clazz -> { classSet.forEach(clazz -> {
// 获取注解中的类型值,与枚举类一一对应 // 获取注解中的类型值,与枚举类一一对应
WsRequestCmdEnum type = clazz.getAnnotation(ReceiveTypeAnnotation.class).type(); WsRequestCmdEnum type = clazz.getAnnotation(CmdTypeAnnotation.class).type();
handlerMap.put(type, clazz); handlerMap.put(type, clazz);
}); });
// 初始化Contenxt, 将其注册到spring容器当中 // 初始化Contenxt, 将其注册到spring容器当中
ReceiveStrategyContext context = new ReceiveStrategyContext(handlerMap); ImCmdContext context = new ImCmdContext(handlerMap);
try { try {
configurableListableBeanFactory.registerResolvableDependency(Class.forName(ReceiveStrategyContext.class.getName()), context); configurableListableBeanFactory.registerResolvableDependency(Class.forName(ImCmdContext.class.getName()), context);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
} }
......
...@@ -15,13 +15,14 @@ import com.wecloud.im.service.ImClientService; ...@@ -15,13 +15,14 @@ import com.wecloud.im.service.ImClientService;
import com.wecloud.im.service.ImConversationMembersService; import com.wecloud.im.service.ImConversationMembersService;
import com.wecloud.im.service.ImInboxService; import com.wecloud.im.service.ImInboxService;
import com.wecloud.im.service.ImMessageService; import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.ws.annotation.ReceiveTypeAnnotation; import com.wecloud.im.ws.annotation.CmdTypeAnnotation;
import com.wecloud.im.ws.enums.WsRequestCmdEnum; import com.wecloud.im.ws.enums.WsRequestCmdEnum;
import com.wecloud.im.ws.model.ResponseModel; import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.request.ReceiveModel; import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.sender.PushTask; import com.wecloud.im.ws.sender.PushTask;
import com.wecloud.im.ws.service.WriteDataService; import com.wecloud.im.ws.service.WriteDataService;
import com.wecloud.im.ws.strategy.AbstractReceiveStrategy; import com.wecloud.im.ws.strategy.ImCmdAbstract;
import io.geekidea.springbootplus.framework.common.api.ApiCode; import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult; import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.shiro.util.SnowflakeUtil; import io.geekidea.springbootplus.framework.shiro.util.SnowflakeUtil;
...@@ -38,10 +39,10 @@ import java.util.List; ...@@ -38,10 +39,10 @@ import java.util.List;
/** /**
* @Description 处理app数据消息 * @Description 处理app数据消息
*/ */
@ReceiveTypeAnnotation(type = WsRequestCmdEnum.DATA) @CmdTypeAnnotation(type = WsRequestCmdEnum.DATA)
@Service @Service
@Slf4j @Slf4j
public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { public class ImChatConcrete extends ImCmdAbstract {
private static final String TO_CONVERSATION_KEY = "toConversation"; private static final String TO_CONVERSATION_KEY = "toConversation";
public static final String PUSH_KEY = "push"; public static final String PUSH_KEY = "push";
...@@ -71,12 +72,11 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -71,12 +72,11 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
private ImClientService imClientService; private ImClientService imClientService;
@Autowired @Autowired
private PushTask pushTask; private PushTask systemPush;
@Override @Override
public void process(ReceiveModel receiveModel, ChannelHandlerContext ctx, String data, String appKey, String clientId) throws JsonProcessingException { public void process(ReceiveModel receiveModel, ChannelHandlerContext ctx, String data, String appKey, String clientId) throws JsonProcessingException {
// String language = ctx.channel().attr(MangerChannelService.LANGUAGE).get();
// 查询imApplication // 查询imApplication
ImApplication imApplication = imApplicationService.getOneByAppKey(appKey); ImApplication imApplication = imApplicationService.getOneByAppKey(appKey);
...@@ -137,7 +137,7 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -137,7 +137,7 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
// 生成消息id // 生成消息id
long messageId = SnowflakeUtil.getId(); long messageId = SnowflakeUtil.getId();
// 保存消息至消息表 // 保存消息至消息表
ImMessage imMessage = saveImMessage(imApplication, imClientSender, toConversationId, messageId, content); ImMessage imMessage = imMessageService.saveImMessage(imApplication, imClientSender, toConversationId, messageId, content);
// 封装响应的实体 // 封装响应的实体
ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend(); ImMessageOnlineSend imMessageOnlineSend = new ImMessageOnlineSend();
...@@ -171,8 +171,8 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -171,8 +171,8 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
} }
// 向接收方推送 // 向接收方推送
ResponseModel<ImMessageOnlineSend> responseModel = new ResponseModel<>(); WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(ResponseModel.ONLINE_MSG); responseModel.setCmd(WsResponseCmdEnum.ONLINE_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS); ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode()); responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage()); responseModel.setMsg(result.getMessage());
...@@ -180,14 +180,15 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -180,14 +180,15 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
responseModel.setReqId(null); responseModel.setReqId(null);
writeDataService.write(responseModel, appKey, imClientReceiver.getClientId()); writeDataService.write(responseModel, appKey, imClientReceiver.getClientId());
// , PushType.ALERT
// 异步推送系统通知消息 // 异步推送系统通知消息
pushTask.push(pushMap, imClientReceiver, imApplication); systemPush.push(pushMap, imClientReceiver, imApplication);
} }
// 响应发送方消息id等信息 // 响应发送方消息id等信息
ResponseModel<HashMap<String, Long>> responseModel = new ResponseModel<>(); WsResponseModel<HashMap<String, Long>> responseModel = new WsResponseModel<>();
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS); ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCmd(ResponseModel.RES); responseModel.setCmd(WsResponseCmdEnum.RES.getCmdCode());
responseModel.setCode(result.getCode()); responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage()); responseModel.setMsg(result.getMessage());
HashMap<String, Long> stringHashMap = new HashMap<>(3); HashMap<String, Long> stringHashMap = new HashMap<>(3);
...@@ -209,21 +210,6 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -209,21 +210,6 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
return imClientSender; return imClientSender;
} }
private ImMessage saveImMessage(ImApplication imApplication, ImClient imClientSender, Long toConversationId, long messageId, String content) {
ImMessage imMessage = new ImMessage();
imMessage.setId(messageId);
imMessage.setCreateTime(new Date());
imMessage.setFkAppid(imApplication.getId());
imMessage.setSender(imClientSender.getId());
imMessage.setContent(content);
imMessage.setWithdraw(false);
imMessage.setEvent(false);
imMessage.setSystem(false);
imMessage.setSendStatus(2);
imMessage.setFkConversationId(toConversationId);
imMessageService.save(imMessage);
return imMessage;
}
private boolean black(ReceiveModel receiveModel, String appKey, String clientUniId, ImClient imClientSender, List<ImConversationMembers> membersList) { private boolean black(ReceiveModel receiveModel, String appKey, String clientUniId, ImClient imClientSender, List<ImConversationMembers> membersList) {
// 判断是否被拉黑 // 判断是否被拉黑
...@@ -232,9 +218,9 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -232,9 +218,9 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
log.info("被对方拉黑了"); log.info("被对方拉黑了");
// 响应发送方 // 响应发送方
ResponseModel<HashMap<String, Long>> responseModel = new ResponseModel<>(); WsResponseModel<HashMap<String, Long>> responseModel = new WsResponseModel<>();
ApiResult<Boolean> result = ApiResult.result(ApiCode.IS_BE_BLACK); ApiResult<Boolean> result = ApiResult.result(ApiCode.IS_BE_BLACK);
responseModel.setCmd(ResponseModel.RES); responseModel.setCmd(WsResponseCmdEnum.RES.getCmdCode());
responseModel.setCode(result.getCode()); responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage()); responseModel.setMsg(result.getMessage());
responseModel.setReqId(receiveModel.getReqId()); responseModel.setReqId(receiveModel.getReqId());
...@@ -247,9 +233,9 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy { ...@@ -247,9 +233,9 @@ public class ImConcreteReceiveStrategy extends AbstractReceiveStrategy {
if (black) { if (black) {
log.info("你把对方拉黑了"); log.info("你把对方拉黑了");
// 响应发送方 // 响应发送方
ResponseModel<HashMap<String, Long>> responseModel = new ResponseModel<>(); WsResponseModel<HashMap<String, Long>> responseModel = new WsResponseModel<>();
ApiResult<Boolean> result = ApiResult.result(ApiCode.IS_TO_BLACK); ApiResult<Boolean> result = ApiResult.result(ApiCode.IS_TO_BLACK);
responseModel.setCmd(ResponseModel.RES); responseModel.setCmd(WsResponseCmdEnum.RES.getCmdCode());
responseModel.setCode(result.getCode()); responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage()); responseModel.setMsg(result.getMessage());
responseModel.setReqId(receiveModel.getReqId()); responseModel.setReqId(receiveModel.getReqId());
......
package com.wecloud.im.ws.strategy.concrete;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientBlacklistService;
import com.wecloud.im.service.ImClientService;
import com.wecloud.im.service.ImConversationMembersService;
import com.wecloud.im.service.ImInboxService;
import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.ws.annotation.CmdTypeAnnotation;
import com.wecloud.im.ws.enums.WsRequestCmdEnum;
import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.sender.PushTask;
import com.wecloud.im.ws.service.WriteDataService;
import com.wecloud.im.ws.strategy.ImCmdAbstract;
import com.wecloud.rtc.entity.RtcSubCmd;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 处理RTC信令消息
*/
@CmdTypeAnnotation(type = WsRequestCmdEnum.SINGLE_RTC)
@Service
@Slf4j
public class SingleRtcConcrete extends ImCmdAbstract {
private static final JsonMapper JSON_MAPPER = new JsonMapper();
@Autowired
private ImClientBlacklistService imClientBlacklistService;
@Autowired
private WriteDataService writeDataService;
@Autowired
private ImMessageService imMessageService;
@Autowired
private ImInboxService imInboxService;
@Autowired
private ImApplicationService imApplicationService;
@Autowired
private ImConversationMembersService imConversationMembersService;
@Autowired
private ImClientService imClientService;
@Autowired
private PushTask systemPush;
@Override
public void process(ReceiveModel receiveModel, ChannelHandlerContext ctx, String data, String appKey, String clientId) throws JsonProcessingException {
// 指令判空
if (receiveModel.getData().get(RtcSubCmd.SUB_CMD) == null) {
return;
}
String cmd = receiveModel.getData().get(RtcSubCmd.SUB_CMD).toString();
switch (cmd) {
// //创建频道
// case RtcSubCmd.CREATE:
// break;
//
// //加入频道
// case RtcSubCmd.JOIN:
// break;
//
// //拒绝加入频道
// case RtcSubCmd.REJECT:
// break;
//
// //SDP数据转发
// case RtcSubCmd.SDP:
// break;
//
// //主动挂断(离开频道)
// case RtcSubCmd.LEAVE:
// break;
}
}
}
...@@ -22,6 +22,52 @@ public class RedisUtils { ...@@ -22,6 +22,52 @@ public class RedisUtils {
@Autowired @Autowired
private StringRedisTemplate redisTemplate; private StringRedisTemplate redisTemplate;
public StringRedisTemplate redisTemplate() {
return redisTemplate;
}
/**
* 添加Key:value
*
* @param key
* @param value
*/
public void setKey(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 删除Key
*
* @param key 如果传入hash类型的key,则把整个hash中所有field删除
*/
public boolean delKey(String key) {
return redisTemplate.delete(key);
}
/**
* 获取Key
*
* @param key
*/
public String getKey(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 模糊查询
*
* @param key
* @return
*/
public Set<String> keys(String key) {
return redisTemplate.keys(key);
}
/** /**
* 获取hash中field对应的值 * 获取hash中field对应的值
* *
...@@ -29,7 +75,7 @@ public class RedisUtils { ...@@ -29,7 +75,7 @@ public class RedisUtils {
* @param field * @param field
* @return * @return
*/ */
public String hget(String key, String field) { public String hashget(String key, String field) {
Object val = redisTemplate.opsForHash().get(key, field); Object val = redisTemplate.opsForHash().get(key, field);
return val == null ? null : val.toString(); return val == null ? null : val.toString();
} }
...@@ -41,7 +87,7 @@ public class RedisUtils { ...@@ -41,7 +87,7 @@ public class RedisUtils {
* @param field * @param field
* @param value * @param value
*/ */
public void hset(String key, String field, String value) { public void hashset(String key, String field, String value) {
redisTemplate.opsForHash().put(key, field, value); redisTemplate.opsForHash().put(key, field, value);
} }
...@@ -52,16 +98,17 @@ public class RedisUtils { ...@@ -52,16 +98,17 @@ public class RedisUtils {
* @param key * @param key
* @param field * @param field
*/ */
public void hdel(String key, String field) { public void hashdel(String key, String field) {
redisTemplate.opsForHash().delete(key, field); redisTemplate.opsForHash().delete(key, field);
} }
/** /**
* 删除key * 删除key
* *
* @param key 如果传入hash类型的key,则把整个hash中所有field删除 * @param key 如果传入hash类型的key,则把整个hash中所有field删除
*/ */
public void kdel(String key) { public void keydel(String key) {
redisTemplate.delete(key); redisTemplate.delete(key);
} }
...@@ -71,7 +118,7 @@ public class RedisUtils { ...@@ -71,7 +118,7 @@ public class RedisUtils {
* @param key * @param key
* @return * @return
*/ */
public Map<String, String> hgetll(String key) { public Map<String, String> hashgetll(String key) {
return redisTemplate.execute((RedisCallback<Map<String, String>>) con -> { return redisTemplate.execute((RedisCallback<Map<String, String>>) con -> {
Map<byte[], byte[]> result = con.hGetAll(key.getBytes()); Map<byte[], byte[]> result = con.hGetAll(key.getBytes());
if (CollectionUtils.isEmpty(result)) { if (CollectionUtils.isEmpty(result)) {
...@@ -93,7 +140,7 @@ public class RedisUtils { ...@@ -93,7 +140,7 @@ public class RedisUtils {
* @param fields * @param fields
* @return * @return
*/ */
public Map<String, String> hmget(String key, List<String> fields) { public Map<String, String> hashmget(String key, List<String> fields) {
List<String> result = redisTemplate.<String, String>opsForHash().multiGet(key, fields); List<String> result = redisTemplate.<String, String>opsForHash().multiGet(key, fields);
Map<String, String> ans = new HashMap<>(fields.size()); Map<String, String> ans = new HashMap<>(fields.size());
int index = 0; int index = 0;
...@@ -126,7 +173,7 @@ public class RedisUtils { ...@@ -126,7 +173,7 @@ public class RedisUtils {
} }
/** /**
* 获取指定key中存放set<String>的集合 * 获取指定key中存放set<String>的集合
* *
* @param key * @param key
*/ */
...@@ -134,12 +181,5 @@ public class RedisUtils { ...@@ -134,12 +181,5 @@ public class RedisUtils {
return redisTemplate.opsForSet().members(key); return redisTemplate.opsForSet().members(key);
} }
/**
* 删除指定key缓存
*
* @param key
*/
public void deleteByKey(String key) {
redisTemplate.delete(key);
}
} }
package com.wecloud.rtc.entity;
import java.io.Serializable;
public class RtcSubCmd implements Serializable {
/**
* subCmd子类型指令码
*/
public static final String SUB_CMD = "subCmd";
}
package com.wecloud.rtc.entity.redis;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* rtc 频道信息
*/
@Data
public class RtcChannelInfo implements Serializable {
@ApiModelProperty("当前房主")
private String owner;
private String appKey;
// private String appId;
@ApiModelProperty("创建时间")
private Long createTimestamp;
}
package com.wecloud.rtc.entity.redis;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* rtc 频道内的用户
*/
@Data
public class RtcJoinUser implements Serializable {
@ApiModelProperty("客户端")
private String clientId;
@ApiModelProperty("加入时间")
private Long createTimestamp;
private String sdpData;
private String sdpType;
}
package com.wecloud.rtc.entity.redis;
import lombok.Data;
import java.io.Serializable;
/**
* rtc 频道内所有用户
*/
@Data
public class RtcJoinUsers implements Serializable {
}
package com.wecloud.rtc.entity.redis;
import java.io.Serializable;
public class RtcRedisKey implements Serializable {
/**
* 维护频道信息 (kv)
*/
public static final String RTC_CHANNEL_INFO = "r:ci:%s";
/**
* 维护用户当前在线的频道ID ( kv)
* user_join_channel = ujc
* rcu:clientA = 10001
* rcu:clientB = 10001
* rcu:clientC = 10002
* rcu:clientD = 10003
*/
public static final String USER_JOIN_CHANNEL = "r:ujc:%s";
/**
* 维护频道中存在的用户 (set 集合):
* rtc_channel_users = rcu
* rcu:10001 = clientA , clientB
* rcu:10002 = clientC
* rcu:10003 = clientD
*/
public static final String RTC_CHANNEL_USERS = "r:cu:%s";
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcCallResponse extends RtcSubDataBase implements Serializable {
private String type;
private Long conversationId;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcCandidateForwardResponse extends RtcSubDataBase implements Serializable {
/**
* 转发的候选者数据
*/
private String candidateData;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcClientJoinResponse extends RtcSubDataBase implements Serializable {
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcClientLeaveResponse extends RtcSubDataBase implements Serializable {
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcClientRejectResponse extends RtcSubDataBase implements Serializable {
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import java.io.Serializable;
/**
* webRtc websocket下发数据封装类
*
* @param <T>
*/
@Data
public class RtcResponseBase<T> implements Serializable {
/**
* 子指令
*/
private Integer subCmd;
/**
* 根据不同子指令 不同的实体
*/
private T subData;
/**
* 自定义拓展字段
*/
private String attrs;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcSdpForwardResponse extends RtcSubDataBase implements Serializable {
/**
* channelId : 1234263457652
* clientId : 7657567
* sdpData : xxxxxxxxxxxxxxxx
* sdpType : Offer/Answer
*/
private String sdpData;
private String sdpType;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import java.io.Serializable;
@Data
public class RtcSubDataBase implements Serializable {
private Long channelId;
private String clientId;
private Long timestamp;
}
package com.wecloud.rtc.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.rtc.entity.redis.RtcChannelInfo;
import java.util.List;
/**
* 管理rtc频道
*/
public interface MangerRtcCacheService {
/**
* 移除频道信息
*
* @param rtcChannelId
* @return
*/
boolean delChannelInfo(Long rtcChannelId);
/**
* 频道中客户端是否为空
*
* @param rtcChannelId
* @return
*/
boolean channelIsEmpty(Long rtcChannelId);
/**
* 获取频道信息
*
* @param rtcChannelId
* @return
* @throws JsonProcessingException
*/
RtcChannelInfo getRtcChannelInfo(Long rtcChannelId) throws JsonProcessingException;
/**
* 创建一个频道
*
* @param appKey
* @param clientId
* @param rtcChannelId 雪花算法生成频道id
*/
void create(String appKey, String clientId, Long rtcChannelId) throws JsonProcessingException;
/**
* 加入频道
*/
void join(String appKey, String clientId, Long rtcChannelId);
/**
* 退出频道
*/
void leave(String appKey, String clientId, Long rtcChannelId);
/**
* 根据频道ID获取频道内所有client
*/
List<String> getClientListByRtcChannelId(Long rtcChannelId);
/**
* 根据客户端ID获取该客户端加入的频道ID
*/
Long getRtcChannelIdListByClientId(String appKey, String clientId);
/**
* 获取客户端忙线/空闲状态
*
* @param appKey
* @param clientId
* @return true:忙线,false空闲
*/
boolean getBusyStatus(String appKey, String clientId);
}
package com.wecloud.rtc.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.im.param.rtc.CandidateForwardParam;
import com.wecloud.im.param.rtc.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CreateRtcChannelResult;
import com.wecloud.im.param.rtc.JoinRtcChannelParam;
import com.wecloud.im.param.rtc.LeaveRtcChannelParam;
import com.wecloud.im.param.rtc.RejectRtcChannelParam;
import com.wecloud.im.param.rtc.SdpForwardParam;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
/**
* 管理rtc频道
*/
public interface RtcService {
/**
* 客户端离线
*/
void clientOffline(String appKey, String clientId);
/**
* 创建一个频道,并向接收方发送系统推送
*/
ApiResult<CreateRtcChannelResult> createAndCall(CreateRtcChannelParam createRtcChannelParam) throws JsonProcessingException;
/**
* 加入频道
*/
ApiResult<Boolean> join(JoinRtcChannelParam joinRtcChannelParam);
/**
* 拒接加入频道
*/
ApiResult<Boolean> reject(RejectRtcChannelParam rejectRtcChannelParam);
/**
* 退出频道
*/
ApiResult<Boolean> leave(LeaveRtcChannelParam leaveRtcChannelParam);
/**
* SDP数据转发
*
* @param sdpForwardParam
* @return
*/
ApiResult<Boolean> sdpForward(SdpForwardParam sdpForwardParam);
/**
* candidate候选者数据转发
*
* @param candidateForwardParam
* @return
*/
ApiResult<Boolean> candidateForward(CandidateForwardParam candidateForwardParam);
}
package com.wecloud.rtc.service;
import com.wecloud.rtc.entity.response.RtcCallResponse;
import com.wecloud.rtc.entity.response.RtcCandidateForwardResponse;
import com.wecloud.rtc.entity.response.RtcClientJoinResponse;
import com.wecloud.rtc.entity.response.RtcClientLeaveResponse;
import com.wecloud.rtc.entity.response.RtcClientRejectResponse;
import com.wecloud.rtc.entity.response.RtcSdpForwardResponse;
/**
* WebRtc webSocket下发指令数据
*/
public interface WsRtcWrite {
/**
* 接收到RTC邀请
*/
void rtcCall(RtcCallResponse rtcCallResponse, String toAppKey, String toClientId);
/**
* 用户状态更新事件(用户加入频道)
*/
void clientJoin(RtcClientJoinResponse rtcClientJoinResponse, String toAppKey, String toClientId);
/**
* 用户状态更新事件(用户退出频道)
*/
void clientLeave(RtcClientLeaveResponse rtcClientLeaveResponse, String toAppKey, String toClientId);
/**
* 用户状态更新事件(用户拒接邀请;不同意进入频道)
*/
void clientReject(RtcClientRejectResponse rtcClientRejectResponse, String toAppKey, String toClientId);
/**
* SDP数据转发
*/
void sdpForward(RtcSdpForwardResponse rtcSdpForwardResponse, String toAppKey, String toClientId);
/**
* candidate候选者数据转发
*/
void candidateForward(RtcCandidateForwardResponse rtcCandidateForwardResponse, String toAppKey, String toClientId);
}
package com.wecloud.rtc.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.ws.utils.RedisUtils;
import com.wecloud.rtc.entity.redis.RtcChannelInfo;
import com.wecloud.rtc.entity.redis.RtcRedisKey;
import com.wecloud.rtc.service.MangerRtcCacheService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
@Service
public class MangerRtcCacheServiceImpl implements MangerRtcCacheService {
@Autowired
private RedisUtils redisUtils;
@Override
public boolean delChannelInfo(Long rtcChannelId) {
String channelKey = String.format(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelId);
redisUtils.delKey(channelKey);
return true;
}
@Override
public boolean channelIsEmpty(Long rtcChannelId) {
List<String> clientListByRtcChannelId = getClientListByRtcChannelId(rtcChannelId);
// // 移除自己
// clientListByRtcChannelId.remove(appKey + clientId);
return clientListByRtcChannelId.isEmpty();
}
@Override
public RtcChannelInfo getRtcChannelInfo(Long rtcChannelId) throws JsonProcessingException {
// 频道信息
String channelKey = String.format(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelId);
String value = redisUtils.getKey(channelKey);
if (StringUtils.isBlank(value)) {
return null;
}
JsonMapper jsonMapper = new JsonMapper();
return jsonMapper.readValue(value, RtcChannelInfo.class);
}
@Override
public void create(String appKey, String clientId, Long rtcChannelId) throws JsonProcessingException {
// --- 频道信息
RtcChannelInfo rtcChannelInfo = new RtcChannelInfo();
rtcChannelInfo.setAppKey(appKey);
// rtcChannelInfo.setAppId("");
//当前房主
rtcChannelInfo.setOwner(clientId);
//创建时间
rtcChannelInfo.setCreateTimestamp(new Date().getTime());
String rtcChannelInfoJson = new JsonMapper().writeValueAsString(rtcChannelInfo);
// --- 保存频道信息
String channelKey = String.format(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelId);
redisUtils.setKey(channelKey, rtcChannelInfoJson);
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
redisUtils.setKey(userJoinChannelKey, rtcChannelId.toString());
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
redisUtils.addForSet(rtcChannelUsers, clientId);
}
@Override
public void join(String appKey, String clientId, Long rtcChannelId) {
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
redisUtils.setKey(userJoinChannelKey, rtcChannelId.toString());
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
redisUtils.addForSet(rtcChannelUsers, clientId);
}
@Override
public void leave(String appKey, String clientId, Long rtcChannelId) {
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
redisUtils.delKey(userJoinChannelKey);
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
redisUtils.removeForSet(rtcChannelUsers, clientId);
}
@Override
public List<String> getClientListByRtcChannelId(Long rtcChannelId) {
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
Set<String> forSetMembers = redisUtils.getForSetMembers(rtcChannelUsers);
return new ArrayList<>(forSetMembers);
}
@Override
public Long getRtcChannelIdListByClientId(String appKey, String clientId) {
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
String key = redisUtils.getKey(userJoinChannelKey);
if (StringUtils.isBlank(key)) {
return null;
}
return Long.valueOf(key);
}
@Override
public boolean getBusyStatus(String appKey, String clientId) {
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
String key = redisUtils.getKey(userJoinChannelKey);
return key != null && !key.isEmpty();
}
}
package com.wecloud.rtc.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient;
import com.wecloud.im.param.rtc.CandidateForwardParam;
import com.wecloud.im.param.rtc.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CreateRtcChannelResult;
import com.wecloud.im.param.rtc.JoinRtcChannelParam;
import com.wecloud.im.param.rtc.LeaveRtcChannelParam;
import com.wecloud.im.param.rtc.RejectRtcChannelParam;
import com.wecloud.im.param.rtc.SdpForwardParam;
import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService;
import com.wecloud.im.ws.service.MangerChannelService;
import com.wecloud.rtc.entity.response.RtcCallResponse;
import com.wecloud.rtc.entity.response.RtcCandidateForwardResponse;
import com.wecloud.rtc.entity.response.RtcClientJoinResponse;
import com.wecloud.rtc.entity.response.RtcClientLeaveResponse;
import com.wecloud.rtc.entity.response.RtcClientRejectResponse;
import com.wecloud.rtc.entity.response.RtcSdpForwardResponse;
import com.wecloud.rtc.service.MangerRtcCacheService;
import com.wecloud.rtc.service.RtcService;
import com.wecloud.rtc.service.WsRtcWrite;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.shiro.util.SnowflakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class RtcServiceImpl implements RtcService {
@Autowired
private ImApplicationService imApplicationService;
@Autowired
private WsRtcWrite wsRtcWrite;
/**
* redis缓存
*/
@Autowired
private MangerRtcCacheService mangerRtcCacheService;
@Autowired
private ImClientService imClientService;
@Autowired
private MangerChannelService mangerChannelService;
@Override
public void clientOffline(String appKey, String clientId) {
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getOneByAppKey(appKey);
ImClient client = imClientService.getCacheImClient(imApplication.getId(), clientId);
// 获取该客户端加入的频道ID
Long listByClientId = mangerRtcCacheService.getRtcChannelIdListByClientId(imApplication.getAppKey(), client.getClientId());
if (listByClientId == null) {
return;
}
LeaveRtcChannelParam leaveRtcChannelParam = new LeaveRtcChannelParam();
leaveRtcChannelParam.setChannelId(listByClientId);
// websocket离线逻辑 服务端踢出频道
this.leave(leaveRtcChannelParam, client, imApplication);
}
@Override
public ApiResult<CreateRtcChannelResult> createAndCall(CreateRtcChannelParam createRtcChannelParam) throws JsonProcessingException {
ImClient client = imClientService.getCurentClient();
Long rtcChannelId = SnowflakeUtil.getId();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 判断发起方必须在线
boolean onlineStatus = mangerChannelService.getOnlineStatus(imApplication.getAppKey(), client.getClientId());
if (!onlineStatus) {
log.info("发起方必须在线" + imApplication.getAppKey() + client.getClientId());
ApiResult.fail();
}
// 添加缓存
mangerRtcCacheService.create(imApplication.getAppKey(), client.getClientId(), rtcChannelId);
CreateRtcChannelResult createRtcChannelResult = new CreateRtcChannelResult();
createRtcChannelResult.setChannelId(rtcChannelId);
// ws向接收方发送通知
RtcCallResponse rtcCallResponse = new RtcCallResponse();
rtcCallResponse.setType(createRtcChannelParam.getType());
// rtcCallResponse.setConversationId(createRtcChannelParam.getConversationId());
rtcCallResponse.setChannelId(rtcChannelId);
rtcCallResponse.setClientId(client.getClientId());
rtcCallResponse.setTimestamp(new Date().getTime());
wsRtcWrite.rtcCall(rtcCallResponse, imApplication.getAppKey(), createRtcChannelParam.getToClient());
// TODO 待开发 下发安卓和ios系统推送
return ApiResult.ok(createRtcChannelResult);
}
@Override
public ApiResult<Boolean> join(JoinRtcChannelParam joinRtcChannelParam) {
ImClient client = imClientService.getCurentClient();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 修改缓存
mangerRtcCacheService.join(imApplication.getAppKey(), client.getClientId(), joinRtcChannelParam.getChannelId());
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(joinRtcChannelParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcClientJoinResponse rtcSdpForwardResponse = new RtcClientJoinResponse();
rtcSdpForwardResponse.setChannelId(joinRtcChannelParam.getChannelId());
rtcSdpForwardResponse.setClientId(client.getClientId());
rtcSdpForwardResponse.setTimestamp(new Date().getTime());
wsRtcWrite.clientJoin(rtcSdpForwardResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
@Override
public ApiResult<Boolean> reject(RejectRtcChannelParam rejectRtcChannelParam) {
ImClient client = imClientService.getCurentClient();
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(rejectRtcChannelParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcClientRejectResponse rtcClientRejectResponse = new RtcClientRejectResponse();
rtcClientRejectResponse.setChannelId(rejectRtcChannelParam.getChannelId());
rtcClientRejectResponse.setClientId(client.getClientId());
rtcClientRejectResponse.setTimestamp(new Date().getTime());
wsRtcWrite.clientReject(rtcClientRejectResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
@Override
public ApiResult<Boolean> leave(LeaveRtcChannelParam leaveRtcChannelParam) {
ImClient client = imClientService.getCurentClient();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
this.leave(leaveRtcChannelParam, client, imApplication);
return ApiResult.ok(true);
}
private void leave(LeaveRtcChannelParam leaveRtcChannelParam, ImClient client, ImApplication imApplication) {
// 修改缓存
mangerRtcCacheService.leave(imApplication.getAppKey(), client.getClientId(), leaveRtcChannelParam.getChannelId());
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(leaveRtcChannelParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcClientLeaveResponse rtcClientLeaveResponse = new RtcClientLeaveResponse();
rtcClientLeaveResponse.setChannelId(leaveRtcChannelParam.getChannelId());
rtcClientLeaveResponse.setClientId(client.getClientId());
rtcClientLeaveResponse.setTimestamp(new Date().getTime());
wsRtcWrite.clientLeave(rtcClientLeaveResponse, imApplication.getAppKey(), toClientId);
}
// 判断频道内是否无其他人了
if (mangerRtcCacheService.channelIsEmpty(leaveRtcChannelParam.getChannelId())) {
// 移除频道信息
mangerRtcCacheService.delChannelInfo(leaveRtcChannelParam.getChannelId());
}
}
@Override
public ApiResult<Boolean> sdpForward(SdpForwardParam sdpForwardParam) {
ImClient client = imClientService.getCurentClient();
Long rtcChannelId = SnowflakeUtil.getId();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 判断发起方必须在线
boolean onlineStatus = mangerChannelService.getOnlineStatus(imApplication.getAppKey(), client.getClientId());
if (!onlineStatus) {
log.info("发起方必须在线" + imApplication.getAppKey() + client.getClientId());
ApiResult.fail();
}
CreateRtcChannelResult createRtcChannelResult = new CreateRtcChannelResult();
createRtcChannelResult.setChannelId(rtcChannelId);
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(sdpForwardParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcSdpForwardResponse rtcSdpForwardResponse = new RtcSdpForwardResponse();
rtcSdpForwardResponse.setSdpData(sdpForwardParam.getSdpData());
rtcSdpForwardResponse.setSdpType(sdpForwardParam.getSdpType());
rtcSdpForwardResponse.setChannelId(rtcChannelId);
rtcSdpForwardResponse.setClientId(client.getClientId());
rtcSdpForwardResponse.setTimestamp(new Date().getTime());
wsRtcWrite.sdpForward(rtcSdpForwardResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
@Override
public ApiResult<Boolean> candidateForward(CandidateForwardParam candidateForwardParam) {
ImClient client = imClientService.getCurentClient();
Long rtcChannelId = SnowflakeUtil.getId();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 判断发起方必须在线
boolean onlineStatus = mangerChannelService.getOnlineStatus(imApplication.getAppKey(), client.getClientId());
if (!onlineStatus) {
log.info("发起方必须在线" + imApplication.getAppKey() + client.getClientId());
ApiResult.fail();
}
CreateRtcChannelResult createRtcChannelResult = new CreateRtcChannelResult();
createRtcChannelResult.setChannelId(rtcChannelId);
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(candidateForwardParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcCandidateForwardResponse rtcCandidateForwardResponse = new RtcCandidateForwardResponse();
rtcCandidateForwardResponse.setCandidateData(candidateForwardParam.getCandidateData());
rtcCandidateForwardResponse.setChannelId(rtcChannelId);
rtcCandidateForwardResponse.setClientId(client.getClientId());
rtcCandidateForwardResponse.setTimestamp(new Date().getTime());
wsRtcWrite.candidateForward(rtcCandidateForwardResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
}
package com.wecloud.rtc.service.impl;
import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.enums.WsRtcResponseSubCmdEnum;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.service.WriteDataService;
import com.wecloud.rtc.entity.response.RtcCallResponse;
import com.wecloud.rtc.entity.response.RtcCandidateForwardResponse;
import com.wecloud.rtc.entity.response.RtcClientJoinResponse;
import com.wecloud.rtc.entity.response.RtcClientLeaveResponse;
import com.wecloud.rtc.entity.response.RtcClientRejectResponse;
import com.wecloud.rtc.entity.response.RtcResponseBase;
import com.wecloud.rtc.entity.response.RtcSdpForwardResponse;
import com.wecloud.rtc.service.WsRtcWrite;
import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class WsRtcWriteImpl implements WsRtcWrite {
// private static final JsonMapper JSON_MAPPER = new JsonMapper();
@Autowired
private WriteDataService writeDataService;
@Override
public void rtcCall(RtcCallResponse rtcCallResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcCallResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.RTC_CALL.getCmdCode());
rtcResponseBase.setSubData(rtcCallResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcCallResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void clientJoin(RtcClientJoinResponse rtcClientJoinResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcClientJoinResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CLIENT_JOIN.getCmdCode());
rtcResponseBase.setSubData(rtcClientJoinResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcClientJoinResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void clientLeave(RtcClientLeaveResponse rtcClientLeaveResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcClientLeaveResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CLIENT_LEAVE.getCmdCode());
rtcResponseBase.setSubData(rtcClientLeaveResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcClientLeaveResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void clientReject(RtcClientRejectResponse rtcClientRejectResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcClientRejectResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CLIENT_REJECT.getCmdCode());
rtcResponseBase.setSubData(rtcClientRejectResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcClientRejectResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void sdpForward(RtcSdpForwardResponse rtcSdpForwardResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcSdpForwardResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.SDP_FORWARD.getCmdCode());
rtcResponseBase.setSubData(rtcSdpForwardResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcSdpForwardResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void candidateForward(RtcCandidateForwardResponse rtcCandidateForwardResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcCandidateForwardResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CANDIDATE_FORWARD.getCmdCode());
rtcResponseBase.setSubData(rtcCandidateForwardResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcCandidateForwardResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
}
...@@ -26,5 +26,31 @@ ...@@ -26,5 +26,31 @@
<include refid="Base_Column_List"/> <include refid="Base_Column_List"/>
from im_client from im_client
</select> </select>
<select id="getInfoList" resultType="com.wecloud.im.vo.GetInfoListVo">
SELECT
im_client.client_id AS clientId,
im_client.head_portrait AS headPortrait,
im_client.nickname,
(SELECT im_conversation_members.client_remark_name FROM im_conversation_members AS im_conversation_members WHERE
im_conversation_members.fk_conversation_id = #{conversationId} AND im_conversation_members.fk_client_id =
clientId ) AS clientRemarkName ,
(SELECT im_conversation_members.attributes FROM im_conversation_members AS im_conversation_members WHERE
im_conversation_members.fk_conversation_id = #{conversationId} AND im_conversation_members.fk_client_id =
clientId ) AS memberAttributes ,
im_client.attributes AS clientAttributes
FROM
im_client AS im_client
WHERE
im_client.fk_appid = #{appId}
AND im_client.client_id IN
<foreach collection="clientIds" item="clientId" index="index" open="(" close=")" separator=",">
#{clientId}
</foreach>
</select>
</mapper> </mapper>
...@@ -92,14 +92,19 @@ ...@@ -92,14 +92,19 @@
<select id="getRepetitionConversationInfo" resultType="com.wecloud.im.entity.ImConversation"> <select id="getRepetitionConversationInfo" resultType="com.wecloud.im.entity.ImConversation">
SELECT im_conversation.*
SELECT im_conversation.*,
(SELECT COUNT(im2.id)
FROM im_conversation_members AS im2
WHERE im2.fk_conversation_id = im_conversation_members.fk_conversation_id) AS members_count
FROM im_conversation_members FROM im_conversation_members
INNER JOIN (SELECT * INNER JOIN (SELECT *
FROM im_conversation_members FROM im_conversation_members
WHERE im_conversation_members.fk_client_id = #{clientId2}) AS im_conversation_members2 WHERE im_conversation_members.fk_client_id = #{clientId2}) AS im_conversation_members2
ON im_conversation_members.fk_conversation_id = im_conversation_members2.fk_conversation_id ON im_conversation_members.fk_conversation_id = im_conversation_members2.fk_conversation_id
INNER JOIN im_conversation ON im_conversation.id = im_conversation_members.fk_conversation_id INNER JOIN im_conversation ON im_conversation.id = im_conversation_members.fk_conversation_id
WHERE im_conversation_members.fk_client_id = #{clientId1} LIMIT 1 WHERE im_conversation_members.fk_client_id = #{clientId1}
HAVING members_count = 2 LIMIT 1
</select> </select>
</mapper> </mapper>
...@@ -20,5 +20,27 @@ ...@@ -20,5 +20,27 @@
<include refid="Base_Column_List"/> <include refid="Base_Column_List"/>
from im_conversation_members from im_conversation_members
</select> </select>
<select id="getRestApiImConversationMembersList"
resultType="com.wecloud.im.param.ApiImConversationMembersQueryVo">
SELECT im_client.client_id AS clientId
FROM im_conversation_members AS imConversationMembers
INNER JOIN im_client AS im_client ON im_client.id = imConversationMembers.fk_client_id
WHERE fk_conversation_id = #{conversationId}
</select>
<select id="getImConversationMembersList" resultType="com.wecloud.im.vo.ImConversationMemberListVo">
SELECT im_client.client_id as clientId,
im_conversation_members.client_remark_name as clientRemarkName,
im_client.head_portrait as headPortrait,
im_client.nickname,
im_conversation_members.attributes AS memberAttributes,
im_client.attributes AS clientAttributes
FROM im_conversation_members AS im_conversation_members
INNER JOIN im_client AS im_client ON im_client.id = im_conversation_members.fk_client_id
WHERE im_conversation_members.fk_conversation_id = #{conversationId}
</select>
</mapper> </mapper>
...@@ -15,7 +15,7 @@ spring-boot-plus: ...@@ -15,7 +15,7 @@ spring-boot-plus:
spring: spring:
datasource: datasource:
url: jdbc:mysql://localhost:3306/wecloud_im_v1_3?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true url: jdbc:mysql://localhost:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: root username: root
password: 123 password: 123
......
...@@ -14,7 +14,6 @@ spring-boot-plus: ...@@ -14,7 +14,6 @@ spring-boot-plus:
request-log-format: false request-log-format: false
response-log-format: false response-log-format: false
spring: spring:
#飞蛙 #飞蛙
...@@ -32,6 +31,7 @@ spring: ...@@ -32,6 +31,7 @@ spring:
nacos: nacos:
discovery: discovery:
server-addr: localhost:8848 server-addr: localhost:8848
# knife4j配置 # knife4j配置
knife4j: knife4j:
enable: ${spring-boot-plus.swagger.enable} enable: ${spring-boot-plus.swagger.enable}
......
...@@ -14,6 +14,7 @@ spring-boot-plus: ...@@ -14,6 +14,7 @@ spring-boot-plus:
response-log-format: false response-log-format: false
# 海外 upay 飞蛙等 IM测试外网
spring: spring:
datasource: datasource:
url: jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true url: jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
...@@ -30,6 +31,25 @@ spring: ...@@ -30,6 +31,25 @@ spring:
nacos: nacos:
discovery: discovery:
server-addr: localhost:8848 server-addr: localhost:8848
# 国内IM测试外网
#spring:
# datasource:
# url: jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
# username: root
# password: temple123456
#
# # Redis配置
# redis:
# database: 0
# host: 127.0.0.1
# password: temple123456
# port: 6379
# cloud:
# nacos:
# discovery:
# server-addr: localhost:8848
# 打印SQL语句和结果集,本地开发环境可开启,线上注释掉 # 打印SQL语句和结果集,本地开发环境可开启,线上注释掉
mybatis-plus: mybatis-plus:
configuration: configuration:
......
...@@ -198,6 +198,9 @@ spring-boot-plus: ...@@ -198,6 +198,9 @@ spring-boot-plus:
# 应用相关 # 应用相关
- /imApplication/** - /imApplication/**
- /signDemo/get - /signDemo/get
- /Wecloud-IM-Websocket-Docs.html
# 服务端rest-API
- /server/**
# 多行字符串权限配置 # 多行字符串权限配置
filter-chain-definitions: | filter-chain-definitions: |
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -8,4 +8,4 @@ ps aux|grep bootstrap-2.0|awk '{print $2}'|xargs kill -9 ...@@ -8,4 +8,4 @@ ps aux|grep bootstrap-2.0|awk '{print $2}'|xargs kill -9
# 启动新jar, # 启动新jar,
nohup java -jar bootstrap-2.0.jar &>/dev/null & nohup java -jar bootstrap-2.0.jar &>/dev/null &
echo "success" echo "run success"
#! /bin/shell
# 国内IM集成版测试环境
# sudo 超级权限启动, 否则无法创建log日志文件夹报错
# 停止服务
ps aux|grep bootstrap-2.0|awk '{print $2}'|xargs kill -9
#/data0/java_projects_jenkins/
# 启动新jar,
#nohup java -jar bootstrap-2.0.jar &>/dev/null &
nohup java -jar \
-Dspring.datasource.url="jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true" \
-Dspring.datasource.username=root \
-Dspring.datasource.password=temple123456 \
-Dspring.redis.database=0 \
-Dspring.redis.host=127.0.0.1 \
-Dspring.redis.password=temple123456 \
-Dspring.redis.port=6379 \
bootstrap-2.0-test.jar &>/dev/null &
echo "run success"
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50730
Source Host : localhost:3306
Source Schema : wecloud_im
Target Server Type : MySQL
Target Server Version : 50730
File Encoding : 65001
Date: 03/12/2021 11:26:12
*/
SET NAMES utf8mb4;
SET
FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for im_application
-- ----------------------------
DROP TABLE IF EXISTS `im_application`;
CREATE TABLE `im_application`
(
`id` bigint(20) NOT NULL COMMENT '应用appid',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`app_key` varchar(200) CHARACTER SET latin1 DEFAULT NULL COMMENT 'key',
`app_secret` varchar(500) CHARACTER SET latin1 DEFAULT NULL COMMENT '密钥',
`app_name` varchar(200) DEFAULT NULL COMMENT 'app名称',
`ios_push_channel` int(255) DEFAULT NULL COMMENT 'ios推送通道,友盟:1;firebase:2; apns原生:3',
`android_push_channel` int(255) DEFAULT NULL COMMENT '安卓推送通道,友盟:1;firebase:2; 信鸽3',
`umeng_key` varchar(500) DEFAULT NULL COMMENT '友盟推送key',
`umeng_secret` varchar(500) DEFAULT NULL COMMENT '友盟推送密钥',
`firebase_secret` varchar(500) DEFAULT NULL COMMENT 'firebase推送密钥',
`repeat_session_status` int(1) DEFAULT '0' COMMENT '是否允许创建重复会话 0不允许 1允许',
`contrast_extended_field_status` int(1) DEFAULT '0' COMMENT '创建会话时对比扩展字段 0不 1是',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `app_key` (`app_key`) USING HASH
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方应用表';
-- ----------------------------
-- Table structure for im_client
-- ----------------------------
DROP TABLE IF EXISTS `im_client`;
CREATE TABLE `im_client`
(
`id` bigint(20) NOT NULL COMMENT '客户端id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`last_offline_time` timestamp NULL DEFAULT NULL COMMENT '最后离线时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`fk_appid` bigint(20) DEFAULT NULL COMMENT '应用appid',
`attributes` json DEFAULT NULL COMMENT '可选 自定义属性,供开发者扩展使用',
`client_id` varchar(200) DEFAULT NULL COMMENT '客户方提供的唯一id',
`valid` int(1) DEFAULT NULL COMMENT '设备不想收到推送提醒',
`device_type` int(1) DEFAULT NULL COMMENT '设备类型1:ios; 2:android',
`device_token` varchar(300) DEFAULT NULL COMMENT '设备推送token',
`head_portrait` varchar(1000) DEFAULT NULL COMMENT '头像',
`nickname` varchar(30) DEFAULT NULL COMMENT '主昵称',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY ```fk_appid``, ``client_id``` (`fk_appid`,`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='终端表';
-- ----------------------------
-- Table structure for im_client_blacklist
-- ----------------------------
DROP TABLE IF EXISTS `im_client_blacklist`;
CREATE TABLE `im_client_blacklist`
(
`id` bigint(20) NOT NULL COMMENT '客户端id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`fk_appid` bigint(20) DEFAULT NULL COMMENT '应用appid',
`fk_client_id_prevent` bigint(20) DEFAULT NULL COMMENT '拉黑者',
`fk_client_id_be_prevent` bigint(20) DEFAULT NULL COMMENT '被拉黑',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='黑名单';
-- ----------------------------
-- Table structure for im_conversation
-- ----------------------------
DROP TABLE IF EXISTS `im_conversation`;
CREATE TABLE `im_conversation`
(
`id` bigint(20) NOT NULL COMMENT '会话id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`last_message` timestamp NULL DEFAULT NULL COMMENT '对话中最后一条消息的发送或接收时间',
`fk_appid` bigint(20) NOT NULL COMMENT '应用appid',
`creator` bigint(20) NOT NULL COMMENT '创建者客户端id',
`name` varchar(255) DEFAULT NULL COMMENT '可选 对话的名字,可为群组命名。',
`attributes` json DEFAULT NULL COMMENT '可选 自定义属性,供开发者扩展使用。',
`system` tinyint(1) DEFAULT NULL COMMENT '可选 对话类型标志,是否是系统对话,后面会说明。',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话表';
-- ----------------------------
-- Table structure for im_conversation_members
-- ----------------------------
DROP TABLE IF EXISTS `im_conversation_members`;
CREATE TABLE `im_conversation_members`
(
`id` bigint(20) NOT NULL COMMENT '唯一id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '加入时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`fk_appid` bigint(20) NOT NULL COMMENT '应用appid',
`fk_conversation_id` bigint(20) NOT NULL COMMENT '会话表id',
`fk_client_id` bigint(20) NOT NULL COMMENT '客户端id',
`display_status` int(2) DEFAULT '1' COMMENT '单向删除(隐藏)会话, 0不显示, 1显示',
`allow_system_push_status` int(2) DEFAULT '1' COMMENT '是否允许系统推送, 0不推送, 1推送',
`attributes` json DEFAULT NULL COMMENT '可选 自定义属性,供开发者扩展使用',
`client_remark_name` varchar(255) DEFAULT NULL COMMENT '会话中client的备注名',
PRIMARY KEY (`id`) USING BTREE,
KEY `fk_conversation_id` (`fk_conversation_id`) USING HASH
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话成员表';
-- ----------------------------
-- Table structure for im_inbox
-- ----------------------------
DROP TABLE IF EXISTS `im_inbox`;
CREATE TABLE `im_inbox`
(
`id` bigint(20) NOT NULL COMMENT '收件id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`read_time` timestamp NULL DEFAULT NULL COMMENT '读取时间',
`receiver_time` timestamp NULL DEFAULT NULL COMMENT '接收时间',
`fk_appid` bigint(20) NOT NULL COMMENT '应用appid',
`receiver` bigint(20) NOT NULL COMMENT '接收客户端id',
`fk_msg_id` bigint(20) NOT NULL COMMENT '消息id',
`read_msg_status` int(1) DEFAULT NULL COMMENT '0未读; 1已读',
`receiver_msg_status` int(1) DEFAULT NULL COMMENT '0未接收; 1已接收',
`fk_conversation_id` bigint(20) NOT NULL COMMENT '会话id',
PRIMARY KEY (`id`) USING BTREE,
KEY `getMyOfflineMsg` (`receiver`) USING HASH
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息收件箱表';
-- ----------------------------
-- Table structure for im_ios_apns
-- ----------------------------
DROP TABLE IF EXISTS `im_ios_apns`;
CREATE TABLE `im_ios_apns`
(
`id` bigint(20) NOT NULL COMMENT 'id',
`fk_app_id` bigint(20) NOT NULL COMMENT '应用appid',
`apns_file_value` text CHARACTER SET latin1 COMMENT 'Base64(apns.p12)',
`env` int(255) DEFAULT NULL COMMENT '环境,正式1,测试0',
`bundle_id` varchar(500) DEFAULT NULL COMMENT 'bundle_id',
`pwd` varchar(500) DEFAULT NULL COMMENT '证书密码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='apns配置表';
-- ----------------------------
-- Table structure for im_message
-- ----------------------------
DROP TABLE IF EXISTS `im_message`;
CREATE TABLE `im_message`
(
`id` bigint(20) NOT NULL COMMENT '消息id',
`fk_conversation_id` bigint(20) DEFAULT NULL COMMENT '会话id',
`fk_appid` bigint(20) NOT NULL COMMENT '应用appid',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`withdraw_time` timestamp NULL DEFAULT NULL COMMENT '撤回时间',
`update_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`sender` bigint(20) DEFAULT NULL COMMENT '发送者客户端id',
`content` json DEFAULT NULL COMMENT '内容',
`withdraw` tinyint(1) DEFAULT '0' COMMENT '0未撤回; 1已撤回',
`event` tinyint(1) DEFAULT '0' COMMENT '0非事件; 1为事件',
`system` tinyint(1) DEFAULT '0' COMMENT '0非系统通知; 1为系统通知',
`at` text COMMENT 'at他人,传入客户端id数组',
`send_status` int(2) DEFAULT NULL COMMENT '发送状态\n1AVIMMessageStatusSending(发送中)\n2AVIMMessageStatusSent(发送成功)\n3AVIMMessageStatusFailed(失败)',
PRIMARY KEY (`id`) USING BTREE,
KEY ```fk_conversation_id``` (`fk_conversation_id`) USING HASH
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息存储表';
-- ----------------------------
-- Table structure for im_rtc_channel
-- ----------------------------
DROP TABLE IF EXISTS `im_rtc_channel`;
CREATE TABLE `im_rtc_channel`
(
`id` bigint(20) NOT NULL COMMENT '会话id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`fk_appid` bigint(20) NOT NULL COMMENT '外键 应用appid',
`fk_conversation` bigint(20) DEFAULT NULL COMMENT '外键 会话id 可为空',
`name` varchar(255) DEFAULT NULL COMMENT '可选 对话的名字,可为群组命名。',
`attributes` json DEFAULT NULL COMMENT '可选 自定义属性,供开发者扩展使用。',
`channel_status` int(1) DEFAULT NULL COMMENT '频道状态, 1打开 , 0关闭',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Rtc频道表';
-- ----------------------------
-- Table structure for im_rtc_log
-- ----------------------------
DROP TABLE IF EXISTS `im_rtc_log`;
CREATE TABLE `im_rtc_log`
(
`id` bigint(20) NOT NULL COMMENT 'rtc记录id',
`fk_appid` bigint(20) NOT NULL COMMENT '应用appid',
`fk_channel_id` bigint(20) DEFAULT NULL COMMENT '频道id',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`fk_conversation` bigint(20) DEFAULT NULL COMMENT '外键 会话id 可为空',
`client_id` bigint(20) DEFAULT NULL COMMENT '事件主体客户端id',
`event_type` int(1) DEFAULT NULL COMMENT '事件类型 1成为房主, 2加入频道,3退出频道,4拒绝邀请 ,5上传sdp, 6上传candidate',
`remark` text COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RTC日志记录表';
SET
FOREIGN_KEY_CHECKS = 1;
# wecloud-RTC音视频客户端信令对接文档
# wecloud-RTC音视频客户端信令对接文档
## 文档描述
此文档为单人RTC音视频通讯技术对接文档
由于RTC基于wecloud-im即时通讯服务,**对接RTC前,需要先对接wecloud-im服务**
## 核心概念说明
### 频道与会话
频道与会话的概念不一样
```
1. 引入"频道RtcChannel"概念, 可以不基于会话发起音视频通话
2. 目前频道只支持两个client,进行通话的两端必须先加入到同一个"频道",所有的指令都在频道内进行转发
3. 允许不在同个会话中的两个client加入到同个频道进行通话 (可配置是否两个client必须在同一个会话才能发起音视频通话)
4. 一个频道可以由通话发起者绑定到会话ID,"挂断","未接听"等状态会同步到会话, 未绑定将不同步到会话(可选)
5. 连接websocket时带上client类型, 如web,安卓,ios
client需要监听频道内 状态更新(房间断开,挂断)、用户状态更新(用户加入, 用户退出)、流状态更新(切换音频 切换视频)
6.如果接收方未在线时收到音视频通话邀请,在规定时间内发起方还在等待中,发起方重新连接可以收到音视频通话的离线信令
```
## 流程图
### 不绑定到会话 示例一
![image-20211018180059906](https://tva1.sinaimg.cn/large/008i3skNly1gvjmbmw0wcj60u016877z02.jpg)
## subCmd指令码说明
枚举类:
```
/**
* 接收到RTC邀请
*/
RTC_CALL(1),
/**
* 用户状态更新事件(用户加入频道)
*/
CLIENT_JOIN(2),
/**
* 用户状态更新事件(用户退出频道)
*/
CLIENT_LEAVE(3),
/**
* 用户状态更新事件(用户拒接邀请,不同意进入频道)
*/
CLIENT_REJECT(4),
/**
* SDP数据转发
*/
SDP_FORWARD(5),
/**
* candidate候选者数据转发
*/
CANDIDATE_FORWARD(6);
```
## 创建频道 发起RTC音视频通话 (http)
见http接口文档
## 接收方收到RTC音视频通话邀请(ws)
服务端向client接收方下发数据:
```json
{
"cmd":4,
"data":{
"subCmd":"1",
"subData":{
"type":"video",
"conversationId":null,
"channelId":1234263457652,
"timestamp":113123123,
"clientId":"client_1010"
},
"attrs":{
"a":"示例: 用户自定义的一些键值对",
"b":"存储用户自定义的一些键值对"
}
}
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| conversationId | Long | 否 | 绑定的会话id |
| subCmd | String | 否 | 子类型指令 |
| clientId | String | 否 | 发起通话的客户端ID |
| channelId | Long | 否 | 由服务端创建的频道id |
| type | String | 否 | 类型: "video" 或 "voice" |
| timestamp | Timestamp | 否 | 频道创建时间戳 |
## 同意加入频道(http)
见http接口文档
## 有client加入频道(ws下发)
服务端向频道内其他client响应数据:
```json
{
"cmd":4,
"data":{
"subCmd":"2",
"subData":{
"channelId":1234263457652,
"clientId":7657567,
}
}
}
```
## 拒绝加入频道 http
见http接口文档
## 有Client拒绝加入频道(ws下发)
服务端向频道内其他client响应数据:
```json
{
"cmd":4,
"data":{
"subCmd":4,
"subData":{
"channelId":1234263457652,
"clientId":7657567,
}
}
}
```
## SDP转发
流媒体描述信息
(服务端仅负责转发)(candidate,anser,offer)
### client上传SDP (http接口)
见http接口文档
### client接收SDP (ws下发)
```json
{
"cmd":4,
"data":{
"subCmd":5,
"subData":{
"channelId":1234263457652,
"clientId":7657567,
"sdpData":"xxxxxxxxxxxxxxxx",
"sdpType":"Offer/Answer"
},
"attrs":{}
}
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| channelId | Long | 否 | 频道id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| subCmd | String | 否 | 子指令 |
| sdpData | String | 否 | sdp转发的数据 |
| sdpType | String | 否 | sdp类型: Offer或Answer |
## Candidate转发
候选人信息
(服务端仅负责转发)(candidate,anser,offer)
### client上传Candidate (http接口)
见http接口文档
### client接收Candidate (ws下发)
```json
{
"cmd":4,
"data":{
"subCmd":6,
"subData":{
"channelId":1234263457652,
"clientId":7657567,
"candidateData":"xxxxxxxxxxxxxxxx",
},
"attrs":{}
}
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| channelId | Long | 否 | 频道id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| subCmd | String | 否 | 子指令 |
| candidateData | String | 否 | 转发的候选者数据 |
## 主动挂断(离开频道) http
见http接口文档
## 有client离开频道(ws下发)
服务端向频道内其他client响应数据:
```json
{
"cmd":4,
"data":{
"subCmd":3,
"subData":{
"channelId":1234263457652,
"clientId":7657567
}
}
}
```
## 查询忙线状态(对方正在通话中) http
见http接口文档
## 断线重连
重新join进频道即可重连
## 对方是否还挂起
## 查询频道信息及在线人员 http
## 视频/音频切换
## 查询对方是否离开频道
# wecloud-im 前端Websocket对接文档
# wecloud-im 前端Websocket对接文档
## 文档描述
本项目有两个文档: websocket对接文档 和 HTTP协议的API文档
**本文档为websocket对接文档**
HTTP协议的API以Swagger实时生成为准
## HTTP协议API文档
Swagge文档地址:
**本地环境 api文档(实时更新 开发查看此文档):**
IP可能有变化 联系后端
```
http://192.168.1.89/api/doc.html#/home
```
账号密码admin admin
**测试外网 api文档:**
```
https://wstest.im199.com/api/doc.html#/home
```
只包含api接口文档 websocket对接说明在此文档中
## 适用对象
web端. 安卓端. ios端. flutter等一切支持websocket的语言
## 核心概念说明
### clientId、用户和登录
即时通讯服务中的每一个终端称为一个「Client」。Client 拥有一个在应用内唯一标识自己的 ID(`clientId`)。这个 ID 由应用自己定义,必须是 **由任意英文字母、数字、半角下划线与半角短横线组成,可以纯数字,不超过 64个字符的字符串组成**。在大部分场合,Client 都可以对应到应用中的某个「用户」,但是并不是只有真的用户才能作为「Client」,你完全可以把一个探测器当成一个「Client」,把它收集到的数据通过即时通讯服务广播给更多「人」。
要使用即时通讯服务,每一个终端设备需要首先建立与即时通讯云服务端的 WebSocket 长连接,并使用唯一的 `clientId` 来加入即时通讯服务,我们把这一过程称为「登录」。请注意这里的登录仅仅指客户端登录即时通讯服务,与应用层面的用户账户注册登录是不一样的。
### appKey与appSecret
appKey为应用在蔚可云的唯一标识, appSecret为安全密钥, 均由蔚可云控制台或联系客服下发,给应用接入方安全验证密钥,生产环境appSecret不建议保存在接入方前端客户端, appKey与appSecret总是成对出现和使用
### 生成sign签名
sign 由接入方后端生成, 进行加签的参数: MD5{timestamp + clientId + appKey + appSecret}
timestamp:毫秒时间戳
clientId:由后端生成
**在生产环境中,你需要自行部署服务器签发 sign**
### 对话(Conversation)
用户登录之后,与其他人(client)进行消息沟通,即为开启了一个「对话(Conversation)」。在即时通讯服务中,「对话」包含了沟通的用户群体(成员),也是所有消息依托的媒介:消息都是由某一个 Client 发往一个「对话」。终端用户在开始聊天之前,需要先创建或者加入一个对话,然后再邀请其他人进来(可选),之后所有参与者在这个对话内进行交流。
### 已读/已接收
**已接收**为客户端接收到该消息,已经保存到本地缓存中,用户还未打开会话查看该条消息的情况. 服务器将不会再次下发已接收状态的离线消息,未接收的消息会在拉取离线消息时返回
**已读**为客户端已经查看该消息
## ping / pong心跳机制
1. 客户端接收到服务端下发"ping"字符串时, 必须立即回应"pong"字符串;
2. 客户端可完善定时向服务端发起"ping"字符串,来保持websocket连接状态, 服务端会立即回应"pong"字符串,如果连续3次未及时响应"pong",即可认定websocket已经断开,进入重新连接流程;
3. 建议客户端发"ping"间隔时间为5秒
## 客户端连接流程
![wc_im_client连接流程-Page-1](https://tva1.sinaimg.cn/large/008i3skNly1gvbhmdzg3kj60zt0u0juf02.jpg)
1. appKey, appSecret为蔚可云下发给客户方安全保护密钥,不建议保存在前端客户端;
2. 第三方应用服务端需提供获取sign的接口, sign 由MD5{timestamp + clientId + appKey + appSecret},其中clientId由后端生成;
3. 前端拿到sign后,调用验证sign接口进行获取token;
4. websocket连接初始化需要带上token即可连接成功;
5. sign 需要在你的应用服务端生成;
### 第三方应用后端生成sign接口示例
本文展示如何在服务端部署一个 sign 生成器。
**java**示例代码 ,供客户应用后端参考
```java
import org.springframework.util.DigestUtils;
private void getSign(String timestamp, String clientId, String appKey, String appSecret) {
String data = timestamp + clientId + appKey + appSecret;
String sign = DigestUtils.md5DigestAsHex(data.getBytes());
}
public static void main(String[] args) {
String clientId = "client_123123";
String appKey = "elLwpel1gWCHDqZy";
String appSecret = "68809bb5a9077a83631aeb0b17b5965d6b2302faf2ab3737";
String timestamp = String.valueOf(new Date().getTime());
getSign(timestamp, clientId, appKey, appSecret);
}
```
第三方应用后端需要响应的参数:
```json
{
"timestamp": "1628838135066",
"clientId": "client_3334444",
"appKey": "D13ug9jsWbJbeVx1",
"sign": "c15a886fe4114dba2c8f078369e6bec9"
}
```
### 连接WebSocket
ws://localhost:8899/ws?token=xxxxxx&platform=1
示例:
```text
ws://localhost:8899/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImhhaGFoXzMwIiwiaXNzIjoid2VjbG91ZF9pbSIsImFwcEtleSI6ImVsTHdwZWwxZ1dDSERxWnkiLCJleHAiOjE2MjkwOTY0MzksImlhdCI6MTYyMDQ1NjQzOSwianRpIjoiNDA1YzE3MWM2Njc5NGJmMDllNGRjZDdhNzA0ZjY3YTgifQ.7g2J_0q9UnuWszpuapSJUXJEwVevvI8Rm2Srg3594Lk&platform=1
```
#### 请求参数
| 字段名 | 字段类型 | 是否可空 | 说明 |
| -------- | -------- | -------- | ------------------------------------- |
| token | String | 否 | token |
| platform | int | 是 | 设备平台类型: 1 安卓; 2 ios; 3 web |
#### 不同环境连接
**本地环境**
```
ws://192.168.1.89:8899/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFhYWFhMyIsImlzcyI6IndlY2xvdWRfaW0iLCJhcHBLZXkiOiJRTnRQM0VqdEx3MjZla3QwIiwiZXhwIjoxNjc2NjYzNTkyLCJpYXQiOjE2MjY2ODQ1ODQsImp0aSI6ImY3NWVkODljMGZlZDQ0OWViZTczNzdiNmRlZTNhMDNlIn0.QxzGhpIpzYmJDaFGmdrMynWBkM7umtT5SnSSBKu46UY
```
**测试外网**
```
wss://wstest.im199.com/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFiY2QxIiwiaXNzIjoid2VjbG91ZF9pbSIsImFwcEtleSI6IkpLdE5IZnJWVXdzaGF4ek4iLCJleHAiOjE2MjgzMjMxNDMsImlhdCI6MTYyMzEzOTE0MywianRpIjoiNWU3NzU5ZjM2ODQ3NDFiMzg4MGEyYjkwMjQ0OWZjZmYifQ.CC-iuGjNwQLH4VxFI2wZEPuP4AGabOUOiRh9snp3IB4
```
_______
**生产环境**
```
wss://ws.im199.com/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFiY2QxIiwiaXNzIjoid2VjbG91ZF9pbSIsImFwcEtleSI6IkpLdE5IZnJWVXdzaGF4ek4iLCJleHAiOjE2MjgzMjMxNDMsImlhdCI6MTYyMzEzOTE0MywianRpIjoiNWU3NzU5ZjM2ODQ3NDFiMzg4MGEyYjkwMjQ0OWZjZmYifQ.CC-iuGjNwQLH4VxFI2wZEPuP4AGabOUOiRh9snp3IB4
```
## 请求CMD指令说明
- 1: 发送IM消息请求
- 3: 单人WebRTC请求
## 响应CMD指令说明
- 1:响应请求(用于im聊天)
- 2:下发在线用户消息
- 3:下发在线消息事件类型
- 4:下发单人在线RTC事件通知
- 5: 会话中的事件
- 6:下发透传消息
## 消息type说明
- 文本消息 -1
- 图像消息 -2
- 音频消息 -3
- 视频消息 -4
- 位置消息 -5
- 文件消息 -6
## 事件类型type
(xx表示为某客户端)
- xx邀请xx加入会话 -1007
- xx被xx移出会话 -1008
- xx已接收某消息 -1009
- xx已读某条消息 -1010
- 你被xx拉入新会话 -1011
- 主动退出会话 -1012
- 成为新群主 -1013
以上基础平台固定类型均使用负数,所有正数留给自定义扩展类型使用,0 作为「没有类型」被保留起来。
## code错误码
- 401 用户token无效
## websocket客户端请求参数说明
**reqId** 为每次客户端通过websocket通道发送请求的唯一request Id, 用来标识每次请求,方便定位问题,以及用于绑定服务端响应对此请求request id的数据.
websocket像服务端发送数据不同于http请求接口.
http发送请求 会一个请求对应一个响应,客户端接收也知道该响应数据是针对哪个请求的.
websocket是异步的 有可能你很快速的发送了几条消息,服务器响应的数据不一定是按你发送的消息顺序响应, 所以你每个请求要自己定一个id, 服务器就算乱序响应数据,客户端也能知道响应的数据对应哪个请求
**cmd** 为请求指令
**data** 为数据,所有需要发送的数据都在此参数下
**data内**的参数: **toConversation**(会话id)与**type**(消息类型)为固定的,其它参数名与参数值皆可由使用方自定义
## websocket服务器响应参数说明
**reqId** 一个请求唯一的id,服务端响应时,会将对应的id与数据一同返回
**cmd** 为响应指令
![image-20210521104503728](https://tva1.sinaimg.cn/large/008i3skNly1gqpurt4tgnj30py0dc3zd.jpg)
## 消息收发流程
![IM_client在线消息发送流程](https://tva1.sinaimg.cn/large/008i3skNgy1guv1rkeif7j60ix0hlgmc02.jpg)
![IM_client在线消息接收流程](https://tva1.sinaimg.cn/large/008i3skNgy1guv1rdrnq3j60fw0dg3yx02.jpg)
### 1. 客户端发送文本消息
**示例一: 必传参数:**
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1402147846261706752,
"type":-1,
"text":"你好,这是一123个纯文本消息"
}
}
```
**示例二: 全部可选参数:**
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"push":{
"title":"收到一条新消息",
"subTitle":"点击查看"
},
"diyAbcd":"aaaa自已定义字段的值",
"toConversation":1402147846261706752,
"type":-1,
"text":"你好,这是一123个纯文本消息",
"attrs":{
"a":"用户自定义的一些键值对",
"b":"用户自定义的一些键值对"
}
}
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------------------ | -------- | -------- | -------------- |
| cmd | String | 否 | 指令码 |
| data | Object | 否 | 除toConversation与type为固定的参数,其它参数皆可由使用方自定义 |
| attrs | Object | 是 | 自定义拓展字段 |
| toConversation | Long | 否 | 发送到会话id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| push | String | 否 | 客户端自定义系统推送内容 |
### 2. 服务端响应给发送者
```json
{
"cmd":1,
"code":200,
"msg":"成功",
"data":{
"msgId":"1394207796915998720"
},
"reqId":"123123123"
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------------------ | -------- | -------- | -------------- |
| cmd | String | 否 | 指令码 |
| msgId | String | 否 | 消息唯一ID |
### 3. 接收方收到在线消息
```json
{
"cmd":2,
"code":200,
"msg":"成功",
"data":{
"msgId":"1394207796915998720",
"createTime":1621240016587,
"withdrawTime":null,
"sender":"hahah_30",
"content":{
"diyAbcd":"aaaa自已定义字段的值",
"text":"你好,这是一123个纯文本消息",
"type":-1,
"attrs":{
"a":"用户自定义的一些键值对",
"b":"用户自定义的一些键值对"
}
},
"withdraw":false,
"event":false,
"system":false,
"at":null,
"conversationId":"1394188055950266368"
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------------------ | -------- | -------- | -------------- |
| cmd | String | 否 | 指令码 |
| data | Object | 否 | 除toConversation与type为固定的参数,其它参数皆可由使用方自定义 |
| attrs | Object | 是 | 自定义拓展字段 |
| toConversation | Long | 否 | 发送到会话id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| push | String | 否 | 客户端自定义系统推送内容 |
| sender | String | 否 | 发送方 |
| withdrawTime | time | 是 | 撤回时间戳 |
| createTime | time | 否 | 消息发送时间戳 |
| at | String | 否 | 提到了其他客户端,以英文逗号分开 |
| withdraw | Boolean | 是 | 是否撤回 |
| event | Boolean | 是 | 是否为事件类型 |
| withdraw | Boolean | 是 | 是否撤回 |
| system | Boolean | 是 | 是否为系统消息 |
### 4.接收方收到,需要回执
已接收回执需参考API对接文档
## 发送图片
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1394188055950266368,
"type":-2,
"file": {
"url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // 必要参数
"objId": "54699d87e4b0a56c64f470a4", // 文件对应的AVFile.objectId
"metaData": {
"name": "IMG_20141223.jpeg", // 图像的名称
"format": "png", // 图像的格式
"height": 768, // 单位:像素
"width": 1024, // 单位:像素
"size": 18 // 单位:b
}
},
"attrs":{}
}
}
```
上面是完整的例子,如果只想简单的发送图像 URL:
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1394188055950266368,
"type":-2,
"file": {
"url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // 必要参数
"objId": "54699d87e4b0a56c64f470a4", // 文件对应的AVFile.objectId
"metaData": {
"name": "IMG_20141223.jpeg", // 图像的名称
"format": "png", // 图像的格式
"height": 768, // 单位:像素
"width": 1024, // 单位:像素
"size": 18 // 单位:b
}
}
}
```
## 其他富媒体消息
如视频 位置 音频 发送红包 好友验证等等
可参考:https://leancloud.cn/docs/realtime_v2.html#hash939050100
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1394188055950266368,
"type":由应用自定义,
"应用自定义参数": 应用自定义值
}
}
```
## 服务端下发透传消息
```json
{
"reqId":"1",
"cmd":6,
"data":{
"toConversation":1394188055950266368,
"createTime":1621240016587,
"content":"json字符串,或任意字符数据"
}
}
```
## 客户端在线接收事件类型消息
### 事件类型type (xx表示为某客户端)
- xx邀请xx加入会话 -1007
- xx被xx移出会话 -1008
- xx已接收某消息 -1009
- xx已读某条消息 -1010
- 你被xx拉入新会话 -1011
- xx主动退出会话 -1012
- xx成为新群主 -1013
- 群拓展字段变动事件 -1014
- 会话名称字段变动事件 -1015
### 下发 会话名称字段变动事件
该event事件消息类型不需要回执,该消息不会存入离线消息列表
```json
{
"cmd":5,
"code":200,
"msg":"成功",
"data":{
"createTime":1629086007054,
"content":{
"type":-1015,
"name":"xxxxxxx"
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
### 下发 会话拓展字段变动事件
该event事件消息类型不需要回执,该消息不会存入离线消息列表
```json
{
"cmd":5,
"code":200,
"msg":"成功",
"data":{
"createTime":1629086007054,
"content":{
"type":-1014,
"attributes":"xxxxxxx"
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
### 下发 xx成为新群主
"cmd":5
该event事件消息类型需要已读和已接收回执,该消息会存入离线消息列表和消息历史记录列表
```json
{
"cmd":5,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007054,
"sender":"aaaaa1",
"content":{
"type":-1013
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------ | -------- | -------- | ----------------- |
| sender | String | 否 | 新群主的client ID |
### 下发 xx主动退出会话
"cmd":5
该event事件消息类型需要已读和已接收回执,该消息会存入离线消息列表和消息历史记录列表
```json
{
"cmd":5,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007054,
"sender":"aaaaa1",
"content":{
"type":-1012
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------ | -------- | -------- | --------------- |
| sender | String | 否 | 退出的client ID |
### 下发 xx被xx移出会话
type为-1008
"cmd":5
该event事件消息类型需要已读和已接收回执,该消息会存入离线消息列表和消息历史记录列表
```json
{
"cmd":5,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007054,
"sender":"aaaaa1",
"content":{
"operator":"aaaaa1",
"passivityOperator":"aaaaa2",
"type":-1008
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ----------------- | -------- | -------- | ----------------- |
| operator | String | 否 | 操作的client ID |
| passivityOperator | String | 否 | 被操作的client ID |
### 下发 xx邀请xx加入会话
type为-1007
"cmd":5
该event事件消息类型需要已读和已接收回执,该消息会存入离线消息列表和消息历史记录列表
```json
{
"cmd":5,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007054,
"sender":"aaaaa1",
"content":{
"operator":"aaaaa1",
"passivityOperator":"aaaaa2",
"type":"-1007"
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------------------ | -------- | -------- | -------------- |
| operator | String | 否 | 操作的client ID |
| passivityOperator | String | 否 | 被操作的client ID |
### 服务端在线下发 某消息*已接收*状态
receiverId:接收方客户端id
conversationId:会话id
type为-1009 : 某消息已接收状态
msgId: 消息id;
该event事件消息类型不需要已读和已接收回执,不会保存进离线,仅下发给当前在线会话客户端.
```json
{
"cmd":3,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007054,
"withdrawTime":null,
"sender":"aaaaa1",
"content":{
"receiverId":"aaaaa1",
"type":"-1009"
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------------------ | -------- | -------- | -------------- |
| cmd | String | 否 | 指令码 |
| conversationId | Long | 否 | 会话id |
| sender | String | 否 | 发送方 |
| createTime | time | 否 | 消息发送时间戳 |
| event | Boolean | 是 | 是否为事件类型 |
### 服务端在线下发 某消息*已读*状态
receiverId:接收方客户端id
conversationId:会话id
type为-1010 : 某消息已读状态
msgId: 消息id;
该event事件消息类型不需要回执,不会保存进离线,仅下发给当前在线会话客户端.
```json
{
"cmd":3,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007084,
"sender":"aaaaa1",
"content":{
"receiverId":"aaaaa1",
"type":"-1010"
},
"event":true,
"conversationId":1427109730563788800
},
"reqId":null
}
```
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ------------------ | -------- | -------- | -------------- |
| cmd | String | 否 | 指令码 |
| conversationId | Long | 否 | 会话id |
| sender | String | 否 | 发送方 |
| createTime | time | 否 | 消息发送时间戳 |
| event | Boolean | 是 | 是否为事件类型 |
# wecloud-im服务端REST API对接文档
# wecloud-im服务端REST API对接文档
## 本地API文档地址
http://192.168.1.110:8082/api/doc.html#/home
账号密码admin admin
以上只包含api接口文档 websocket对接说明在此文档中
## 测试外网
文档:
```
https://wstest.im199.com/api/doc.html#/home
```
___
测试外网请求示例:
```
https://wstest.im199.com/api/imApplication/add
```
_______
## 生产环境
ws.im199.com
文档:
```
https://ws.im199.com/api/doc.html#/home
```
___
测试外网请求示例:
```
https://ws.im199.com/api/imApplication/add
```
_______
## 鉴权方式
### 方式一(正在开发):
对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP header 的 Content-Type 需要设置为 `application/json`
用户验证通过 HTTP header 来进行,**appkey** 标明正在运行的是哪个应用(应用的 App ID), **appSecret** 用来授权鉴定 endpoint:
**示例**: rest-api-会话成员表分页列表
- Request URL: http://192.168.1.89:8082/api/restApi/imConversationMembers/findList
- Request Method: POST
**请求头部**
```
appkey: D13ug9jsWbJbeVx1
appSecret: c92edcc7ba0c68b9b1da4cec6f3511876b2302faf2ab3737
```
**请求body**
```
{
"conversationId": 1442742976269914112
}
```
**响应数据**
```
{"code":200,"message":"成功","data":[{"clientId":"1442742442803503105"},{"clientId":"1435497966619996162"}]}
```
### 方式二(暂时没开发)
**更安全的鉴权**
我们还支持一种新的 API 鉴权方式,即在 HTTP header 中使用 **X-LC-Sign** 来代替 **X-LC-Key**,以降低 App Key 的泄露风险。例如:
```json
curl -X PUT \
-H "X-LC-Id: {{appid}}" \
-H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \
-H "Content-Type: application/json" \
-d '{"content": "在 HTTP header 中使用 X-LC-Sign 来更新一篇博客的内容"}' \
https://https://ws.im199.com/api/
```
**X-LC-Sign** 的值是由 `sign,timestamp` 组成的字符串:
| 取值 | 约束 | 描述 |
| :-------- | :--- | :----------------------------------------------------------- |
| sign | 必须 | 将 timestamp 加上 App Key 组成的字符串,再对它做 MD5 签名后的结果。 |
| timestamp | 必须 | 客户端产生本次请求的 unix 时间戳(UTC),精确到**毫秒**。 |
举例来说,假设应用的信息如下:
| App Id | `FFnN2hso42Wego3pWq4X5qlu` |
| ---------- | --------------------------------- |
| App Key | `UtOCzqb67d3sN12Kts4URwy8` |
| 请求时间 | 2016-01-17 15:15:43.466 GMT+08:00 |
| timestamp | `1453014943466` |
**使用 App Key 来计算(MD5) sign**
```sh
-H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \
```
## 响应格式
对于所有的请求,响应格式都是一个 JSON 对象。
一个请求是否成功是由 HTTP 状态码标明的。一个 2XX 的状态码表示成功,而一个 4XX 表示请求失败。当一个请求失败时响应的主体仍然是一个 JSON 对象,但是总是会包含 `code``error` 这两个字段,你可以用它们来进行调试。举个例子,如果尝试用非法的属性名来保存一个对象会得到如下信息:
## 单聊、群聊
### 创建对话
### 查询对话
### 更新对话
### 删除对话
### 增加成员
### 移除成员
### 查询成员
### 单聊、群聊-发消息
### 查询历史消息
### 单聊、群聊-修改消息
### 单聊、群聊-撤回消息
## 黑名单
### 增加对话黑名单
### 移除对话黑名单
### 查询对话黑名单
# wecloud-im 即时通讯云对接文档
# wecloud-im 即时通讯云对接文档
[TOC]
## 使用场景
即时通讯服务现在已经被广泛使用**在应用内社交、工作协同、客服系统、超大型赛事和电视直播、以及游戏状态同步**等多种业务场景之中。
即时通讯是主要**解决产品内聊天**即时通信(Instant Messaging)、实时数据同步等需求,其设计上的主要目标是:
- **支持为现有应用快速加入多种通讯能力**
我们很多客户的产品都已经达到一个比较稳定的形态,即时通讯只是其中一个锦上添花的功能,所以如何和现有系统无缝集成,是我们设计上一个重要的出发点。即时通讯服务可以在应用账户系统独立的情况下,快速接入并稳定安全地运行。
我们支持了多种典型通讯场景,提供了UI 库和脚手架来帮助开发者快速接入。并且考虑到业务运行环境,我们提供了支持的 SDK。
- **强大的自定义机制满足业务各种扩展需求**
我们默认支持了**文本、图片、音视频、地理位置**等多种类型的消息收发,同时也允许产品开发者来扩展自己的消息类型和 UI 样式。并且在基本功能之外,我们也支持更多高阶需求,例如消息撤回与修改、@ 成员提醒、暂态消息、「已读」回执、离线推送、敏感内容过滤等等与消息收发相关的功能在这里都可以得到满足。
- **安全和权限控制**
我们始终把系统安全性放在首要位置,客户端与云端使用 WebSocket 全双工通讯,全程 TLS 加密传输。在用户登录和操作权限的控制上,我们专门设计了第三方操作签名的机制,让应用层在快速接入的同时,也可以实时、完整地控制用户在即时通讯系统内的所有活动。在群聊和开放聊天室等业务场景里,我们也提供了成员角色管理和黑名单的机制,以满足产品运营管理的多样需求。
- **最大限度降低客户的生产运维成本**
我们提供专业的技术支持服务,富有经验的资深工程师 7 × 24 小时对接,以帮助开发者快速、有效地完成产品集成,缩短开发周期。此外在产品运营阶段,让开发者彻底摆脱后端系统日常的运维细节和突发的软硬件故障处理,也不用关心用户量和流量的变化。帮助客户在享受高品质技术服务的同时,也可以最大限度降低生产运维成本,并且以更快的速度推进产品迭代,是我们始终追求的目标。
## 功能和特性
即时通讯服务提供的主要功能有:
- **基本聊天功能**,包括:
- 支持多种聊天场景。除了普通的单聊、群聊
- 用户之间可以发送多种多样的消息,如文本、图片、语音、音视频、地理位置等,以及更多的应用层自定义消息。
- 聊天消息自动保存在云端,可选支持永久消息存储,支持各种复杂的查找和翻页方式。
- **特殊的消息收发需求**。除了普通的消息收发之外,我们还支持:
- 带有提醒功能的 **@ 消息**(如微信里面的 @ 某人)
- 消息的 **撤回和修改**
- 消息送达和对方已读的 **回执通知**
- 群聊里面为了避免过于干扰,允许用户开启 **消息免打扰** 开关
- 在消息接收方离线时,自动转为 **系统推送通知,ios和安卓**(Push Notification)
- **安全控制**
任何终端用户要开启即时通讯服务,只需要提供一个唯一标识自己的 `clientId` 即可,不需要传入任何用户数据,以保证信息不被泄露,这种与产品自有账户系统解耦合的方式,带来了集成的便利,也可以促使通讯服务商专注做好底层的「信使」角色。 同时我们也提供 **第三方鉴权** 的机制,通过在聊天流程中加入开发者服务器签名授权这一环节,来确保通讯操作的安全。 而且,即时通讯 SDK 与云端是 WebSocket 全双工通讯,且全程使用 TLS 安全加密。
- **强大的业务扩展能力**
对于很多典型的需求,我们提供了默认的实现,而为了支持业务的多样性和特殊性,我们也提供了丰富的扩展机制:
- 为了和产品自有用户系统进行对接,我们提供了第三方操作鉴权的扩展接口,确保在用户登录、创建/加入/退出对话群组、以及拉取聊天记录时,所有操作都得到了授权。
- 同时我们还支持开发者对消息传递的过程进行 **hook 处理**,在消息到达云端但是还没有投递之前和投递之后,分别完成自定义的处理逻辑,例如过滤掉竞品的品牌,以及自定义离线推送消息,等等。
- 我们也支持通过简单的 **web hook** 来完成云端和应用后端的消息同步。
- 在提供移动端的 SDK 之外,我们还提供了 REST API,以帮助产品在可信环境下更好地实现业务处理。
**我们相信灵活性和扩展性也是云服务的核心竞争力。**
## 核心概念说明
### clientId、用户和登录
即时通讯服务中的每一个终端称为一个「Client」。Client 拥有一个在应用内唯一标识自己的 ID(`clientId`)。这个 ID 由应用自己定义,必须是 **由任意英文字母、数字、半角下划线与半角短横线组成,可以纯数字,不超过 64个字符的字符串组成**。在大部分场合,Client 都可以对应到应用中的某个「用户」,但是并不是只有真的用户才能作为「Client」,你完全可以把一个探测器当成一个「Client」,把它收集到的数据通过即时通讯服务广播给更多「人」。
要使用即时通讯服务,每一个终端设备需要首先建立与即时通讯云服务端的 WebSocket 长连接,并使用唯一的 `clientId` 来加入即时通讯服务,我们把这一过程称为「登录」。请注意这里的登录仅仅指客户端登录即时通讯服务,与应用层面的用户账户注册登录是不一样的。
### appKey与appSecret
appKey为应用在蔚可云的唯一标识, appSecret为安全密钥, 均由蔚可云控制台或联系客服下发,给应用接入方安全验证密钥,生产环境appSecret不建议保存在接入方前端客户端, appKey与appSecret总是成对出现和使用
### 生成sign签名
sign 由接入方后端生成, 进行加签的参数: MD5{timestamp + clientId + appKey + appSecret}
timestamp:毫秒时间戳
clientId:由后端生成
**在生产环境中,你需要自行部署服务器签发 sign**
### 对话(Conversation)
用户登录之后,与其他人(client)进行消息沟通,即为开启了一个「对话(Conversation)」。在即时通讯服务中,「对话」包含了沟通的用户群体(成员),也是所有消息依托的媒介:消息都是由某一个 Client 发往一个「对话」。终端用户在开始聊天之前,需要先创建或者加入一个对话,然后再邀请其他人进来(可选),之后所有参与者在这个对话内进行交流。
### 已读/已接收
**已接收**为客户端接收到该消息,已经保存到本地缓存中,用户还未打开会话查看该条消息的情况. 服务器将不会再次下发已接收状态的离线消息,未接收的消息会在拉取离线消息时返回
**已读**为客户端已经查看该消息
## 业务场景的需求
在解释对话类型之前,我们先列举一下即时通讯可能的使用场景。
- 单聊/私聊
就是两个 Client 之间的对话,公开与否(能否让其他人看到这个对话存在)由应用层自己控制。通常的业务场景里它是私密的,并且加入新的成员之后,会切换成新的群聊(当然,也可以依然不离开当前对话,这一点还是由应用层来决定), 可选两个client之间只能存在一个会话。
- 群聊
就是两个(含)以上 Client 之间的对话,通常可以添加和删除成员,并且会赋予群聊一个名字,例如「家人群」、「朋友群」、「部门同事群」等等。随着成员的减少,群聊也可能只有两个甚至一个成员(成员的多少并不是区分群聊和单聊的关键)。群聊能否公开(譬如支持名字搜索),由应用自己决定。
## 客户端连接流程
![wc_im_client连接流程-Page-1](https://tva1.sinaimg.cn/large/008i3skNly1gvv3zjcuajj30zt0u0div.jpg)
1. appKey, appSecret为蔚可云下发给客户方安全保护密钥,不建议保存在前端客户端;
2. 第三方应用服务端需提供获取sign的接口, sign 由MD5{timestamp + clientId + appKey + appSecret},其中clientId由后端生成;
3. 前端拿到sign后,调用验证sign接口进行获取token;
4. websocket连接初始化需要带上token即可连接成功;
5. sign 需要在你的应用服务端生成;
### 第三方应用后端生成sign接口示例
本文展示如何在服务端部署一个 sign 生成器。
**java**示例代码 ,供客户应用后端参考
```java
import org.springframework.util.DigestUtils;
private void getSign(String timestamp, String clientId, String appKey, String appSecret) {
String data = timestamp + clientId + appKey + appSecret;
String sign = DigestUtils.md5DigestAsHex(data.getBytes());
}
public static void main(String[] args) {
String clientId = "client_123123";
String appKey = "elLwpel1gWCHDqZy";
String appSecret = "68809bb5a9077a83631aeb0b17b5965d6b2302faf2ab3737";
String timestamp = String.valueOf(new Date().getTime());
getSign(timestamp, clientId, appKey, appSecret);
}
```
第三方应用后端**必须**要响应给前端的参数:
```json
{
"timestamp": "1628838135066",
"clientId": "client_3334444",
"appKey": "D13ug9jsWbJbeVx1",
"sign": "c15a886fe4114dba2c8f078369e6bec9"
}
```
# wecloud-im 视频通话云对接文档
# wecloud-im 视频通话云对接文档
[TOC]
## 产品概述
集成视频 SDK,实现高清流畅视频通话。
视频通话 SDK 可实现一对一视频通话,同时具备纯语音通话和视频通话功能。
**即将推出多对多视频通话**
## 使用场景
##### 视频聊天
支持 1 对 1 视频通话,适用于视频聊天、视频客服、远程医疗、金融双录、远程定损等场景
##### 在线教育
视频面对面教学,真实还原线下教学场景,支持 1v1 教学
##### 视频客服
支持 1v1 专属 VIP 视频客服,助力用户服务升级,提供更优质的服务体验
**即将推出多对多视频通话**
## 文档描述
此文档为一对一音视频通话技术对接文档
由于视频通话基于wecloud-im聊天服务,**对接视频通话前,需要先对接wecloud-im聊天服务**
## 核心概念说明
### 频道
两个client加入到同一个"频道"进行音视频通话
...@@ -45,8 +45,16 @@ public class IndexController { ...@@ -45,8 +45,16 @@ public class IndexController {
* SwaggerUI * SwaggerUI
*/ */
@GetMapping("/docs") @GetMapping("/docs")
public String swagger() { public String docs() {
return "redirect:/swagger-ui.html"; return "redirect:/swagger-ui.html";
} }
// /**
// * SwaggerUI
// */
// @GetMapping("/websocketDocs")
// public String websocket() {
// return "redirect:/Wecloud-IM-Websocket-Docs.html";
// }
} }
...@@ -4,13 +4,11 @@ String appKey = "QNtP3EjtLw26ekt0"; ...@@ -4,13 +4,11 @@ String appKey = "QNtP3EjtLw26ekt0";
String appSecret = "a5e619003868258e0f7c5b5821ea00fb6b2302faf2ab3737"; String appSecret = "a5e619003868258e0f7c5b5821ea00fb6b2302faf2ab3737";
-- --
aaaaa1 clientA1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFhYWFhMSIsImlzcyI6IndlY2xvdWRfaW0iLCJhcHBLZXkiOiJRTnRQM0VqdEx3MjZla3QwIiwiZXhwIjoxNjc5MjU0MjAzLCJpYXQiOjE2MjkyNzUxOTUsImp0aSI6IjEwNTM0N2YxOGUyYzQ4MzY4ZmQ2OTZjM2Q5NWZhZWZiIn0.kD7eKKQdxOnE8pKGyvtup-xq-JV6GI2qhd6_oUBRl2A
-- --
aaaaa2 aaaaa2
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFhYWFhMiIsImlzcyI6IndlY2xvdWRfaW0iLCJhcHBLZXkiOiJRTnRQM0VqdEx3MjZla3QwIiwiZXhwIjoxNjc5MjU0MjQ3LCJpYXQiOjE2MjkyNzUyMzksImp0aSI6IjM5NjUwZjZiMzgwYTRkNjFhNzIxYzFmNzQyMjRhMjA1In0.39hQkadKFIXhXJbIfIvsMcLa5YccQF21kP9Wh13qACY
-- --
...@@ -19,9 +17,9 @@ aaaaa3 ...@@ -19,9 +17,9 @@ aaaaa3
-- --
-- 会话 ## 会话
1邀请2 1邀请2
1427910060675305472 1447818154184151040
--群聊发送文本------- --群聊发送文本-------
{ {
...@@ -29,7 +27,7 @@ aaaaa3 ...@@ -29,7 +27,7 @@ aaaaa3
"cmd":1, "cmd":1,
"data":{ "data":{
"diyAbcd":"aaaa自已定2义字段的值", "diyAbcd":"aaaa自已定2义字段的值",
"toConversation":1427910060675305472, "toConversation":1447818154184151040,
"type":-1, "type":-1,
"text":"发给12312123213这是一123个纯文本消息,发给12312123213这是一123个纯文本消息发给12312123213这是一123个纯文本消息", "text":"发给12312123213这是一123个纯文本消息,发给12312123213这是一123个纯文本消息发给12312123213这是一123个纯文本消息",
"attrs":{ "attrs":{
...@@ -78,13 +76,13 @@ load-blance: ...@@ -78,13 +76,13 @@ load-blance:
server-type: Local server-type: Local
2021年09月27日17:00:50部署国内IM集成版 ## 2021年09月27日17:00:50部署国内IM集成版
部署1.3 版本 部署1.3 版本
| weikeyun_imapi | 121.37.208.9 | 国内IM集成版 weikeyun_imapi | 121.37.208.9 | 国内IM集成版
docker run -p 6379:6379 -d --restart=always --name redis6 -v $PWD/dockerData/redis6:/data redis:6 --appendonly yes --requirepass "axT8knPN5hAP" docker run -p 6379:6379 -d --restart=always --name redis6 -v $PWD/dockerData/redis6:/data redis:6 --appendonly yes --requirepass "axT8knPN5hAP"
##mysql5.7(如果本地不开,可以连上测试环境) ## mysql5.7(如果本地不开,可以连上测试环境)
### docker 安装mysql ### docker 安装mysql
docker run -p 3306:3306 --name mysql57 -v $PWD/dockerData/mysql57/data:/var/lib/mysql --restart always -e MYSQL_ROOT_PASSWORD=JH86uc53r8Ca -d mysql:5.7 docker run -p 3306:3306 --name mysql57 -v $PWD/dockerData/mysql57/data:/var/lib/mysql --restart always -e MYSQL_ROOT_PASSWORD=JH86uc53r8Ca -d mysql:5.7
mysql mysql
...@@ -97,5 +95,229 @@ mysql ...@@ -97,5 +95,229 @@ mysql
port: 6379 port: 6379
utf8mb4_unicode_ci utf8mb4_unicode_ci
建库语句 ## 建库语句
CREATE DATABASE IF NOT EXISTS wecloud_im DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; CREATE DATABASE IF NOT EXISTS wecloud_im DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
## 单人RTC设计思路 , 2021年10月08日 周五
1. 引入"频道RtcChannel"概念, 可以不基于会话发起音视频通话
2. 不在同个会话中的两个client加入到同一个频道进行通话 (可配置是否两个client必须在同一个会话才能发起音视频通话)
3. 目前频道只支持两个client,进行通话的两端必须先加入到同一个"频道",所有的指令都在频道内进行转发
4. 一个频道可以由通话发起者绑定到会话ID,"挂断","未接听"等状态会同步到会话, 未绑定将不同步到会话(可选)
5. 连接websocket时带上client类型, 如web,安卓,ios
client需要监听频道内 状态更新(房间断开 .挂断)、用户状态更新(用户加入, 用户退出)、流状态更新(切换音频 切换视频)
# RTC接口
## 创建频道
* 请求参数
接收者id (数组)
扩展参数(Object)
绑定的聊天会话ID(可选)
* 响应参数 :
生成的唯一频道id
## 维护判断信息
rci
rtc_channel_info:10001
=
{
"owner" = "123123123",
"createTimestamp","123123"
}
### 维护用户当前在线的频道ID
new Map<String,Long> map
map.put("clientA",10001)
map.put("clientB",10001)
map.put("clientC",10002)
map.put("clientD",10003)
redis Key:
user_join_channel = ujc
rcu:clientA = 10001
rcu:clientB = 10001
rcu:clientC = 10002
rcu:clientD = 10003
### 维护频道中存在的用户
Map<Long,List<String>> map
new List<String> list
list.add("clientA")
list.add("clientB")
map.put(10001,list)
redis Key (set 集合):
rtc_channel_users = rcu
rcu:10001 = clientA , clientB
rcu:10002 = clientC
rcu:10003 = clientD
rtc官网demo
# 发起人
## http 获取ice地址
https://appr.tc/v1alpha/iceconfig?key=
返回{"iceServers": [{"urls": ["stun:turn2.l.google.com"]}]}
## http 加入房间
https://appr.tc/join/790192954
返回:
is_initiator: "true"
ice_server_transports: ""
ice_server_url: "https://appr.tc/v1alpha/iceconfig?key="
client_id: "12876268"
## http上传offer SDP
https://appr.tc/message/790192954/12876268
请求
{
"sdp": "v=0\r\no=- 6939265646429638945 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS 135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 110 112 113 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:O094\r\na=ice-pwd:R+IHhU9wwCuIDR8pd/hnmMco\r\na=ice-options:trickle\r\na=fingerprint:sha-256 82:CF:2C:D2:7C:8D:A2:96:29:B7:C1:87:CE:39:16:FD:ED:12:99:AF:3B:73:B6:3D:5E:03:DD:1E:99:0F:3C:B1\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 52c233e3-d82f-4ad4-baaa-014d7bfb0deb\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:112 telephone-event/32000\r\na=rtpmap:113 telephone-event/16000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:109924000 cname:VEO/rU2hhPHuuPu0\r\na=ssrc:109924000 msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 52c233e3-d82f-4ad4-baaa-014d7bfb0deb\r\na=ssrc:109924000 mslabel:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\r\na=ssrc:109924000 label:52c233e3-d82f-4ad4-baaa-014d7bfb0deb\r\nm=video 9 UDP/TLS/RTP/SAVPF 98 100 96 97 99 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:O094\r\na=ice-pwd:R+IHhU9wwCuIDR8pd/hnmMco\r\na=ice-options:trickle\r\na=fingerprint:sha-256 82:CF:2C:D2:7C:8D:A2:96:29:B7:C1:87:CE:39:16:FD:ED:12:99:AF:3B:73:B6:3D:5E:03:DD:1E:99:0F:3C:B1\r\na=setup:actpass\r\na=mid:1\r\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:13 urn:3gpp:video-orientation\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 profile-id=0\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 VP9/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 profile-id=2\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:102 H264/90000\r\na=rtcp-fb:102 goog-remb\r\na=rtcp-fb:102 transport-cc\r\na=rtcp-fb:102 ccm fir\r\na=rtcp-fb:102 nack\r\na=rtcp-fb:102 nack pli\r\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:121 rtx/90000\r\na=fmtp:121 apt=102\r\na=rtpmap:127 H264/90000\r\na=rtcp-fb:127 goog-remb\r\na=rtcp-fb:127 transport-cc\r\na=rtcp-fb:127 ccm fir\r\na=rtcp-fb:127 nack\r\na=rtcp-fb:127 nack pli\r\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\r\na=rtpmap:120 rtx/90000\r\na=fmtp:120 apt=127\r\na=rtpmap:125 H264/90000\r\na=rtcp-fb:125 goog-remb\r\na=rtcp-fb:125 transport-cc\r\na=rtcp-fb:125 ccm fir\r\na=rtcp-fb:125 nack\r\na=rtcp-fb:125 nack pli\r\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:107 rtx/90000\r\na=fmtp:107 apt=125\r\na=rtpmap:108 H264/90000\r\na=rtcp-fb:108 goog-remb\r\na=rtcp-fb:108 transport-cc\r\na=rtcp-fb:108 ccm fir\r\na=rtcp-fb:108 nack\r\na=rtcp-fb:108 nack pli\r\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\r\na=rtpmap:109 rtx/90000\r\na=fmtp:109 apt=108\r\na=rtpmap:35 AV1X/90000\r\na=rtcp-fb:35 goog-remb\r\na=rtcp-fb:35 transport-cc\r\na=rtcp-fb:35 ccm fir\r\na=rtcp-fb:35 nack\r\na=rtcp-fb:35 nack pli\r\na=rtpmap:36 rtx/90000\r\na=fmtp:36 apt=35\r\na=rtpmap:124 H264/90000\r\na=rtcp-fb:124 goog-remb\r\na=rtcp-fb:124 transport-cc\r\na=rtcp-fb:124 ccm fir\r\na=rtcp-fb:124 nack\r\na=rtcp-fb:124 nack pli\r\na=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032\r\na=rtpmap:119 rtx/90000\r\na=fmtp:119 apt=124\r\na=rtpmap:123 H264/90000\r\na=rtcp-fb:123 goog-remb\r\na=rtcp-fb:123 transport-cc\r\na=rtcp-fb:123 ccm fir\r\na=rtcp-fb:123 nack\r\na=rtcp-fb:123 nack pli\r\na=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032\r\na=rtpmap:118 rtx/90000\r\na=fmtp:118 apt=123\r\na=rtpmap:114 red/90000\r\na=rtpmap:115 rtx/90000\r\na=fmtp:115 apt=114\r\na=rtpmap:116 ulpfec/90000\r\na=ssrc-group:FID 1330070973 2207934575\r\na=ssrc:1330070973 cname:VEO/rU2hhPHuuPu0\r\na=ssrc:1330070973 msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\r\na=ssrc:1330070973 mslabel:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\r\na=ssrc:1330070973 label:745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\r\na=ssrc:2207934575 cname:VEO/rU2hhPHuuPu0\r\na=ssrc:2207934575 msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\r\na=ssrc:2207934575 mslabel:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\r\na=ssrc:2207934575 label:745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\r\n",
"type": "offer"
}
## http上传candidate
https://appr.tc/message/790192954/12876268
请求
{
"type": "candidate",
"label": 0,
"id": "0",
"candidate": "candidate:2199032595 1 udp 2122260223 192.168.1.89 50421 typ host generation 0 ufrag O094 network-id 1 network-cost 10"
}
上行 先加入到ws频道
{"cmd":"register","roomid":"065395880","clientid":"12440330"}
下行 另一端SDP应答
{"msg":"{\"sdp\":\"v=0\\r\\no=- 4129310300810400747 2 IN IP4 127.0.0.1\\r\\ns=-\\r\\nt=0 0\\r\\na=group:BUNDLE 0 1\\r\\na=extmap-allow-mixed\\r\\na=msid-semantic: WMS BVdxzDjOlGOX5CJe8d6QD5STXJMRLx4YYlPx\\r\\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 110 112 113 126\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:npgH\\r\\na=ice-pwd:Xg7PFj5k3skAzKRQszRTFZNS\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 30:89:F0:F6:C7:4E:40:AD:CD:8F:8C:B5:96:64:D9:91:99:5E:92:C7:82:F3:7D:E4:43:B4:C1:C1:E8:C6:9B:09\\r\\na=setup:active\\r\\na=mid:0\\r\\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\\r\\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\\r\\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendrecv\\r\\na=msid:BVdxzDjOlGOX5CJe8d6QD5STXJMRLx4YYlPx 18e6bcf6-4d4b-4279-843b-cf015b1b836a\\r\\na=rtcp-mux\\r\\na=rtpmap:111 opus/48000/2\\r\\na=rtcp-fb:111 transport-cc\\r\\na=fmtp:111 minptime=10;useinbandfec=1\\r\\na=rtpmap:103 ISAC/16000\\r\\na=rtpmap:104 ISAC/32000\\r\\na=rtpmap:9 G722/8000\\r\\na=rtpmap:0 PCMU/8000\\r\\na=rtpmap:8 PCMA/8000\\r\\na=rtpmap:110 telephone-event/48000\\r\\na=rtpmap:112 telephone-event/32000\\r\\na=rtpmap:113 telephone-event/16000\\r\\na=rtpmap:126 telephone-event/8000\\r\\na=ssrc:1526334845 cname:QDAIpojrEkvKVrx7\\r\\nm=video 9 UDP/TLS/RTP/SAVPF 98 100 96 97 99 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:npgH\\r\\na=ice-pwd:Xg7PFj5k3skAzKRQszRTFZNS\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 30:89:F0:F6:C7:4E:40:AD:CD:8F:8C:B5:96:64:D9:91:99:5E:92:C7:82:F3:7D:E4:43:B4:C1:C1:E8:C6:9B:09\\r\\na=setup:active\\r\\na=mid:1\\r\\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\\r\\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\\r\\na=extmap:13 urn:3gpp:video-orientation\\r\\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\\r\\na=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\\r\\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\\r\\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendrecv\\r\\na=msid:BVdxzDjOlGOX5CJe8d6QD5STXJMRLx4YYlPx ddd008ed-cb04-49c5-a838-d3777061c416\\r\\na=rtcp-mux\\r\\na=rtcp-rsize\\r\\na=rtpmap:98 VP9/90000\\r\\na=rtcp-fb:98 goog-remb\\r\\na=rtcp-fb:98 transport-cc\\r\\na=rtcp-fb:98 ccm fir\\r\\na=rtcp-fb:98 nack\\r\\na=rtcp-fb:98 nack pli\\r\\na=fmtp:98 profile-id=0\\r\\na=rtpmap:100 VP9/90000\\r\\na=rtcp-fb:100 goog-remb\\r\\na=rtcp-fb:100 transport-cc\\r\\na=rtcp-fb:100 ccm fir\\r\\na=rtcp-fb:100 nack\\r\\na=rtcp-fb:100 nack pli\\r\\na=fmtp:100 profile-id=2\\r\\na=rtpmap:96 VP8/90000\\r\\na=rtcp-fb:96 goog-remb\\r\\na=rtcp-fb:96 transport-cc\\r\\na=rtcp-fb:96 ccm fir\\r\\na=rtcp-fb:96 nack\\r\\na=rtcp-fb:96 nack pli\\r\\na=rtpmap:97 rtx/90000\\r\\na=fmtp:97 apt=96\\r\\na=rtpmap:99 rtx/90000\\r\\na=fmtp:99 apt=98\\r\\na=rtpmap:101 rtx/90000\\r\\na=fmtp:101 apt=100\\r\\na=rtpmap:102 H264/90000\\r\\na=rtcp-fb:102 goog-remb\\r\\na=rtcp-fb:102 transport-cc\\r\\na=rtcp-fb:102 ccm fir\\r\\na=rtcp-fb:102 nack\\r\\na=rtcp-fb:102 nack pli\\r\\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\\r\\na=rtpmap:121 rtx/90000\\r\\na=fmtp:121 apt=102\\r\\na=rtpmap:127 H264/90000\\r\\na=rtcp-fb:127 goog-remb\\r\\na=rtcp-fb:127 transport-cc\\r\\na=rtcp-fb:127 ccm fir\\r\\na=rtcp-fb:127 nack\\r\\na=rtcp-fb:127 nack pli\\r\\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\\r\\na=rtpmap:120 rtx/90000\\r\\na=fmtp:120 apt=127\\r\\na=rtpmap:125 H264/90000\\r\\na=rtcp-fb:125 goog-remb\\r\\na=rtcp-fb:125 transport-cc\\r\\na=rtcp-fb:125 ccm fir\\r\\na=rtcp-fb:125 nack\\r\\na=rtcp-fb:125 nack pli\\r\\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\\r\\na=rtpmap:107 rtx/90000\\r\\na=fmtp:107 apt=125\\r\\na=rtpmap:108 H264/90000\\r\\na=rtcp-fb:108 goog-remb\\r\\na=rtcp-fb:108 transport-cc\\r\\na=rtcp-fb:108 ccm fir\\r\\na=rtcp-fb:108 nack\\r\\na=rtcp-fb:108 nack pli\\r\\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\\r\\na=rtpmap:109 rtx/90000\\r\\na=fmtp:109 apt=108\\r\\na=rtpmap:35 AV1X/90000\\r\\na=rtcp-fb:35 goog-remb\\r\\na=rtcp-fb:35 transport-cc\\r\\na=rtcp-fb:35 ccm fir\\r\\na=rtcp-fb:35 nack\\r\\na=rtcp-fb:35 nack pli\\r\\na=rtpmap:36 rtx/90000\\r\\na=fmtp:36 apt=35\\r\\na=rtpmap:124 H264/90000\\r\\na=rtcp-fb:124 goog-remb\\r\\na=rtcp-fb:124 transport-cc\\r\\na=rtcp-fb:124 ccm fir\\r\\na=rtcp-fb:124 nack\\r\\na=rtcp-fb:124 nack pli\\r\\na=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f\\r\\na=rtpmap:119 rtx/90000\\r\\na=fmtp:119 apt=124\\r\\na=rtpmap:123 H264/90000\\r\\na=rtcp-fb:123 goog-remb\\r\\na=rtcp-fb:123 transport-cc\\r\\na=rtcp-fb:123 ccm fir\\r\\na=rtcp-fb:123 nack\\r\\na=rtcp-fb:123 nack pli\\r\\na=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\\r\\na=rtpmap:118 rtx/90000\\r\\na=fmtp:118 apt=123\\r\\na=rtpmap:114 red/90000\\r\\na=rtpmap:115 rtx/90000\\r\\na=fmtp:115 apt=114\\r\\na=rtpmap:116 ulpfec/90000\\r\\na=ssrc-group:FID 2674131219 2077425702\\r\\na=ssrc:2674131219 cname:QDAIpojrEkvKVrx7\\r\\na=ssrc:2077425702 cname:QDAIpojrEkvKVrx7\\r\\n\",\"type\":\"answer\"}","error":""}
下行 另一端candidate信息
{"msg":"{\"type\":\"candidate\",\"label\":0,\"id\":\"0\",\"candidate\":\"candidate:4258379142 1 udp 2122260223 192.168.1.250 55712 typ host generation 0 ufrag npgH network-id 1\"}","error":""}
接收方
http 加入房间
https://appr.tc/join/790192954
响应已加入到房间的client的sdp与candidate
{
"params": {
"is_initiator": "false",
"room_link": "https://appr.tc/r/790192954",
"version_info": "{\"gitHash\": \"06b18b54af995bab9e16c2648ddb7edbbe553541\", \"branch\": \"master\", \"time\": \"Mon Sep 14 17:52:24 2020 +0200\"}",
"messages": [
"{\"sdp\":\"v=0\\r\\no=- 6939265646429638945 2 IN IP4 127.0.0.1\\r\\ns=-\\r\\nt=0 0\\r\\na=group:BUNDLE 0 1\\r\\na=extmap-allow-mixed\\r\\na=msid-semantic: WMS 135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\\r\\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 110 112 113 126\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:O094\\r\\na=ice-pwd:R+IHhU9wwCuIDR8pd/hnmMco\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 82:CF:2C:D2:7C:8D:A2:96:29:B7:C1:87:CE:39:16:FD:ED:12:99:AF:3B:73:B6:3D:5E:03:DD:1E:99:0F:3C:B1\\r\\na=setup:actpass\\r\\na=mid:0\\r\\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\\r\\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\\r\\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendrecv\\r\\na=msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 52c233e3-d82f-4ad4-baaa-014d7bfb0deb\\r\\na=rtcp-mux\\r\\na=rtpmap:111 opus/48000/2\\r\\na=rtcp-fb:111 transport-cc\\r\\na=fmtp:111 minptime=10;useinbandfec=1\\r\\na=rtpmap:103 ISAC/16000\\r\\na=rtpmap:104 ISAC/32000\\r\\na=rtpmap:9 G722/8000\\r\\na=rtpmap:0 PCMU/8000\\r\\na=rtpmap:8 PCMA/8000\\r\\na=rtpmap:110 telephone-event/48000\\r\\na=rtpmap:112 telephone-event/32000\\r\\na=rtpmap:113 telephone-event/16000\\r\\na=rtpmap:126 telephone-event/8000\\r\\na=ssrc:109924000 cname:VEO/rU2hhPHuuPu0\\r\\na=ssrc:109924000 msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 52c233e3-d82f-4ad4-baaa-014d7bfb0deb\\r\\na=ssrc:109924000 mslabel:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\\r\\na=ssrc:109924000 label:52c233e3-d82f-4ad4-baaa-014d7bfb0deb\\r\\nm=video 9 UDP/TLS/RTP/SAVPF 98 100 96 97 99 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:O094\\r\\na=ice-pwd:R+IHhU9wwCuIDR8pd/hnmMco\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 82:CF:2C:D2:7C:8D:A2:96:29:B7:C1:87:CE:39:16:FD:ED:12:99:AF:3B:73:B6:3D:5E:03:DD:1E:99:0F:3C:B1\\r\\na=setup:actpass\\r\\na=mid:1\\r\\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\\r\\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\\r\\na=extmap:13 urn:3gpp:video-orientation\\r\\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\\r\\na=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\\r\\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\\r\\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendrecv\\r\\na=msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\\r\\na=rtcp-mux\\r\\na=rtcp-rsize\\r\\na=rtpmap:96 VP8/90000\\r\\na=rtcp-fb:96 goog-remb\\r\\na=rtcp-fb:96 transport-cc\\r\\na=rtcp-fb:96 ccm fir\\r\\na=rtcp-fb:96 nack\\r\\na=rtcp-fb:96 nack pli\\r\\na=rtpmap:97 rtx/90000\\r\\na=fmtp:97 apt=96\\r\\na=rtpmap:98 VP9/90000\\r\\na=rtcp-fb:98 goog-remb\\r\\na=rtcp-fb:98 transport-cc\\r\\na=rtcp-fb:98 ccm fir\\r\\na=rtcp-fb:98 nack\\r\\na=rtcp-fb:98 nack pli\\r\\na=fmtp:98 profile-id=0\\r\\na=rtpmap:99 rtx/90000\\r\\na=fmtp:99 apt=98\\r\\na=rtpmap:100 VP9/90000\\r\\na=rtcp-fb:100 goog-remb\\r\\na=rtcp-fb:100 transport-cc\\r\\na=rtcp-fb:100 ccm fir\\r\\na=rtcp-fb:100 nack\\r\\na=rtcp-fb:100 nack pli\\r\\na=fmtp:100 profile-id=2\\r\\na=rtpmap:101 rtx/90000\\r\\na=fmtp:101 apt=100\\r\\na=rtpmap:102 H264/90000\\r\\na=rtcp-fb:102 goog-remb\\r\\na=rtcp-fb:102 transport-cc\\r\\na=rtcp-fb:102 ccm fir\\r\\na=rtcp-fb:102 nack\\r\\na=rtcp-fb:102 nack pli\\r\\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\\r\\na=rtpmap:121 rtx/90000\\r\\na=fmtp:121 apt=102\\r\\na=rtpmap:127 H264/90000\\r\\na=rtcp-fb:127 goog-remb\\r\\na=rtcp-fb:127 transport-cc\\r\\na=rtcp-fb:127 ccm fir\\r\\na=rtcp-fb:127 nack\\r\\na=rtcp-fb:127 nack pli\\r\\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\\r\\na=rtpmap:120 rtx/90000\\r\\na=fmtp:120 apt=127\\r\\na=rtpmap:125 H264/90000\\r\\na=rtcp-fb:125 goog-remb\\r\\na=rtcp-fb:125 transport-cc\\r\\na=rtcp-fb:125 ccm fir\\r\\na=rtcp-fb:125 nack\\r\\na=rtcp-fb:125 nack pli\\r\\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\\r\\na=rtpmap:107 rtx/90000\\r\\na=fmtp:107 apt=125\\r\\na=rtpmap:108 H264/90000\\r\\na=rtcp-fb:108 goog-remb\\r\\na=rtcp-fb:108 transport-cc\\r\\na=rtcp-fb:108 ccm fir\\r\\na=rtcp-fb:108 nack\\r\\na=rtcp-fb:108 nack pli\\r\\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\\r\\na=rtpmap:109 rtx/90000\\r\\na=fmtp:109 apt=108\\r\\na=rtpmap:35 AV1X/90000\\r\\na=rtcp-fb:35 goog-remb\\r\\na=rtcp-fb:35 transport-cc\\r\\na=rtcp-fb:35 ccm fir\\r\\na=rtcp-fb:35 nack\\r\\na=rtcp-fb:35 nack pli\\r\\na=rtpmap:36 rtx/90000\\r\\na=fmtp:36 apt=35\\r\\na=rtpmap:124 H264/90000\\r\\na=rtcp-fb:124 goog-remb\\r\\na=rtcp-fb:124 transport-cc\\r\\na=rtcp-fb:124 ccm fir\\r\\na=rtcp-fb:124 nack\\r\\na=rtcp-fb:124 nack pli\\r\\na=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032\\r\\na=rtpmap:119 rtx/90000\\r\\na=fmtp:119 apt=124\\r\\na=rtpmap:123 H264/90000\\r\\na=rtcp-fb:123 goog-remb\\r\\na=rtcp-fb:123 transport-cc\\r\\na=rtcp-fb:123 ccm fir\\r\\na=rtcp-fb:123 nack\\r\\na=rtcp-fb:123 nack pli\\r\\na=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032\\r\\na=rtpmap:118 rtx/90000\\r\\na=fmtp:118 apt=123\\r\\na=rtpmap:114 red/90000\\r\\na=rtpmap:115 rtx/90000\\r\\na=fmtp:115 apt=114\\r\\na=rtpmap:116 ulpfec/90000\\r\\na=ssrc-group:FID 1330070973 2207934575\\r\\na=ssrc:1330070973 cname:VEO/rU2hhPHuuPu0\\r\\na=ssrc:1330070973 msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\\r\\na=ssrc:1330070973 mslabel:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\\r\\na=ssrc:1330070973 label:745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\\r\\na=ssrc:2207934575 cname:VEO/rU2hhPHuuPu0\\r\\na=ssrc:2207934575 msid:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz 745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\\r\\na=ssrc:2207934575 mslabel:135gqeiEqFDCSBQvkrFA7ZIXRnW4YyXCWJDz\\r\\na=ssrc:2207934575 label:745e25c1-11b3-4a11-8bcc-0a3f8d80a60a\\r\\n\",\"type\":\"offer\"}",
"{\"type\":\"candidate\",\"label\":0,\"id\":\"0\",\"candidate\":\"candidate:2199032595 1 udp 2122260223 192.168.1.89 50421 typ host generation 0 ufrag O094 network-id 1 network-cost 10\"}"
],
"error_messages": [],
"client_id": "89012080",
"ice_server_transports": "",
"bypass_join_confirmation": "false",
"wss_url": "wss://apprtc-ws.webrtc.org:443/ws",
"media_constraints": "{\"audio\": true, \"video\": {\"optional\": [{\"minWidth\": \"1280\"}, {\"minHeight\": \"720\"}], \"mandatory\": {}}}",
"include_loopback_js": "",
"header_message": "This is a demo of AppRTC and not an official product like Duo or Meet.",
"is_loopback": "false",
"offer_options": "{}",
"pc_constraints": "{\"optional\": []}",
"pc_config": "{\"rtcpMuxPolicy\": \"require\", \"bundlePolicy\": \"max-bundle\", \"iceServers\": []}",
"wss_post_url": "https://apprtc-ws.webrtc.org:443",
"ice_server_url": "https://appr.tc/v1alpha/iceconfig?key=",
"warning_messages": [],
"room_id": "790192954"
},
"result": "SUCCESS"
}
ws
加入ws
{"cmd":"register","roomid":"790192954","clientid":"89012080"}
上传SDP : "type\":\"answer\"
{"cmd":"send","msg":"{\"sdp\":\"v=0\\r\\no=- 5124761144050338825 2 IN IP4 127.0.0.1\\r\\ns=-\\r\\nt=0 0\\r\\na=group:BUNDLE 0 1\\r\\na=extmap-allow-mixed\\r\\na=msid-semantic: WMS PvyHNDA1KG9m7mwiXxuZw0t6jS5gK1slEVtL\\r\\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 110 112 113 126\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:VoGg\\r\\na=ice-pwd:3BDNnB1TRubalgrp84I8qug8\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 89:A4:F5:30:EA:A6:B1:A4:9D:6A:8A:85:8D:B7:FD:C3:B0:A4:05:6B:31:97:F3:19:0E:4C:BD:63:4D:56:3C:AB\\r\\na=setup:active\\r\\na=mid:0\\r\\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\\r\\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\\r\\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendrecv\\r\\na=msid:PvyHNDA1KG9m7mwiXxuZw0t6jS5gK1slEVtL ac72faad-b426-4501-baa0-d1f45e262c3d\\r\\na=rtcp-mux\\r\\na=rtpmap:111 opus/48000/2\\r\\na=rtcp-fb:111 transport-cc\\r\\na=fmtp:111 minptime=10;useinbandfec=1\\r\\na=rtpmap:103 ISAC/16000\\r\\na=rtpmap:104 ISAC/32000\\r\\na=rtpmap:9 G722/8000\\r\\na=rtpmap:0 PCMU/8000\\r\\na=rtpmap:8 PCMA/8000\\r\\na=rtpmap:110 telephone-event/48000\\r\\na=rtpmap:112 telephone-event/32000\\r\\na=rtpmap:113 telephone-event/16000\\r\\na=rtpmap:126 telephone-event/8000\\r\\na=ssrc:4262391127 cname:SbDl2+NlSKabussO\\r\\nm=video 9 UDP/TLS/RTP/SAVPF 98 100 96 97 99 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:VoGg\\r\\na=ice-pwd:3BDNnB1TRubalgrp84I8qug8\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 89:A4:F5:30:EA:A6:B1:A4:9D:6A:8A:85:8D:B7:FD:C3:B0:A4:05:6B:31:97:F3:19:0E:4C:BD:63:4D:56:3C:AB\\r\\na=setup:active\\r\\na=mid:1\\r\\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\\r\\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\\r\\na=extmap:13 urn:3gpp:video-orientation\\r\\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\\r\\na=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\\r\\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\\r\\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendrecv\\r\\na=msid:PvyHNDA1KG9m7mwiXxuZw0t6jS5gK1slEVtL 56012d93-126e-451f-8e18-6c3a60f7a445\\r\\na=rtcp-mux\\r\\na=rtcp-rsize\\r\\na=rtpmap:98 VP9/90000\\r\\na=rtcp-fb:98 goog-remb\\r\\na=rtcp-fb:98 transport-cc\\r\\na=rtcp-fb:98 ccm fir\\r\\na=rtcp-fb:98 nack\\r\\na=rtcp-fb:98 nack pli\\r\\na=fmtp:98 profile-id=0\\r\\na=rtpmap:100 VP9/90000\\r\\na=rtcp-fb:100 goog-remb\\r\\na=rtcp-fb:100 transport-cc\\r\\na=rtcp-fb:100 ccm fir\\r\\na=rtcp-fb:100 nack\\r\\na=rtcp-fb:100 nack pli\\r\\na=fmtp:100 profile-id=2\\r\\na=rtpmap:96 VP8/90000\\r\\na=rtcp-fb:96 goog-remb\\r\\na=rtcp-fb:96 transport-cc\\r\\na=rtcp-fb:96 ccm fir\\r\\na=rtcp-fb:96 nack\\r\\na=rtcp-fb:96 nack pli\\r\\na=rtpmap:97 rtx/90000\\r\\na=fmtp:97 apt=96\\r\\na=rtpmap:99 rtx/90000\\r\\na=fmtp:99 apt=98\\r\\na=rtpmap:101 rtx/90000\\r\\na=fmtp:101 apt=100\\r\\na=rtpmap:102 H264/90000\\r\\na=rtcp-fb:102 goog-remb\\r\\na=rtcp-fb:102 transport-cc\\r\\na=rtcp-fb:102 ccm fir\\r\\na=rtcp-fb:102 nack\\r\\na=rtcp-fb:102 nack pli\\r\\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\\r\\na=rtpmap:121 rtx/90000\\r\\na=fmtp:121 apt=102\\r\\na=rtpmap:127 H264/90000\\r\\na=rtcp-fb:127 goog-remb\\r\\na=rtcp-fb:127 transport-cc\\r\\na=rtcp-fb:127 ccm fir\\r\\na=rtcp-fb:127 nack\\r\\na=rtcp-fb:127 nack pli\\r\\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\\r\\na=rtpmap:120 rtx/90000\\r\\na=fmtp:120 apt=127\\r\\na=rtpmap:125 H264/90000\\r\\na=rtcp-fb:125 goog-remb\\r\\na=rtcp-fb:125 transport-cc\\r\\na=rtcp-fb:125 ccm fir\\r\\na=rtcp-fb:125 nack\\r\\na=rtcp-fb:125 nack pli\\r\\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\\r\\na=rtpmap:107 rtx/90000\\r\\na=fmtp:107 apt=125\\r\\na=rtpmap:108 H264/90000\\r\\na=rtcp-fb:108 goog-remb\\r\\na=rtcp-fb:108 transport-cc\\r\\na=rtcp-fb:108 ccm fir\\r\\na=rtcp-fb:108 nack\\r\\na=rtcp-fb:108 nack pli\\r\\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\\r\\na=rtpmap:109 rtx/90000\\r\\na=fmtp:109 apt=108\\r\\na=rtpmap:35 AV1X/90000\\r\\na=rtcp-fb:35 goog-remb\\r\\na=rtcp-fb:35 transport-cc\\r\\na=rtcp-fb:35 ccm fir\\r\\na=rtcp-fb:35 nack\\r\\na=rtcp-fb:35 nack pli\\r\\na=rtpmap:36 rtx/90000\\r\\na=fmtp:36 apt=35\\r\\na=rtpmap:124 H264/90000\\r\\na=rtcp-fb:124 goog-remb\\r\\na=rtcp-fb:124 transport-cc\\r\\na=rtcp-fb:124 ccm fir\\r\\na=rtcp-fb:124 nack\\r\\na=rtcp-fb:124 nack pli\\r\\na=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f\\r\\na=rtpmap:119 rtx/90000\\r\\na=fmtp:119 apt=124\\r\\na=rtpmap:123 H264/90000\\r\\na=rtcp-fb:123 goog-remb\\r\\na=rtcp-fb:123 transport-cc\\r\\na=rtcp-fb:123 ccm fir\\r\\na=rtcp-fb:123 nack\\r\\na=rtcp-fb:123 nack pli\\r\\na=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\\r\\na=rtpmap:118 rtx/90000\\r\\na=fmtp:118 apt=123\\r\\na=rtpmap:114 red/90000\\r\\na=rtpmap:115 rtx/90000\\r\\na=fmtp:115 apt=114\\r\\na=rtpmap:116 ulpfec/90000\\r\\na=ssrc-group:FID 4287818524 659493843\\r\\na=ssrc:4287818524 cname:SbDl2+NlSKabussO\\r\\na=ssrc:659493843 cname:SbDl2+NlSKabussO\\r\\n\",\"type\":\"answer\"}"}
上传candidate
{"cmd":"send","msg":"{\"type\":\"candidate\",\"label\":0,\"id\":\"0\",\"candidate\":\"candidate:2199032595 1 udp 2122260223 192.168.1.89 33313 typ host generation 0 ufrag VoGg network-id 1 network-cost 10\"}"}
# 发起方断线重连
若房间内只剩一个人 ,下发只剩一个人的通知 并要求仅剩的客户端上传offer的SDP 与candidate, 使原发起方成为"房主"来等待接收方来应答"answer"
原发起方来重连时, 调用join接口,则返回另一端的offer的SDP,同时上传自己的answer SDP
##webrtc测试时https的问题
https://blog.csdn.net/sxc1989/article/details/81291320
在chorme中需要在https的情况下才能使用webrtc,在没有正式证书情况下, 可采用以下几种方式。
mac
用命令行启动chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --unsafely-treat-insecure-origin-as-secure="http://192.168.1.250:5500" --user-data-dir=/Users/giaogiao/Downloads/chr
win
右击chrome游览器图标,选择“属性”,在目标后面加入如下内容:
--unsafely-treat-insecure-origin-as-secure="http://ip:port" --user-data-dir="C:/temp"
1
其中ip、port为服务端地址,–user-data-dir提供一个本地路径即可。
国内IM测试 和正式部署在同一台服务器了. 测试外网启动 传递参数
nohup java -jar -Dnetty.port=9001 -Dserver.port=9002 bootstrap-2.0-test.jar &>/dev/null &
docker run -p 6479:6379 -d --restart=always --name redis6Test redis:6 --appendonly yes --requirepass "axT8knPN5hAP"
docker run -p 3406:3306 --name mysql57Test --restart always -e MYSQL_ROOT_PASSWORD=JH86uc53r8Ca -d mysql:5.7
#资讯项目 本地开发ios的推送证书
MIIMYQIBAzCCDCgGCSqGSIb3DQEHAaCCDBkEggwVMIIMETCCBqcGCSqGSIb3DQEHBqCCBpgwggaUAgEAMIIGjQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIy2AcZWKOEMsCAggAgIIGYOqk/v75aRYVcPj+IAEfsiI0s/HpzKoHpdXNyZOTfYZyJjJYhSFkCubL4K5MrTVqar1wlM07L6vGEDSpzanzyt4ziYidIUwvy7S85xcPmykgZJUnXyUvsWyNOGb79Axs3qBgAMB88e6zYptJD/IAgfoBTQxYnLNzN2kBBaZm+DTVVeiL+ihvRULr3nGw1tkZEYS3Beh474ZziOgXiHI3gfEd4UASjfJ4g8To/CcdeTtjnij55/8aSote6/OSGfqQj/nSS5vINwPttlrTUb1XfnVzqRrK5lq8m/do7jolhoZYmz2J97MYt5mMZZKUDOTkoXc2tTCN/KFjFpsRe/bQEGfzM/YXi6h3n2QA76fMKPppqA5ynRC5uBffV7q5uIu/eN0Iyq1cSrrE8MIhMTEOg3NgGnIi4ME93Nna2loWyI4BgQzX8/xl7+38RPSp90EVbfjnBA0s1x5dwew8uEMutJRZ5kVs6G0b0uqDpBHQezMqeLGgWuNf4HpylnHnuuRYACs6oBP93VqqxXcN6PYg7jS1jzIPEG52Zq8CazPd5CNLCbIBU3mGN+tDh64wK4GlWVSk0N2e8biiPamqj6onWq4i437OZLyrjIVq0vWWjL+Dqa85OjC83XsUBUcEDqb+WtN3o2tonzziJv5cPuYT97yrw6yWknPGK8XHfCoVL0XccZ3DXlb+ANggJ/Hc8CTq5AOZtKT6XQdsbBbOvhJ6ynhkL2F6LVNfwDsBMXh5NJ5gNGP7D7ctY1ki7tLZNe7puVKW4epdIzi8xSTIbC6FRG2Ggjc4bcvmSXbm6eUcZfW/tfBK6miBbWvRuCo/UJDN1NnioZ8G0bUQ2HTe4nM07yvRxpJ08NrkEassNv5FORqKivR1FDfrlQ/ZN+OWQ1PJGYAZyT+DAI3AVv23XI1udn1XOPs5wVjtYWe6265QrKbK1QFBQMT/0oAyHKiAhtVZOMCV87DyTCq9oUBwUq0iZeshj8EbBJy0YHXwwKJigZWGBdIE3AWXyzD+WAVx2g6zd1vRKmHF5LlKAHjCWcemK07k5bMJxqq+rE1TW/YlvB2fBQRQgudqOOw0qWxqILAsyo87DK9QPZW9bzl4VhQ5+dyIqnTs1goxzUMRGqUKyhCTT6R8+JiCqBbSOJeWZ9yljZqrN4l8f7xVg33U/f+m0KuWJlGfpNrg+C9z/akBaeRVuSbXlCwx9DW6kygknxXkmJrxSleIMBbj3iNpxQoTbRNZpHwj34pxoD/eNX2cJXJW0UOVRiiOtFiFKL6rqm1cvJVZELDudF8KVBuJuCb1+j3z6BlhnrVaenTBv3rO5kXQpf8ABFdyZI8C3Lkwg8mMiUGqFniFU6KpZE0BEUkwKPWPTzDpa4TUBrq7IBTn/rtCxSehfr52zH0s112C0zKDo0+yXoo1damuCX7efxZObj7M6/rDE9PS0GY5/GmaJ0d2R9/nNqHiEPRAX0JLiaDOR2lirD2fbwvF3gxpKq2iN/VTA8PAxix0zb3e3dyltbxOCDP1f/5KZiMES/EEn2dGR8rStppTSrVjiL9VPHe6CLCBI+F3gt4Qhy7ym/C8UX8Y6w5Es0zmbqvsIkBqfFtzhUJN31iNl7VDKs3JKYaWmabJTcvKHUn0ULnmKoTjVmC3R86OzVbTOfjfkFaGozkEef+rUxD29VTnQxp0K9fxTMExUTnPnWOlcvY/ye6VWqDPVvRMdl1+OD73O7qPCjCyiyS50mXWdnOQTheQuVMgfGlyZY4DcFqJ+KV9cF1NB+rNCea+SaucieQfQ8Ec1LccECdn7pCQ8CaLnjLWMNz94Hqkne6kMJhNNimAfvqQepOahFHaeEuYGm6CYwPbx/Waa1HOy6gUntHbjV0bqT1g3f9vvjERjxsRbcl5RvQWmNbJCV9BeUWsQgHxywwpnsQuK/InVU2GfzXx8e3RObJfFQR3gX+wuc+9RwX29RNWYJwMvb4YfF8XJTWpoqPdC+qIMGhwnILK6DcG2gfVjNqhnR7AZAup1kvXuYV8C1TArT0/yj/NuWneW9nhpuXDr0/5Y32J2JqlwYqKc0XSQO/j0c4+g0k179uNqwUhI1XZ7xQNZRJ7VAOtOoLuqzLkBn4GknV3dsWPTPfdxc5Pf+Ew1Ma0g9JpDqp4BDGxNwPlK+y3f2q5ioO1JVr1XlvviNEf9DCCBWIGCSqGSIb3DQEHAaCCBVMEggVPMIIFSzCCBUcGCyqGSIb3DQEMCgECoIIE7jCCBOowHAYKKoZIhvcNAQwBAzAOBAiqarX9FUn/8QICCAAEggTI2uEFG3XA3lVTuhYKOLgnQ5Mzsuao8yoY5PA3+4iRrR5/oHU7l6nwUpb6JO3FDnh3PKMR/uJDa8shYDXmYXP7AV0ptuxamvxd9RYt0n+6fu+rgCz6F3Jy2/6B851ZJCHGmWielXETCfTtjQknMbtb4CPNyVGf30hKHGNsPeuTLLm26zzfYC2UqCxrL3sx4Bq33zzMbfkEg08wUSN600DhjgGSHRyV7G41koHTHEcXgqvEjThHlczg3YKSkUHSAO6m5PkEDuwZGZAAopgfqyZpVOH24n3r5Bzh3cMPqcbj8K/rXwa7AMT4W7i7089WbcXjSKzH67nv0XzDtmz0vlov4iQHYP4ZqvN4G4CsQ5aQq/5nbv6ZhWKGBO0mdWAscQ8zWjE+d1Bky5dD3jIIl8aVUnmTZIgF3LVzAh2odL03Kn9OFllLXdkiBfI7wl2mMuXPjs6kdc7IL5+1i8j3LLDtG6M2y70ctIIHB7NC+ElBX3qhyxtYybnPUybMkpRHwPU3Rjk+I+uH/Kvref8MWYVe8JQKCj7pj4mwD0avM1EPwT3a20xdZB3R4W9pQKrOa64q5czaci/gGCPhO056f6bNFtRLC91TXAYKMJhhPm7Rukndi+CeraOOOFki15v5Kt5waP9ThRqjprunzwYScAsmKuria2yVzav3c1CoGyqBGJs9yk75n1j7OxxWFBkgHVn11ceBbawyLROpgR89sC1U8AuDLBt1e2TtoDYv100hZtVxaoWYSnflH2x9YpTYlfJHortvvK7uFumqCMzFRjXAEYYQaNG65cIf5NziWo1j78ZAInzFC5muNYls0xJ7PRootI8VXOdcFMNa1rM92MGK1pnY/YImOgghGS/DN3aBRvfjpXtIGqJk7kvUGkZ+iGZVVnhzajhr6RQkN5MMDXUIj0rzC9+Cws+pT74T4oX4pFAoNAAfK//Ezs3WMPuxgl48eF08XwjbuNIk84zqhxwnbSo7DxlLMYWg7SDN1l6YGcw4Nl1Js8zFZcFjOdXKRF0pgrysPoKb17vD9pJJ3X6kwNnopvKzix6jk0uO6dmePfXYdyCy+qJYxKE+DNPmyDtIILtc0SgS2LLr5760WQjKkdFW9s0itEDWjZ7vIJ96uUH92WZ+netB1OUGMivxesp5OsbgYox0CaT3uKhBGdnseEuLisOtv+RjUDryRvzbmFnBWzBP156OqdL1ua2fnK1Qq2iSbrI92TVw6q//dJTfwF4RWb+TSqNcHg2/m9XP/QTHeYzkVZZF6+S3sSAOBjHL4GSQ5JXEr5v35dd83yUmtcqxmkhDFP/DM6RWKADKpF5/7S/PZjd7QSnoC+du+dD0h41Au0TC6ss21HmJmUPSdXa30LiSUMXG6DkcM1BBLiMrnar+pnD5Gf1dkxoLCPbPEVthfQh9bZ6O/+Dv6FR9CyV8KZTSFsCZDSa7jiFT3JUjXTYuR7nduecPhMeXaysQT07n4rW0ggWYQYzLJXysWbIkmvAmW1DwPddKpNKMhSJQ418D7sv4dYGMPtBtQQvnh7GRPYxf9Zbl4Y1B5GSXpUSyOaoCZLB5kAarIYRQPzhGtUF8BdsTZyeKriTH26kpoo8FQX3Ubr6LpprtuNniph+Lewyx7WZqMUYwHwYJKoZIhvcNAQkUMRIeEAB0AG8AZwBlAHQAaABlAHIwIwYJKoZIhvcNAQkVMRYEFLoJFR1+D/BMQ3ZFPi0BbYECHmfdMDAwITAJBgUrDgMCGgUABBQZHvszpKrhluYRqFPG6pxqkNnU1QQI4/FM0kZzMU8CAQE=
com.xteng.Hibro 123456
#资讯项目 测试外网和正式的ios的推送证书
MIIM0QIBAzCCDJgGCSqGSIb3DQEHAaCCDIkEggyFMIIMgTCCBxcGCSqGSIb3DQEHBqCCBwgwggcEAgEAMIIG/QYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQItY+nWdbS9lACAggAgIIG0Jp6zPx6H7AMsz+rJUOX3ew/FDElADQ5BnoEai8B3G/t92okkJtveab/A8ZXBzkef83ShN+LDFRijK31tWjG7MmZFqBIl1UUr01JLKc2yhG8FwZzqZfDnShjK5DCV4ilILgTQXS7EdFHCacxucYPs+VOpbr4x+o/Ep6vgY9wdMyNc5t4gLgZM1f1zv/yP/vedfEcDHiY/7VVfMXjy3OCMp0i/BLTksxf0DnCKFX5Jimw0Vs//lI4ry4sQw51UqcLX4L4Cfg4keYX5jxyc7vFxl93OlQvpLx7DS1+BJyVYjfMjvjVxllh8EHg2r9KogdExipUbjPNYFnqNdwaz7/9YwdB0OVIeW/i/szIwMnO9hOrc5IAuvvteLwlR46+U9ohpsF6+NJyqaaOj/S7tTAbz0xjNO/YsOF7Qauj6eaBd+rqud3WUlJwDF8ogvD/E2GbW450oMr7chSgYsfP0k4CpNJ5aCc7OTcgPmuu0A8YEjibXTYTP06/9w9qv/gcu2wkQ+oCLuRD9lwWrR1Re88Li9t7EmxRdSuv23a91AfumoPTITZzFg9WFztXpiwwIgMgA18EQv2T4bNxQGEcJ4KDhCaYpqltB0NcnIgUY8ntAo0trClQsD36waab0zl8HZZ8EAwhk1WhCpbHtFk4qs1txPLvyUROms+WlRrU/5BJMoD3yMDkWsb/BWKWq79eW+oOuACu4N4EU8GzjCZfzh6sHyyRtWxcOPVUCyuso7OBizhci0nlNY8fnAM//NSPanSoGOn9DkN5mh9O21/z/S6qXPqfTKA9I5XYZbuC8086wsUt6Df717aXnGk2zAKW2iS5Hdl28DmVCxL+EmNhndo3EdDzt0YLsMG/mI6KS3ai5ZYN/p89h1nJubIAHsdKdS3rJFXwKE3MGq2sfPR+v0kcbarQrpmP/4xE4sS2c8S6OE1vSFvQpX1NI79RBy+B4R6x2dc5O8bXU+A9ZLCZfS+mWaOXY+M2py70a4yt2gzQzx7X/DRQBSu61qheIWKIyFzhzkBjflR/9gZl7lPhM8+Cv8wifpnUH1LPAqpAw8pGn1ScUsRC4iDAZcZI2/jIsy6gj2d8Rr6x5WDYErOhw41z5ULsyTgv531qLIpUzGjJTZFOxnSFgmevUp96Qbr1TDqR+idrwS8k8x2H5sK7o8nXj86ePqL55YPB42A5xbXKLOLbj5pgG0aIJOxg/zO6rdhuL4NAFoqV0IOrTP6/Wx5Mg5O/woGSquPNQ/N/c9onc3b086kiGExMJG3F8FFM/VbdGXZrzGi+Dwe6lvvwuHb//vEw3j3sU2FPu7a4OQ3rRXDW+GWaWeegQ+lpQ+1aqeWjBbvaET7Yidm6w8mXKiIzXoaLuvgn88Te80r+IrarqI8ByUluw/8u0DS2YBitXvGk196xWqHFRVsH5BgdQcBWijtHRDFjstOnNML+cqDMLn3SX17Okd107BNaa/Kt79WyGtB0j0E+x9k2TVtq8jWhwUntwNLk/cC5J0EJxA2Wjo9SCdRfklkZln5fFrhJEbyVNvSHNIKEppvqFWygOvGzG8mNrf2+59bd8Neicn8wG+nwlyJov+WrURDx2zyN4ACTcxo4ySgV1wSjbM5VA5DRKeGV1XtCrFs8CPTSdck389F+qKfz71YTCG7qcbN1KpuO4NGd7U+V7SkX53yXDFTlFWUEVWQLdoe9hNiJFLxB5wRAGZzXHl5B3Gc1NfBkH339lsFtFqB0vn1xvpVDkhb2dvrYz2cg6WqZQOXMnj9t3T1aEslAeexglFFyjVaT9BhOkl9pSOost9FXEoQD97zPEoiineyRhOR6BQcp45Rw9eN5R0o0OKhAm1S0V6qjTW2HByZQn5iaA9NZv7QmtLXNUU8UwpFh9MajzoNN1JG7o6rrjq8j4zIXKiEXzwAAhxUmcNfyS7O2+Bq94l6OucjG9ISDogmEmZtW0k374wDfYXF9jFbp8RPbdy3h0DjC57qLPnfG2/9NOmsxtXF7d1SX4UcOF7X5t8h98qGEpqt9k9lZb4YI1w4cexylGsc6EIm1R0w3XBiTPMkA43JaOEevMt6WpqLsYGhOVqmmtGVhD4DF2MeWr0o57MPYu6E8wJYXq48yTLXyCErjlUkztbPiJXeNXiuE8ica9o2pwtq87jpi06/pIjpC8ifNxsPPBQcP+xQXoaLo0jZKXqPtJgkxSbqzr3WICxVKA0HvLGSFroISmYntiz/G1jurpODBjcSodMTuSfmRnNN69n/bUNHOqkNM2qo8mSwoeUWsvSkv32j8Ect9S+plGUdYqVM2T7PSMQto+QIHcb7FCbXW8xSvwpAwggViBgkqhkiG9w0BBwGgggVTBIIFTzCCBUswggVHBgsqhkiG9w0BDAoBAqCCBO4wggTqMBwGCiqGSIb3DQEMAQMwDgQIsjDKod0zjqMCAggABIIEyNxPAtrOscxvOcgREnB16jK6jgZYSdgh0ufCNwIJyEuf4wRPwhQQGO4D8A93AvGL7J+zzAGNY8XlfksPyE9NREJGEqch3EyIbEPOX07yG/HimG3/pDuh0slyGeDPURLYMnyzzU1hHOr9Jaws2Zmm1ywRy2sWmsKZ3+4MS252EID8NO+icd0vBg8HNLeUmuxp4SNGUUksnUqqhVnA8Oa7wuFzbRVGO8EoNaxjfUCiZ2hJbscDIXwlg2qCTLcVBCiSL9RjzfwVlJleZq3RVyKudtAqwtHEyoSTtEWcQhR2Mlz1O0MIPKmke4Dy2UPI7HgG2gu4klmKxtc7OfCmVryMKGVLz6YirNVZvLOR9N3UAlkYaMLkkpFhZ9FHScXNgBiUqqgGbT42F9wqjlFkeTS6Q3/1olKrLPfvXHpunjXurTLJHtstsQNdzd485+046Dvl0e5MxODuzRgb0ZNedfGVih4XK5wsPbt7Mjjx/alLti5SbPMdnEkQ9McPeKmVkUhlilqlpJtcA7BuUGQ0pLrPggCnFuFRlfNjJts83SeRq5RSgPpdo4N2z4CcCjcenZRlyLX8DQnRgmuBc2JzD6jegKXEjsSb4UporU37PeWBSl+u8X3J7jxxiERGOOC4/OGMkwK4bjlI5YH2wLd81z3z1rhr8zuAHN4/BwUn8+uq75DSxrLzx+oDAjWCYYe9bd22LznsIQsarSbgTeDO1QOMuXoVkD7hgnGJMjo9gnKbDO1IKxMM6s2Vsq1Rtan2TDR6yri9IiA84N9yVosexArCCJPdiCANh7qClf9HTKjSxaCvYvtQ+UzrWuJNYuJnPIbrhcwjirAr1raPJZ5zBtjXEu6dkQ3xzX4bV3sN5ZWpvjYq8Gn77zy/393mSiKhSe0IRX1UssWGRPUhjzVItBPQNg7TPM3eCHoSnQjGoY1TfPPnWxb0EDqUo8zHQRuGeQs5aova4O4GFHsVvQIkP5C5X/BS8sGr3KGhi1EeccQ5sHEpZwiF5N3gd58LnvVMLxNglsFSSFYjgv47JChnp5wsyAb7z7d9S/zBdpJddxq7YE5ibJSMmFSSFZ2wGfejfITi5tOv6vCYjheZVERNvKeGGaXs+6VjSINfjmWoT/WF+fpNRvQ3uq4ms+WIQUuU2AdGoszO6iz2iJJ1V8SvGOGkRY19q1frl0S4h67cJgoi331f9jRbrDii6MFoK/QM2qffuWy36owykv2M7HYyu23dZxaOcBno2pZ8yzBwwD60JjaLPGpnzbnBNYnWNUuu2moyPjWg4RFaO5BENk0+XSR2MqjgA0Cgd2I99U+jq3O4VKf3OI1BVN1OGhYsYDYgA1plzIg+o1K6cfGgFBp7oxBpOlDt895vckJr0ET+who0qO0N40GonvR55jdzj6OQZ5U8wOaHL/z3/TXwbkFhZdJ+cQdkbp9dm0w/CBkLA4Fa49LSDIGw3MppD1z3bN6NbTOPH+JlZEY1/lhjGcB6b3bwiBqLbmxMjpws6Xr8VZkGO20MU62N7d9TUauPspl7pTcX/5tw9kG7HjtOlC9oOz9MAE35TFc9KFBYwPAvPwGVbVbPZjvuN2q52MJ+awzdB5gpUzjVRKalitqeXMxYe90kCdGchrJ+Vxbs7DFGMB8GCSqGSIb3DQEJFDESHhAAdABvAGcAZQB0AGgAZQByMCMGCSqGSIb3DQEJFTEWBBS6CRUdfg/wTEN2RT4tAW2BAh5n3TAwMCEwCQYFKw4DAhoFAAQUedQsO35lNW0UCNn4kLs41JDiiTAECPTWCpHS94LxAgEB
com.xteng.Hibro 123456
国内IM集成版
正式环境
121.37.208.9
域名
测试外网
用探路者原来的空闲服务器
探路者_测试_华为_121.37.234.35
域名:
imapitest.wecloud.cn
web示例:
imwebtest.wecloud.cn
1. 增加客户端api: 查询会话成员列表, 字段: client id ,client头像, client的拓展字段,
在该会话的会话成员拓展字段,在该会话的昵称
5. 增加客户端api: 添加或修改client的主昵称
6. 获取token接口 要返回在im系统中的头像和昵称
2.增加客户端api: 添加或修改 client的拓展字段
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment