!31 fix: 设置取消群管理员时实时更新

Merge pull request !31 from 云裂痕/dev
This commit is contained in:
云裂痕
2025-10-21 12:06:57 +00:00
committed by Gitee
17 changed files with 126 additions and 82 deletions

View File

@@ -131,8 +131,8 @@ HuLa-Server 是一款基于 SpringCloud、SpringBoot3、Netty、MyBatis-Plus 和
8. **目标WS节点消费分发过来的主题消息**
9. **查找本地会话映射表**
10. **推送消息到具体客户端**
11. **客户端返回ACK确认** ![进行中](https://img.shields.io/badge/🐣-进行中-ee9f20?style=flat&labelColor=fef7e6&color=ee9f20)
12. **更新消息状态为已送达** ![进行中](https://img.shields.io/badge/🐣-进行中-ee9f20?style=flat&labelColor=fef7e6&color=ee9f20)
11. **客户端返回ACK确认**
12. **更新消息状态为已送达**
![messageFlow.png](preview/messageFlow.png)
## 🌐 性能对比 WS 服务)

View File

@@ -216,7 +216,7 @@ public class DefUserServiceImpl extends SuperCacheServiceImpl<DefUserManager, Lo
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updatePassword(DefUserPasswordUpdateVO data) {
ArgumentAssert.notEmpty(data.getOldPassword(), "请输入旧密码");
// ArgumentAssert.notEmpty(data.getOldPassword(), "请输入旧密码");
DefUser user = superManager.getUserByEmail(2, data.getEmail());
ArgumentAssert.notNull(user, "用户不存在");
ArgumentAssert.equals(user.getId(), ContextUtil.getUid(), "只能修改自己的密码");

View File

@@ -1,8 +1,7 @@
package com.luohuo.flex.im.core.chat.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.luohuo.flex.im.domain.vo.request.admin.AdminAddReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminRevokeReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminSetReq;
import com.luohuo.flex.im.domain.vo.request.contact.ContactAddReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberExitReq;
import jakarta.validation.Valid;
@@ -189,12 +188,12 @@ public interface RoomAppService {
/**
* 添加管理员
*/
void addAdmin(Long uid, @Valid AdminAddReq request);
void addAdmin(Long uid, @Valid AdminSetReq request);
/**
* 移除管理员
*/
void revokeAdmin(Long uid, @Valid AdminRevokeReq request);
void revokeAdmin(Long uid, @Valid AdminSetReq request);
/**
* 创建系统好友

View File

@@ -11,6 +11,7 @@ import com.luohuo.flex.im.domain.vo.response.msg.BodyDTO;
import com.luohuo.flex.im.domain.vo.response.msg.VideoCallMsgDTO;
import com.luohuo.flex.im.domain.vo.response.msg.MergeMsgDTO;
import com.luohuo.flex.im.domain.vo.response.msg.NoticeMsgDTO;
import com.luohuo.flex.model.entity.ws.AdminChangeDTO;
import com.luohuo.flex.model.entity.ws.WSNotice;
import com.luohuo.flex.model.enums.MessageMarkTypeEnum;
import com.luohuo.flex.im.domain.enums.MessageStatusEnum;
@@ -197,6 +198,16 @@ public class MessageAdapter {
return wsBaseResp;
}
/**
* 构建设置管理员
*/
public static WsBaseResp<AdminChangeDTO> buildSetAdminMessage(AdminChangeDTO adminChangeDTO) {
WsBaseResp<AdminChangeDTO> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.GROUP_SET_ADMIN.getType());
wsBaseResp.setData(adminChangeDTO);
return wsBaseResp;
}
/**
* 已读群公告
*/

View File

@@ -28,10 +28,10 @@ import com.luohuo.flex.im.domain.dto.SummeryInfoDTO;
import com.luohuo.flex.im.domain.entity.*;
import com.luohuo.flex.im.domain.enums.*;
import com.luohuo.flex.im.domain.vo.request.ChatMessageReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminAddReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminRevokeReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminSetReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberExitReq;
import com.luohuo.flex.im.domain.entity.msg.TextMsgReq;
import com.luohuo.flex.model.entity.ws.AdminChangeDTO;
import com.luohuo.flex.model.enums.ChatActiveStatusEnum;
import com.luohuo.flex.im.domain.vo.request.contact.ContactAddReq;
import jakarta.validation.Valid;
@@ -110,7 +110,13 @@ import java.util.stream.Collectors;
import static com.luohuo.flex.im.core.chat.constant.GroupConst.MAX_MANAGE_COUNT;
import static com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum.UNREAD;
/**
* 聊天室应用服务实现类
* - 聊天室创建、管理和维护
* - 群组成员管理(添加、删除、管理员设置)
* - 聊天消息处理与转发
* - 用户会话管理、在线状态管理、公告发布与管理
*/
@Slf4j
@Service
@AllArgsConstructor
@@ -307,30 +313,25 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
@Override
@RedissonLock(prefixKey = "addAdmin:", key = "#request.roomId")
@Transactional(rollbackFor = Exception.class)
public void addAdmin(Long uid, AdminAddReq request) {
public void addAdmin(Long uid, AdminSetReq request) {
// 1. 判断群聊是否存在
RoomGroup roomGroup = roomGroupCache.getByRoomId(request.getRoomId());
AssertUtil.isNotEmpty(roomGroup, GroupErrorEnum.GROUP_NOT_EXIST);
RoomGroup roomGroup = verifyGet(uid, request);
// 2. 判断该用户是否是群主
Boolean isLord = groupMemberDao.isLord(roomGroup.getId(), uid);
AssertUtil.isTrue(isLord, GroupErrorEnum.NOT_ALLOWED_OPERATION);
// 3. 判断群成员是否在群中
Boolean isGroupShip = groupMemberDao.isGroupShip(roomGroup.getRoomId(), request.getUidList());
AssertUtil.isTrue(isGroupShip, GroupErrorEnum.USER_NOT_IN_GROUP);
// 4. 判断管理员数量是否达到上限
// 4.1 查询现有管理员数量
// 2. 判断管理员数量是否达到上限
// 2.1 查询现有管理员数量
List<Long> manageUidList = groupMemberDao.getManageUidList(roomGroup.getId());
// 4.2 去重
// 2.2 去重
HashSet<Long> manageUidSet = new HashSet<>(manageUidList);
manageUidSet.addAll(request.getUidList());
AssertUtil.isFalse(manageUidSet.size() > MAX_MANAGE_COUNT, GroupErrorEnum.MANAGE_COUNT_EXCEED);
// 5. 增加管理员
// 3. 增加管理员
groupMemberDao.addAdmin(roomGroup.getId(), request.getUidList());
// 5. 发送给所有群成员
List<Long> memberUidList = groupMemberCache.getMemberUidList(roomGroup.getRoomId());
pushService.sendPushMsg(MessageAdapter.buildSetAdminMessage(new AdminChangeDTO(roomGroup.getRoomId(), request.getUidList(), true)), memberUidList, uid);
// 每个被邀请的人都要收到邀请进群的消息
setAdminNotice(NoticeTypeEnum.GROUP_SET_ADMIN, uid, request.getUidList(), manageUidList, roomGroup.getRoomId());
}
@@ -358,7 +359,8 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
// uid,
// uuid,
// id,
// roomId
// roomId,
// ""
// );
});
}
@@ -372,7 +374,22 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
@Override
@RedissonLock(prefixKey = "revokeAdmin:", key = "#request.roomId")
@Transactional(rollbackFor = Exception.class)
public void revokeAdmin(Long uid, AdminRevokeReq request) {
public void revokeAdmin(Long uid, AdminSetReq request) {
// 1. 校验
RoomGroup roomGroup = verifyGet(uid, request);
// 2. 撤销管理员
groupMemberDao.revokeAdmin(roomGroup.getId(), request.getUidList());
List<Long> memberUidList = groupMemberCache.getMemberUidList(roomGroup.getRoomId());
pushService.sendPushMsg(MessageAdapter.buildSetAdminMessage(new AdminChangeDTO(roomGroup.getRoomId(), request.getUidList(), false)), memberUidList, uid);
setAdminNotice(NoticeTypeEnum.GROUP_RECALL_ADMIN, uid, request.getUidList(), new ArrayList<>(), roomGroup.getRoomId());
}
/**
* 校验人员在群里的权限
*/
private RoomGroup verifyGet(Long uid, AdminSetReq request) {
// 1. 判断群聊是否存在
RoomGroup roomGroup = roomGroupCache.getByRoomId(request.getRoomId());
AssertUtil.isNotEmpty(roomGroup, GroupErrorEnum.GROUP_NOT_EXIST);
@@ -384,10 +401,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
// 3. 判断群成员是否在群中
Boolean isGroupShip = groupMemberDao.isGroupShip(roomGroup.getRoomId(), request.getUidList());
AssertUtil.isTrue(isGroupShip, GroupErrorEnum.USER_NOT_IN_GROUP);
// 4. 撤销管理员
groupMemberDao.revokeAdmin(roomGroup.getId(), request.getUidList());
setAdminNotice(NoticeTypeEnum.GROUP_RECALL_ADMIN, uid, request.getUidList(), new ArrayList<>(), roomGroup.getRoomId());
return roomGroup;
}
/**

View File

@@ -8,6 +8,9 @@ import com.luohuo.flex.im.domain.vo.res.NoticeVO;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.model.entity.ws.WSNotice;
/**
* 系统通知服务,群通知、好友通知
*/
public interface NoticeService {
Notice getByApplyId(Long uid, Long applyId);

View File

@@ -19,8 +19,7 @@ import com.luohuo.flex.im.domain.vo.request.ChatMessageMemberReq;
import com.luohuo.flex.im.domain.vo.request.GroupAddReq;
import com.luohuo.flex.im.domain.vo.request.RoomInfoReq;
import com.luohuo.flex.im.domain.vo.request.RoomMyInfoReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminAddReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminRevokeReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminSetReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberAddReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberDelReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberExitReq;
@@ -128,7 +127,7 @@ public class RoomController {
@PutMapping("/group/admin")
@Operation(summary ="添加管理员")
public R<Boolean> addAdmin(@Valid @RequestBody AdminAddReq request) {
public R<Boolean> addAdmin(@Valid @RequestBody AdminSetReq request) {
Long uid = ContextUtil.getUid();
roomService.addAdmin(uid, request);
return R.success();
@@ -136,7 +135,7 @@ public class RoomController {
@DeleteMapping("/group/admin")
@Operation(summary ="撤销管理员")
public R<Boolean> revokeAdmin(@Valid @RequestBody AdminRevokeReq request) {
public R<Boolean> revokeAdmin(@Valid @RequestBody AdminSetReq request) {
Long uid = ContextUtil.getUid();
roomService.revokeAdmin(uid, request);
return R.success();

View File

@@ -1,24 +0,0 @@
package com.luohuo.flex.im.domain.vo.request.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 撤销管理员请求信息
* @author nyh
*/
@Data
public class AdminRevokeReq {
@NotNull
@Schema(description ="房间号")
private Long roomId;
@NotNull
@Size(min = 1, max = 3)
@Schema(description ="需要撤销管理的列表")
private List<Long> uidList;
}

View File

@@ -12,7 +12,7 @@ import java.util.List;
* @author nyh
*/
@Data
public class AdminAddReq {
public class AdminSetReq {
@NotNull
@Schema(description ="房间号")
private Long roomId;

View File

@@ -56,6 +56,6 @@ public interface CaptchaService {
* @date 2022/7/26 8:05 PM
* @create [2022/7/26 8:05 PM ] [tangyh] [初始创建]
*/
R<Boolean> sendEmailCode(BindEmailReq bindEmailReq);
R<Long> sendEmailCode(BindEmailReq bindEmailReq);
}

View File

@@ -3,6 +3,7 @@ package com.luohuo.flex.oauth.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.utils.TimeUtils;
import com.luohuo.flex.model.vo.query.BindEmailReq;
import com.luohuo.flex.service.SysConfigService;
@@ -20,7 +21,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import com.luohuo.basic.base.R;
import com.luohuo.basic.cache.redis2.CacheResult;
import com.luohuo.basic.cache.repository.CacheOps;
import com.luohuo.basic.model.cache.CacheKey;
import com.luohuo.basic.utils.ArgumentAssert;
import com.luohuo.flex.base.service.tenant.DefUserService;
@@ -49,7 +49,7 @@ import com.luohuo.flex.msg.facade.MsgFacade;
public class CaptchaServiceImpl implements CaptchaService {
private final SysConfigService configService;
private final CacheOps cacheOps;
private final CachePlusOps cachePlusOps;
private final CaptchaProperties captchaProperties;
private final MsgFacade msgFacade;
private final DefUserService defUserService;
@@ -86,7 +86,7 @@ public class CaptchaServiceImpl implements CaptchaService {
Captcha captcha = createCaptcha();
CacheKey cacheKey = CaptchaCacheKeyBuilder.build(key, CaptchaTokenGranter.GRANT_TYPE);
cacheOps.set(cacheKey, captcha.text().toLowerCase());
cachePlusOps.set(cacheKey, captcha.text().toLowerCase());
HashMap<String, Object> map = new HashMap<>();
map.put("uuid", key);
@@ -114,7 +114,7 @@ public class CaptchaServiceImpl implements CaptchaService {
String code = RandomUtil.randomNumbers(4);
CacheKey cacheKey = CaptchaCacheKeyBuilder.build(mobile, templateCode);
// cacheKey.setExpire(Duration.ofMinutes(15)); // 可以修改有效期
cacheOps.set(cacheKey, code);
cachePlusOps.set(cacheKey, code);
log.info("短信验证码 cacheKey={}, code={}", cacheKey, code);
@@ -127,7 +127,7 @@ public class CaptchaServiceImpl implements CaptchaService {
}
@Override
public R<Boolean> sendEmailCode(BindEmailReq bindEmailReq) {
public R<Long> sendEmailCode(BindEmailReq bindEmailReq) {
if (MsgTemplateCodeEnum.REGISTER_EMAIL.eq(bindEmailReq.getTemplateCode())) {
// 查user表判断重复
boolean flag = defUserService.checkEmail(bindEmailReq.getEmail(), null);
@@ -139,14 +139,16 @@ public class CaptchaServiceImpl implements CaptchaService {
}
// CacheKey imgKey = CaptchaCacheKeyBuilder.build(bindEmailReq.getClientId(), CaptchaTokenGranter.GRANT_TYPE);
// CacheResult<String> result = cacheOps.get(imgKey);
// CacheResult<String> result = cachePlusOps.get(imgKey);
// ArgumentAssert.isFalse(!bindEmailReq.getCode().equals(result.getValue()), "图片验证码错误");
String code = RandomUtil.randomNumbers(6);
CacheKey cacheKey = CaptchaCacheKeyBuilder.build(bindEmailReq.getEmail(), bindEmailReq.getTemplateCode());
ArgumentAssert.isFalse(cacheOps.exists(cacheKey), "请勿重复发送验证码");
if(cachePlusOps.exists(cacheKey)){
return R.success(cachePlusOps.ttl(cacheKey));
}
cacheOps.set(cacheKey, code);
cachePlusOps.set(cacheKey, code);
log.info("邮件验证码 cacheKey={}, code={}", cacheKey, code);
// 在「运营平台」-「消息模板」配置一个「模板标识」为 templateCode 且「模板内容」中需要有 code 占位符
@@ -159,7 +161,8 @@ public class CaptchaServiceImpl implements CaptchaService {
msgSendVO.addParam("currentTime", TimeUtils.nowToStr());
msgSendVO.addRecipient(bindEmailReq.getEmail());
return R.success(msgFacade.sendByTemplate(msgSendVO));
msgFacade.sendByTemplate(msgSendVO);
return R.success(cachePlusOps.ttl(cacheKey));
}
@Override
@@ -168,14 +171,14 @@ public class CaptchaServiceImpl implements CaptchaService {
return R.fail(CAPTCHA_ERROR.build("请输入验证码"));
}
CacheKey cacheKey = CaptchaCacheKeyBuilder.build(key, templateCode);
CacheResult<String> code = cacheOps.get(cacheKey);
CacheResult<String> code = cachePlusOps.get(cacheKey);
if (StrUtil.isEmpty(code.getValue())) {
return R.fail(CAPTCHA_ERROR.build("验证码已过期"));
}
if (!StrUtil.equalsIgnoreCase(value, code.getValue())) {
return R.fail(CAPTCHA_ERROR.build("验证码不正确"));
}
cacheOps.del(cacheKey);
cachePlusOps.del(cacheKey);
return R.success(true);
}

View File

@@ -77,7 +77,7 @@ public class CaptchaController {
})
@Operation(summary = "发送邮箱验证码", description = "发送邮箱验证码")
@PostMapping(value = "/sendEmailCode")
public R<Boolean> sendEmailCode(@RequestBody BindEmailReq bindEmailReq) {
public R<Long> sendEmailCode(@RequestBody BindEmailReq bindEmailReq) {
return captchaService.sendEmailCode(bindEmailReq);
}

View File

@@ -35,6 +35,7 @@ public enum WSRespTypeEnum {
REQUEST_APPROVAL_FRIEND("requestApprovalFriend", "同意好友请求", WSFriendApproval.class),
NEW_APPLY("newApply", "好友申请、群聊邀请", WSNotice.class),
ROOM_DISSOLUTION("roomDissolution", "群解散", null),
GROUP_SET_ADMIN("groupSetAdmin", "设置管理员", AdminChangeDTO.class),
ROOM_GROUP_NOTICE_READ_MSG("roomGroupNoticeReadMsg", "群公告已读", null),
FEED_SEND_MSG("feedSendMsg", "朋友圈发布", null),

View File

@@ -0,0 +1,38 @@
package com.luohuo.flex.model.entity.ws;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;
/**
* 管理员变更数据传输对象
* @author 乾乾
*/
@Data
public class AdminChangeDTO implements Serializable {
/**
* 房间ID
*/
private String roomId;
/**
* 管理员ID列表
*/
private List<String> uids;
/**
* true: 设置 false: 取消
*/
private Boolean status;
public AdminChangeDTO(Long roomId, List<Long> uids, Boolean status) {
this.roomId = roomId != null ? roomId.toString() : null;
this.uids = uids != null ? uids.stream().map(Object::toString).collect(Collectors.toList()) : null;
this.status = status;
}
public AdminChangeDTO() {
}
}

View File

@@ -35,7 +35,7 @@ public class RoomMetadataService {
public Boolean isRoomClosed(Long roomId) {
CacheResult<Boolean> result = cachePlusOps.get(CloseRoomCacheKeyBuilder.builder(roomId));
return result.isNull()? true: result.getRawValue();
return result.isNull() || result.isNullVal()? true: result.getRawValue();
}
/**

View File

@@ -146,7 +146,7 @@ public class VideoChatService {
public void forwardSignal(Long senderUid, Long roomId, String signal, String signalType) {
// 1. 获取房间内其他成员
try {
List<Long> uidList = getOnlineUidList(roomId);
List<Long> uidList = getUserList(roomId);
uidList.remove(senderUid);
if (uidList.isEmpty()) return;
@@ -168,7 +168,7 @@ public class VideoChatService {
* 获取房间内所有人员id
* @param roomId 房间id
*/
public List<Long> getOnlineUidList(Long roomId) {
public List<Long> getUserList(Long roomId) {
return onlineService.getGroupMembers(roomId);
}
@@ -276,8 +276,8 @@ public class VideoChatService {
* @param data 通知数据
*/
private <T> List<Long> notifyRoomMembers(Long roomId, Long excludeUid, WSRespTypeEnum respType, T data) {
// 1. 获取房间内在线成员
List<Long> uidList = getOnlineUidList(roomId);
// 1. 获取房间内所有成员
List<Long> uidList = getUserList(roomId);
if (uidList.isEmpty()) return new ArrayList<>();
// 2. 排除当前用户

View File

@@ -1,18 +1,18 @@
## 本地如何跑hula
注意!! 开发环境默认大家已经安装nacos版本我用的是3.0.2web端端口是8080老版本是8848、rocketMQ、mysql、redis这些组件了
如果没有的话先看一下 [reids、mysql、rocketmq、nacos一键部署.md](../docs/install/docker/reids%E3%80%81mysql%E3%80%81rocketmq%E3%80%81nacos%E4%B8%80%E9%94%AE%E9%83%A8%E7%BD%B2.md) (组件部署文档)
如果没有的话先看一下 [redis、mysql、rocketmq、nacos一键部署.md](install/docker/reids%E3%80%81mysql%E3%80%81rocketmq%E3%80%81nacos%E4%B8%80%E9%94%AE%E9%83%A8%E7%BD%B2.md) (组件部署文档)
下面执行顺序不能乱因为install cloud模块的时候需要将 src/main/filters 里面的配置打到target里面去
1. 将 luohuo-cloud/pom.xml 导入IDEA
2. 导入luohuo-util项目
![img.png](preview/img.png)![img_1.png](preview/img_1.png)
![img.png](preview/img.png)![img_1.png](preview/img_1.png)
3. 安装luohuo-util到本地![img_2.png](preview/img_2.png)
4. 导入nacos环境以我本地为例127.0.0.1:8080![img5.png](preview/img5.png)![img_5.png](preview/img_5.png)
5. 修改redis.yml、mysql.yml、rocketmq.yml、luohuo-im-server.yml的配置![img6.png](preview/img6.png)
6. 安装luohuo-cloud到本地![img_3.png](preview/img_3.png)
7. 导入数据库 [luohuo_dev.sql](../docs/install/sql/luohuo_dev.sql)、[luohuo_im_01.sql](../docs/install/sql/luohuo_im_01.sql)
7. 导入数据库 [luohuo_dev.sql](install/sql/luohuo_dev.sql)、[luohuo_im_01.sql](install/sql/luohuo_im_01.sql)
8. 运行效果图![img7.png](preview/img7.png)
9. 前端配置访问地址:
```bash