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 getUserGroups(Long userId) { List members = groupMemberMapper.selectList(new LambdaQueryWrapper() .eq(GroupMember::getUserId, userId)); if (members.isEmpty()) { return List.of(); } List groupIds = members.stream() .map(GroupMember::getGroupId) .toList(); return groupMapper.selectList(new LambdaQueryWrapper() .in(Group::getId, groupIds) .eq(Group::getStatus, 1)); } /** * 创建群(群ID自增生成,群主为传入用户,并自动把机器人拉入群) *

* 机器人取 sys_config 中 cfg_key=robot_id 的配置值。 * 先落本地库,再调 OpenIM 建群:OpenIM 失败会抛异常回滚本地事务, * 保证不出现"本地有群、IM 无群"的脏数据。反向残留(IM 有群、本地回滚)可安全重试, * 因为 {@link OpenIMApiClient#createGroup} 对已存在的群做了幂等跳过。 *

*/ @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 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() .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() .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() .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() .eq(GroupMember::getGroupId, groupId) .eq(GroupMember::getUserId, targetUserId)); // 审计日志 auditService.log(groupId, null, operatorId, "KICK_USER", String.format("{\"targetUserId\":%d}", targetUserId)); } /** * 设置管理员 *

* 本地更新群成员角色后,调用 OpenIM 接口同步角色(roleLevel=60=管理员)。 * OpenIM 调用失败会抛异常回滚本地事务,保证本地与 IM 角色一致。 *

*/ @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() .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)); } /** * 取消管理员 *

* 本地更新群成员角色后,调用 OpenIM 接口同步角色(roleLevel=20=普通成员)。 * OpenIM 调用失败会抛异常回滚本地事务,保证本地与 IM 角色一致。 *

*/ @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() .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 getMembers(Long groupId) { return groupMemberMapper.selectList(new LambdaQueryWrapper() .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() .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() .eq(Group::getOpenimGroupId, openimGroupId)); } }