Files
xiu_b/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java
T
2026-08-09 08:35:18 +08:00

365 lines
13 KiB
Java

package com.tailbet.service.impl;
import com.tailbet.model.entity.Group;
import com.tailbet.model.entity.GroupMember;
import com.tailbet.model.entity.SysConfig;
import com.tailbet.model.entity.User;
import com.tailbet.mapper.GroupMapper;
import com.tailbet.mapper.GroupMemberMapper;
import com.tailbet.mapper.SysConfigMapper;
import com.tailbet.mapper.UserMapper;
import com.tailbet.openim.OpenIMApiClient;
import com.tailbet.service.IGroupService;
import com.tailbet.service.IAuditService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 群Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class GroupServiceImpl implements IGroupService {
/**
* 系统配置键:机器人用户ID
*/
private static final String CFG_KEY_ROBOT_ID = "robot_id";
private final GroupMapper groupMapper;
private final GroupMemberMapper groupMemberMapper;
private final UserMapper userMapper;
private final SysConfigMapper sysConfigMapper;
private final IAuditService auditService;
private final OpenIMApiClient openIMApiClient;
/**
* 获取群信息
*/
@Override
public Group getById(Long groupId) {
return groupMapper.selectById(groupId);
}
/**
* 获取用户的群列表
*/
@Override
public List<Group> getUserGroups(Long userId) {
List<GroupMember> members = groupMemberMapper.selectList(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getUserId, userId));
if (members.isEmpty()) {
return List.of();
}
List<Long> groupIds = members.stream()
.map(GroupMember::getGroupId)
.toList();
return groupMapper.selectList(new LambdaQueryWrapper<Group>()
.in(Group::getId, groupIds)
.eq(Group::getStatus, 1));
}
/**
* 创建群(群ID自增生成,群主为传入用户,并自动把机器人拉入群)
* <p>
* 机器人取 sys_config 中 cfg_key=robot_id 的配置值。
* 先落本地库,再调 OpenIM 建群:OpenIM 失败会抛异常回滚本地事务,
* 保证不出现"本地有群、IM 无群"的脏数据。反向残留(IM 有群、本地回滚)可安全重试,
* 因为 {@link OpenIMApiClient#createGroup} 对已存在的群做了幂等跳过。
* </p>
*/
@Override
@Transactional
public Group createGroup( String name, Long ownerUserId, Integer roundInterval, Long operatorId) {
if (name == null || name.isBlank()) {
throw new RuntimeException("群名称不能为空");
}
if (ownerUserId == null) {
throw new RuntimeException("群主用户ID不能为空");
}
// 群主必须是已存在的用户
User owner = userMapper.selectById(ownerUserId);
if (owner == null) {
throw new RuntimeException("群主用户不存在: " + ownerUserId);
}
// 机器人账号(sys_config 中 cfg_key=robot_id)
Long botUserId = getRobotUserId();
if (userMapper.selectById(botUserId) == null) {
throw new RuntimeException("机器人用户不存在: " + botUserId + ",请检查 sys_config.robot_id 配置");
}
// 群主本人就是机器人时,只需建一条群主记录
boolean ownerIsBot = botUserId.equals(ownerUserId);
LocalDateTime now = LocalDateTime.now();
// 建群:id 由数据库自增生成
// openim_group_id 为 NOT NULL 且要与自增 id 保持一致,
// 故先占位插入拿到 id,再回填。同一事务内,不存在中间态外泄。
Group group = new Group();
group.setName(name);
group.setOpenimGroupId("");
group.setOwnerUserId(ownerUserId);
group.setRoundInterval(roundInterval);
group.setStatus(1);
group.setCreateTime(now);
group.setUpdateTime(now);
groupMapper.insert(group);
Long groupId = group.getId();
group.setOpenimGroupId(String.valueOf(groupId));
groupMapper.updateById(group);
// 群主入群
groupMemberMapper.insert(buildMember(groupId, ownerUserId, ROLE_OWNER, now));
// 机器人入群
if (!ownerIsBot) {
groupMemberMapper.insert(buildMember(groupId, botUserId, ROLE_NORMAL, now));
}
// OpenIM 侧建群:群主为 owner,机器人作为初始成员
List<String> memberUserIDs = ownerIsBot
? List.of()
: List.of(String.valueOf(botUserId));
openIMApiClient.createGroup(String.valueOf(groupId), name, String.valueOf(ownerUserId), memberUserIDs);
// 审计日志
auditService.log(groupId, null, operatorId, "CREATE_GROUP",
String.format("{\"name\":\"%s\",\"ownerUserId\":%d,\"botUserId\":%d}",
name.replace("\\", "\\\\").replace("\"", "\\\""), ownerUserId, botUserId));
log.info("创建群成功: groupId={}, name={}, ownerUserId={}, botUserId={}, operatorId={}",
groupId, name, ownerUserId, botUserId, operatorId);
return group;
}
/**
* 读取机器人用户ID(sys_config 中 cfg_key=robot_id)
*/
private Long getRobotUserId() {
SysConfig config = sysConfigMapper.selectOne(new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getCfgKey, CFG_KEY_ROBOT_ID));
if (config == null || config.getCfgValue() == null || config.getCfgValue().isBlank()) {
throw new RuntimeException("系统配置缺失: sys_config 中未配置 " + CFG_KEY_ROBOT_ID);
}
try {
return Long.parseLong(config.getCfgValue().trim());
} catch (NumberFormatException e) {
throw new RuntimeException("系统配置 " + CFG_KEY_ROBOT_ID + " 不是合法的用户ID: " + config.getCfgValue());
}
}
/**
* 构造群成员记录
*/
private GroupMember buildMember(Long groupId, Long userId, int role, LocalDateTime now) {
GroupMember member = new GroupMember();
member.setGroupId(groupId);
member.setUserId(userId);
member.setRole(role);
member.setJoinTime(now);
member.setCreateTime(now);
member.setUpdateTime(now);
return member;
}
/**
* 拉用户入群
*/
@Override
@Transactional
public void pullUser(Long groupId, Long userId, Long operatorId) {
// 检查是否已在群中
GroupMember existing = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, userId));
if (existing != null) {
throw new RuntimeException("用户已在群中");
}
GroupMember member = new GroupMember();
member.setGroupId(groupId);
member.setUserId(userId);
member.setRole(ROLE_NORMAL);
member.setJoinTime(LocalDateTime.now());
member.setCreateTime(LocalDateTime.now());
member.setUpdateTime(LocalDateTime.now());
groupMemberMapper.insert(member);
// OpenIM 侧邀请用户入群
openIMApiClient.inviteUserToGroup(String.valueOf(groupId), List.of(String.valueOf(userId)));
// 审计日志
auditService.log(groupId, null, operatorId, "PULL_USER",
String.format("{\"userId\":%d}", userId));
}
/**
* 踢出用户
*/
@Override
@Transactional
public void kickUser(Long groupId, Long targetUserId, Long operatorId) {
Group group = groupMapper.selectById(groupId);
// 检查权限
GroupMember operator = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, operatorId));
if (operator == null) {
throw new RuntimeException("你不是群成员");
}
// 只有群主和管理员可以踢人
if (operator.getRole() != ROLE_OWNER && operator.getRole() != ROLE_ADMIN) {
throw new RuntimeException("只有群主和管理员可以踢人");
}
// 不能踢群主
if (group.getOwnerUserId().equals(targetUserId)) {
throw new RuntimeException("不能踢出群主");
}
// 删除成员
groupMemberMapper.delete(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, targetUserId));
// 审计日志
auditService.log(groupId, null, operatorId, "KICK_USER",
String.format("{\"targetUserId\":%d}", targetUserId));
}
/**
* 设置管理员
* <p>
* 本地更新群成员角色后,调用 OpenIM 接口同步角色(roleLevel=60=管理员)。
* OpenIM 调用失败会抛异常回滚本地事务,保证本地与 IM 角色一致。
* </p>
*/
@Override
@Transactional
public void setAdmin(Long groupId, Long targetUserId, Long operatorId) {
Group group = groupMapper.selectById(groupId);
// 只有群主可以设置管理员
if (!group.getOwnerUserId().equals(operatorId)) {
throw new RuntimeException("只有群主可以设置管理员");
}
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, targetUserId));
if (member == null) {
throw new RuntimeException("目标用户不是群成员");
}
member.setRole(ROLE_ADMIN);
member.setUpdateTime(LocalDateTime.now());
groupMemberMapper.updateById(member);
// OpenIM 同步:roleLevel=60 表示管理员
openIMApiClient.setGroupMemberRole(
String.valueOf(groupId), String.valueOf(targetUserId), 60);
// 审计日志
auditService.log(groupId, null, operatorId, "SET_ADMIN",
String.format("{\"targetUserId\":%d}", targetUserId));
}
/**
* 取消管理员
* <p>
* 本地更新群成员角色后,调用 OpenIM 接口同步角色(roleLevel=20=普通成员)。
* OpenIM 调用失败会抛异常回滚本地事务,保证本地与 IM 角色一致。
* </p>
*/
@Override
@Transactional
public void removeAdmin(Long groupId, Long targetUserId, Long operatorId) {
Group group = groupMapper.selectById(groupId);
if (!group.getOwnerUserId().equals(operatorId)) {
throw new RuntimeException("只有群主可以取消管理员");
}
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, targetUserId));
if (member == null) {
throw new RuntimeException("目标用户不是群成员");
}
member.setRole(ROLE_NORMAL);
member.setUpdateTime(LocalDateTime.now());
groupMemberMapper.updateById(member);
// OpenIM 同步:roleLevel=20 表示普通成员
openIMApiClient.setGroupMemberRole(
String.valueOf(groupId), String.valueOf(targetUserId), 20);
// 审计日志
auditService.log(groupId, null, operatorId, "REMOVE_ADMIN",
String.format("{\"targetUserId\":%d}", targetUserId));
}
/**
* 获取群成员
*/
@Override
public List<GroupMember> getMembers(Long groupId) {
return groupMemberMapper.selectList(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId));
}
/**
* 检查用户是否是群主
*/
@Override
public boolean isOwner(Long groupId, Long userId) {
Group group = groupMapper.selectById(groupId);
return group != null && group.getOwnerUserId().equals(userId);
}
/**
* 检查用户是否是管理员
*/
@Override
public boolean isAdmin(Long groupId, Long userId) {
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, userId));
return member != null && (member.getRole() == ROLE_ADMIN || member.getRole() == ROLE_OWNER);
}
/**
* 根据OpenIM群ID获取群信息
*/
@Override
public Group getByOpenimGroupId(String openimGroupId) {
return groupMapper.selectOne(new LambdaQueryWrapper<Group>()
.eq(Group::getOpenimGroupId, openimGroupId));
}
}