Browse Source

群聊功能开发中

master
xie.bx 3 years ago
parent
commit
dc361f21fa
  1. 8
      im-platform/src/main/java/com/lx/implatform/controller/GroupController.java
  2. 2
      im-platform/src/main/java/com/lx/implatform/service/IGroupService.java
  3. 5
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java
  4. 51
      im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java
  5. 13
      im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java
  6. 12
      im-ui/src/components/chat/ChatGroup.vue
  7. 3
      im-ui/src/components/chat/ChatPrivate.vue
  8. 42
      im-ui/src/components/common/HeadImage.vue
  9. 2
      im-ui/src/components/friend/AddFriend.vue
  10. 8
      im-ui/src/components/group/AddGroupMember.vue
  11. 26
      im-ui/src/components/group/GroupMember.vue
  12. 16
      im-ui/src/view/Chat.vue
  13. 42
      im-ui/src/view/Friend.vue
  14. 62
      im-ui/src/view/Group.vue

8
im-platform/src/main/java/com/lx/implatform/controller/GroupController.java

@ -79,5 +79,13 @@ public class GroupController {
return ResultUtils.success();
}
@ApiOperation(value = "踢出群聊",notes="将用户踢出群聊")
@DeleteMapping("/kick/{groupId}")
public Result<GroupVO> kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId,
@NotNull(message = "用户id不能为空") @RequestParam Long userId){
groupService.kickGroup(groupId,userId);
return ResultUtils.success();
}
}

2
im-platform/src/main/java/com/lx/implatform/service/IGroupService.java

