fix(live): 修复上麦 goUp 的 NPE 风险、异常吞噬和重复 HTTP 调用

P0 - BUG 修复:
- 将 liveMicrophone 和 micUser 的 null 检查移至使用前,彻底消除 NPE 风险
- 替换 catch(Exception) 全吞逻辑:业务异常直接上抛保留原始错误码,
  不再将所有错误误报为"麦克风已被占用",改用 log.error 记录完整堆栈

P1 - 性能优化:
- checkMicAvailableUse 前移至加锁后第一步,麦位已占时提前返回,
  避免后续 5 次 HTTP 调用空跑
- userProfileClient.getByUserId 由 2 次减为 1 次:
  先获取 UserProfileDTO 后直接传给 toLiveMicUser,
  删除 getLiveMicUser 内部的重复调用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 8e9ca4f3434762969e7bbe76d69dc64aec08df89)
This commit is contained in:
tianfeng 2026-04-07 18:19:37 +08:00
parent e36a28cf85
commit 49281d503a

View File

@ -42,6 +42,7 @@ import com.red.circle.other.inner.model.cmd.live.LiveHeartbeatCmd;
import com.red.circle.other.inner.model.cmd.live.MemberRolesQryCmd; import com.red.circle.other.inner.model.cmd.live.MemberRolesQryCmd;
import com.red.circle.other.inner.model.dto.live.RoomProfileDTO; import com.red.circle.other.inner.model.dto.live.RoomProfileDTO;
import com.red.circle.other.inner.model.dto.live.RoomProfileManagerDTO; import com.red.circle.other.inner.model.dto.live.RoomProfileManagerDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.other.inner.model.dto.user.WealthAndCharmLevelDTO; import com.red.circle.other.inner.model.dto.user.WealthAndCharmLevelDTO;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.func.VoidConsumer; import com.red.circle.tool.core.func.VoidConsumer;
@ -159,27 +160,38 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
@Override @Override
public LiveMicrophoneNoticeDTO goUp(Long roomId, Integer micIndex, Long userId) { public LiveMicrophoneNoticeDTO goUp(Long roomId, Integer micIndex, Long userId) {
micOpsLock(roomId, micIndex); micOpsLock(roomId, micIndex);
// RLock lock = redissonClient.getLock(roomId.toString()+micIndex.toString()+"room");
// lock.lock();
try { try {
// 验证用户是否真实存在 // P1: 优先检查麦克风可用性失败立即返回避免后续 HTTP 调用空跑
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfileClient.getByUserId(userId));
RoomProfileDTO roomProfile = roomManagerClient.getById(roomId).getBody();
// 房间是否真实存在
ResponseAssert.notNull(RoomErrorCode.ROOM_NOT_EXISTS, roomProfile);
// 获取上麦克风需要数据
LiveMicrophone liveMicrophone = getLiveMicrophone(roomId, micIndex, userId);
liveMicrophone.getUser().setCharmLevel(userLevelClient.getUserLevel(SysOriginPlatformEnum.valueOf(roomProfile.getSysOrigin()),userId).getBody().getCharmLevel());
ResponseAssert.notNull(LiveMicErrorCode.MIC_INDEX_NOT_FOUND, liveMicrophone);
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, liveMicrophone.getUser());
checkMicAvailableUse(roomId, micIndex); checkMicAvailableUse(roomId, micIndex);
// P1: 获取用户信息一次后续复用避免 getLiveMicUser 内部重复调用
UserProfileDTO userProfile = userProfileClient.getByUserId(userId).getBody();
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfile);
RoomProfileDTO roomProfile = roomManagerClient.getById(roomId).getBody();
ResponseAssert.notNull(RoomErrorCode.ROOM_NOT_EXISTS, roomProfile);
// P0: 先判 null 再使用避免 NPE catch 吞掉误报为"麦克风已被占用"
LiveMicSettingSeat micSeat = liveMicSettingService.getMicSeat(roomId, micIndex);
ResponseAssert.notNull(LiveMicErrorCode.MIC_INDEX_NOT_FOUND, micSeat);
// P1: 直接用已有 userProfile 构建 micUser跳过重复的 HTTP 调用
LiveMicUser micUser = liveMicrophoneInfraConvertor.toLiveMicUser(userProfile);
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, micUser);
micUser.setRoles(Objects.toString(roomMemberClient.getMemberRoles(roomId, userId).getBody()));
micUser.setCharmLevel(userLevelClient.getUserLevel(
SysOriginPlatformEnum.valueOf(roomProfile.getSysOrigin()), userId).getBody().getCharmLevel());
LiveMicrophone liveMicrophone = new LiveMicrophone()
.setMicIndex(micSeat.getMicIndex())
.setUser(micUser)
.updateLastActivityTime();
// 下掉已上麦克风 // 下掉已上麦克风
List<LiveMicrophone> lives = liveMicCacheService.listLiveMicrophone(roomId, userId); List<LiveMicrophone> lives = liveMicCacheService.listLiveMicrophone(roomId, userId);
if (CollectionUtils.isNotEmpty(lives)) { if (CollectionUtils.isNotEmpty(lives)) {
liveMicCacheService.goDown(roomId, liveMicCacheService.goDown(roomId, lives.stream().map(LiveMicrophone::getMicIndex).toList());
lives.stream().map(LiveMicrophone::getMicIndex).toList());
} }
// 上麦克风 // 上麦克风
@ -190,13 +202,12 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
// 发送通知 // 发送通知
return micUserChangeNotice(roomProfile); return micUserChangeNotice(roomProfile);
}catch (Exception e){
e.printStackTrace(); } catch (RuntimeException e) {
ResponseAssert.isTrue(LiveMicErrorCode.MICROPHONE_OCCUPIED,false); // P0: 不再吞掉所有异常业务异常直接上抛保留原始错误码记录日志便于排查
return null; log.error("上麦失败 roomId={}, micIndex={}, userId={}", roomId, micIndex, userId, e);
} throw e;
finally { } finally {
// lock.unlock();
micOpsUnlock(roomId, micIndex); micOpsUnlock(roomId, micIndex);
} }
} }
@ -522,7 +533,6 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
} }
Map<Integer, LiveMicrophone> liveMicrophoneMap = mapLiveMicrophone(roomId); Map<Integer, LiveMicrophone> liveMicrophoneMap = mapLiveMicrophone(roomId);
log.info("在线取麦克风列表信息 -Map: {}", liveMicrophoneMap);
return liveMicSettingSeats.stream().map(seat -> new LiveMicrophoneDTO() return liveMicSettingSeats.stream().map(seat -> new LiveMicrophoneDTO()
.setRoomId(roomId) .setRoomId(roomId)
.setMicIndex(seat.getMicIndex()) .setMicIndex(seat.getMicIndex())