@ -27,6 +27,8 @@ public interface IGroupService extends IService<Group> {
void quitGroup(Long groupId);
void kickGroup(Long groupId,Long userId);
List<GroupVO> findGroups();
void invite(GroupInviteVO vo);

5
im-platform/src/main/java/com/lx/implatform/service/impl/GroupMessageServiceImpl.java

@ -65,10 +65,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
}
userIds.parallelStream().forEach(id->{
if(id == userId){
// 自己不需要推送给自己
return;
}
String key = RedisKey.IM_USER_SERVER_ID + id;
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
if(serverId != null){

51
im-platform/src/main/java/com/lx/implatform/service/impl/GroupServiceImpl.java

@ -23,14 +23,11 @@ import com.lx.implatform.vo.GroupMemberVO;
import com.lx.implatform.vo.GroupVO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Member;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -80,6 +77,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
return vo;
}
/**
* 修改群聊信息
*
@ -109,6 +107,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
return vo;
}
/**
* 删除群聊
*
@ -121,9 +120,6 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public void deleteGroup(Long groupId) {
UserSession session = SessionContext.getSession();
Group group = this.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
}
if(group.getOwnerId() != session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"只有群主才有权限解除群聊");
}
@ -134,7 +130,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
/**
*退出群聊
* 退出群聊
*
* @param groupId 群聊id
* @return
@ -143,9 +139,6 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
public void quitGroup(Long groupId) {
UserSession session = SessionContext.getSession();
Group group = this.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
}
if(group.getOwnerId() == session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您是群主,不可退出群聊");
}
@ -154,13 +147,31 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}
/**
* 将用户踢出群聊
*
* @param groupId 群聊id
* @param userId 用户id
* @return
*/
@Override
public GroupVO findById(Long groupId) {
public void kickGroup(Long groupId, Long userId) {
UserSession session = SessionContext.getSession();
Group group = super.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在");
Group group = this.getById(groupId);
if(group.getOwnerId() != session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您不是群主,没有权限踢人");
}
if(userId == session.getId()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"亲,不能自己踢自己哟");
}
// 删除群聊成员
groupMemberService.removeByGroupAndUserId(groupId,userId);
}
@Override
public GroupVO findById(Long groupId) {
UserSession session = SessionContext.getSession();
Group group = this.getById(groupId);
GroupMember member = groupMemberService.findByGroupAndUserId(groupId,session.getId());
if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您未加入群聊");
@ -172,7 +183,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}
/**
*根据id查找群聊并进行缓存
* 根据id查找群聊并进行缓存
*
* @param groupId 群聊id
* @return
@ -180,7 +191,14 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Cacheable(value = "#groupId")
@Override
public Group GetById(Long groupId){
return super.getById(groupId);
Group group = super.getById(groupId);
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组不存在");
}
if(group.getDeleted()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群组已解散");
}
return group;
}
@ -275,5 +293,4 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
return vos;
}
}

13
im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/GroupMessageProcessor.java

@ -31,11 +31,14 @@ public class GroupMessageProcessor extends MessageProcessor<GroupMessageInfo> {
for(Long recvId:recvIds){
ChannelHandlerContext channelCtx = WebsocketChannelCtxHloder.getChannelCtx(recvId);
if(channelCtx != null){
// 推送消息到用户
SendInfo sendInfo = new SendInfo();
sendInfo.setCmd(WSCmdEnum.GROUP_MESSAGE.getCode());
sendInfo.setData(data);
channelCtx.channel().writeAndFlush(sendInfo);
// 自己发的消息不用推送
if(recvId != data.getSendId()){
// 推送消息到用户
SendInfo sendInfo = new SendInfo();
sendInfo.setCmd(WSCmdEnum.GROUP_MESSAGE.getCode());
sendInfo.setData(data);
channelCtx.channel().writeAndFlush(sendInfo);
}
// 设置已读最大id
String key = RedisKey.IM_GROUP_READED_POSITION + data.getGroupId()+":"+recvId;
redisTemplate.opsForValue().set(key,data.getId());

12
im-ui/src/components/chat/ChatGroup.vue

@ -250,16 +250,10 @@
}
},
watch: {
'chat.targetId': {
handler: function(newGroupId, oldGroupId) {
this.loadGroup(newGroupId);
},
immediate: true
}
},
mounted() {
// this.loadGroup(this.chat.targetId);
console.log("group mount...")
this.loadGroup(this.chat.targetId);
this.scrollToBottom();
}
}
</script>

3
im-ui/src/components/chat/ChatPrivate.vue

@ -215,7 +215,8 @@
}
},
mounted() {
console.log(this.chat);
console.log("private mount...")
this.scrollToBottom();
}
}
</script>

42
im-ui/src/components/common/HeadImage.vue

@ -1,23 +1,23 @@
<template>
<img :src="url"
:style="{width: size+'px',height: size+'px',cursor: 'pointer'}" />
<div class="head-image">
<img :src="url" :style="{width: size+'px',height: size+'px',cursor: 'pointer'}" />
<slot></slot>
</div>
</template>
<script>
export default {
name: "headImage",
data() {
return {
}
},
methods:{
return {}
},
methods: {},
props: {
size: {
type: Number,
default: 50
},
url:{
url: {
type: String
}
}
@ -25,19 +25,21 @@
</script>
<style scoped lang="scss">
.head-image {
position: relative;
img {
position: relative;
overflow: hidden;
border-radius: 5%;
position: relative;
overflow: hidden;
border-radius: 5%;
}
img:before {
content: '';
display: block;
width: 100%;
height: 100%;
background:url('../../assets/default_head.png') no-repeat 0 0;
background-size: 100%;
img:before {
content: '';
display: block;
width: 100%;
height: 100%;
background: url('../../assets/default_head.png') no-repeat 0 0;
background-size: 100%;
}
</style>
}
</style>

2
im-ui/src/components/friend/AddFriend.vue

@ -1,6 +1,6 @@
<template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="30%" :before-close="handleClose">
<el-input placeholder="搜索好友" class="input-with-select" v-model="searchText" @keyup.enter.native="handleSearch()">
<el-input placeholder="输入好友昵称,最多展示10条" class="input-with-select" v-model="searchText" @keyup.enter.native="handleSearch()">
<el-button slot="append" icon="el-icon-search" @click="handleSearch()"></el-button>
</el-input>
<el-scrollbar style="height:400px">

8
im-ui/src/components/group/AddGroupMember.vue

@ -7,9 +7,8 @@
</el-input>
<el-scrollbar style="height:500px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-show="friend.nickName.startsWith(searchText)"
:showDelete="false" @click.native="handleSwitchCheck(friend)" :friend="friend"
:index="index" :active="index === activeIndex">
<friend-item v-show="friend.nickName.startsWith(searchText)" :showDelete="false" @click.native="handleSwitchCheck(friend)"
:friend="friend" :index="index" :active="index === activeIndex">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" v-model="friend.isCheck"
size="medium"></el-checkbox>
</friend-item>
@ -106,7 +105,8 @@
this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.find((m) => m.userId == f.id);
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
console.log(m);
if (m) {
//

26
im-ui/src/components/group/GroupMember.vue

@ -1,6 +1,8 @@
<template>
<div class="group-member">
<head-image :url="member.headImage" :size="60" class=""></head-image>
<head-image :url="member.headImage" :size="60" class="">
<div v-if="showDel" @click.stop="handleDelete()" class="btn-kick el-icon-error"></div>
</head-image>
<div class="member-name">{{member.aliasName}}</div>
</div>
</template>
@ -21,7 +23,12 @@
},
showDel:{
type: Boolean,
default: true
default: false
}
},
methods:{
handleDelete(){
this.$emit("del",this.member);
}
}
}
@ -43,5 +50,20 @@
text-overflow:ellipsis;
overflow:hidden
}
.btn-kick {
display: none;
position: absolute;
right: -8px;
top: -8px;
color: darkred;
font-size: 20px;
cursor: pointer;
}
&:hover .btn-kick{
display: block;
color: #ce1818;
}
}
</style>

16
im-ui/src/view/Chat.vue

@ -6,10 +6,12 @@
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</div>
<div v-for="(chat,index) in chatStore.chats" :key="chat.type+chat.targetId">
<chat-item :chat="chat" :index="index" @click.native="handleActiveItem(index)" @del="handleDelItem(chat,index)"
:active="index === chatStore.activeIndex"></chat-item>
</div>
<el-scrollbar class="l-chat-list" >
<div v-for="(chat,index) in chatStore.chats" :key="chat.type+chat.targetId">
<chat-item :chat="chat" :index="index" @click.native="handleActiveItem(index)" @del="handleDelItem(chat,index)"
:active="index === chatStore.activeIndex"></chat-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="r-chat-box">
<chat-private :chat="activeChat" v-if="activeChat.type=='PRIVATE'"></chat-private>
@ -137,6 +139,8 @@
<style lang="scss">
.el-container {
.l-chat-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px;
background: white;
width: 3rem;
@ -146,6 +150,10 @@
background-color: white;
line-height: 50px;
}
.l-friend-ist{
flex: 1;
}
}
.r-chat-box {

42
im-ui/src/view/Friend.vue

@ -1,7 +1,7 @@
<template>
<el-container>
<el-aside width="250px" class="l-friend-box">
<div class="l-friend-header" height="60px">
<div class="l-friend-header" height="5%">
<div class="l-friend-search">
<el-input width="200px" placeholder="搜索好友" v-model="searchText">
<el-button slot="append" icon="el-icon-search"></el-button>
@ -9,17 +9,21 @@
</div>
<el-button plain icon="el-icon-plus" style="border: none; padding:12px; font-size: 20px;color: black;" title="添加好友"
@click="handleShowAddFriend()"></el-button>
<add-friend :dialogVisible="showAddFriend" @close="handleCloseAddFriend">
</add-friend>
</div>
<div v-for="(friend,index) in $store.state.friendStore.friends" :key="friend.id">
<friend-item v-show="friend.nickName.startsWith(searchText)" :friend="friend" :index="index" :active="index === $store.state.friendStore.activeIndex"
@del="handleDelItem(friend,index)" @click.native="handleActiveItem(friend,index)">
</friend-item>
</div>
<el-scrollbar class="l-friend-list" >
<div v-for="(friend,index) in $store.state.friendStore.friends" :key="friend.id">
<friend-item v-show="friend.nickName.startsWith(searchText)" :friend="friend" :index="index" :active="index === $store.state.friendStore.activeIndex"
@del="handleDelItem(friend,index)" @click.native="handleActiveItem(friend,index)">
</friend-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="r-friend-box">
<div class="r-friend-header" v-show="userInfo.id">
{{userInfo.nickName}}
</div>
<div v-show="userInfo.id">
<div class="user-detail">
<head-image class="detail-head-image" :size="200" :url="userInfo.headImage"></head-image>
@ -146,6 +150,8 @@
<style scoped lang="scss">
.el-container {
.l-friend-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px;
background: white;
.l-friend-header {
@ -158,18 +164,38 @@
flex: 1;
}
}
.l-friend-ist{
flex: 1;
}
}
.r-friend-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px;
.r-friend-header {
width: 100%;
height: 50px;
padding: 5px;
line-height: 50px;
font-size: 20px;
text-align: left;
text-indent: 10px;
background-color: white;
border: #dddddd solid 1px;
}
.user-detail {
width: 100%;
display: flex;
padding: 50px 10px 10px 50px;
text-align: center;
justify-content: space-around;
.info-item {
width: 400px;
height: 200px;
margin-left: 20px;
background-color: #ffffff;
}
.description {

62
im-ui/src/view/Group.vue

@ -10,19 +10,20 @@
<el-button plain icon="el-icon-plus" style="border: none; padding: 12px; font-size: 20px;color: black;" title="创建群聊"
@click="handleCreateGroup()"></el-button>
</div>
<div v-for="(group,index) in groupStore.groups" :key="group.id">
<group-item v-show="group.remark.startsWith(searchText)" :group="group" :active="index === groupStore.activeIndex"
@click.native="handleActiveItem(group,index)">
</group-item>
</div>
<el-scrollbar class="l-group-list">
<div v-for="(group,index) in groupStore.groups" :key="group.id">
<group-item v-show="group.remark.startsWith(searchText)" :group="group" :active="index === groupStore.activeIndex"
@click.native="handleActiveItem(group,index)">
</group-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="r-group-box">
<div class="r-group-header" v-show="groupStore.activeIndex>=0">
<div class="r-group-header" v-show="activeGroup.id">
{{activeGroup.remark}}({{groupMembers.length}})
</div>
<div class="r-group-container">
<div v-show="groupStore.activeIndex>=0">
<div v-show="activeGroup.id">
<div class="r-group-info">
<div>
<file-upload class="avatar-uploader" action="/api/image/upload" :disabled="!isOwner" :showLoading="true"
@ -37,7 +38,7 @@
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled ></el-input>
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="activeGroup.remark" placeholder="群聊的备注仅自己可见" maxlength="20"></el-input>
@ -60,7 +61,8 @@
<el-scrollbar style="height:400px;">
<div class="r-group-member-list">
<div v-for="(member) in groupMembers" :key="member.id">
<group-member class="r-group-member" :member="member" :showDel="true"></group-member>
<group-member v-show="!member.quit" class="r-group-member" :member="member" :showDel="isOwner&&member.userId!=activeGroup.ownerId"
@del="handleKick"></group-member>
</div>
<div class="r-group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="handleInviteMember()">
@ -97,10 +99,7 @@
return {
searchText: "",
maxSize: 5 * 1024 * 1024,
activeGroup: {
empty: true,
remark: ""
},
activeGroup: {},
groupMembers: [],
showAddGroupMember: false,
rules: {
@ -176,6 +175,25 @@
});
})
},
handleKick(member) {
this.$confirm(`确定将成员'${member.aliasName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/api/group/kick/${this.activeGroup.id}`,
method: 'delete',
params: {
userId: member.userId
}
}).then(() => {
this.$message.success(`已将${member.aliasName}移出群聊`);
member.quit = true;
});
})
},
handleQuit() {
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', {
@ -192,7 +210,7 @@
this.$store.commit("removeGroupChat", this.activeGroup.id);
});
})
},
handleSendMessage() {
let chat = {
@ -210,7 +228,7 @@
url: `/api/group/members/${this.activeGroup.id}`,
method: "get"
}).then((members) => {
this.groupMembers = members.filter((m)=>!m.quit);
this.groupMembers = members;
})
}
},
@ -227,7 +245,7 @@
}
},
mounted() {
if(this.groupStore.activeIndex>=0){
if (this.groupStore.activeIndex >= 0) {
let activeGroup = this.groupStore.groups[this.groupStore.activeIndex];
// store
this.activeGroup = JSON.parse(JSON.stringify(activeGroup));
@ -241,6 +259,8 @@
<style lang="scss">
.im-group-box {
.l-group-box {
display: flex;
flex-direction: column;
border: #dddddd solid 1px;
background: white;
@ -255,6 +275,10 @@
flex: 1;
}
}
.l-group-ist{
flex: 1;
}
}
.r-group-box {
@ -317,6 +341,10 @@
display: block;
}
}
.send-btn {
margin-top: 10px;
}
}
.r-group-member-list {

Loading…
Cancel
Save