rtc修复

This commit is contained in:
hy001 2026-04-28 19:42:55 +08:00
parent 485c15184b
commit 65d3a85c18
14 changed files with 913 additions and 221 deletions

View File

@ -1,8 +1,9 @@
package com.red.circle.external.inner.endpoint.message.api;
import com.red.circle.framework.dto.ResultResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@ -37,6 +38,15 @@ public interface AgoraClientApi {
* @param cname
* @return
*/
@GetMapping("/channel/user")
ResultResponse<List<Long>> channelUser(@RequestParam("cname") String cname);
}
@GetMapping("/channel/user")
ResultResponse<List<Long>> channelUser(@RequestParam("cname") String cname);
/**
* 查询频道状态.
*
* @param cname 频道名
* @return 频道状态
*/
@GetMapping("/channel/state")
ResultResponse<AgoraChannelStateDTO> channelState(@RequestParam("cname") String cname);
}

View File

@ -0,0 +1,52 @@
package com.red.circle.external.inner.model.dto;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 声网频道状态.
*/
@Data
@Accessors(chain = true)
public class AgoraChannelStateDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 查询是否成功.
*/
private Boolean querySuccess;
/**
* 频道是否存在.
*/
private Boolean channelExist;
/**
* 主播 uid 列表.
*/
private List<Long> broadcasters = new ArrayList<>();
/**
* 观众 uid 列表.
*/
private List<Long> audience = new ArrayList<>();
/**
* 原始返回或错误信息.
*/
private String rawResponse;
public boolean querySuccess() {
return Boolean.TRUE.equals(querySuccess);
}
public boolean channelExist() {
return Boolean.TRUE.equals(channelExist);
}
}

View File

@ -1,6 +1,7 @@
package com.red.circle.external.inner.endpoint.message;
import com.red.circle.external.inner.endpoint.message.api.AgoraClientApi;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import com.red.circle.external.inner.endpoint.message.api.AgoraClientApi;
import com.red.circle.external.inner.service.message.AgoraClientService;
import com.red.circle.framework.dto.ResultResponse;
import lombok.RequiredArgsConstructor;
@ -38,7 +39,12 @@ public class AgoraClientEndpoint implements AgoraClient{
}
@Override
public ResultResponse<List<Long>> channelUser(String cname) {
return ResultResponse.success(agoraClientService.channelUser(cname));
}
}
public ResultResponse<List<Long>> channelUser(String cname) {
return ResultResponse.success(agoraClientService.channelUser(cname));
}
@Override
public ResultResponse<AgoraChannelStateDTO> channelState(String cname) {
return ResultResponse.success(agoraClientService.channelState(cname));
}
}

View File

@ -1,7 +1,8 @@
package com.red.circle.external.inner.service.message;
import java.util.List;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import java.util.List;
/**
* 声网.
@ -29,5 +30,13 @@ public interface AgoraClientService {
* @param cname
* @return
*/
List<Long> channelUser(String cname);
}
List<Long> channelUser(String cname);
/**
* 查询频道状态.
*
* @param cname 频道名
* @return 频道状态
*/
AgoraChannelStateDTO channelState(String cname);
}

View File

@ -1,9 +1,10 @@
package com.red.circle.external.inner.service.message.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.red.circle.external.infra.props.RcMessageAgoraProperties;
import com.red.circle.external.inner.service.message.AgoraClientService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.red.circle.external.infra.props.RcMessageAgoraProperties;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import com.red.circle.external.inner.service.message.AgoraClientService;
import com.red.circle.tool.core.collection.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -92,7 +93,7 @@ public class AgoraClientServiceImpl implements AgoraClientService {
@Override
public List<Long> channelUser(String cname) {
public List<Long> channelUser(String cname) {
log.info("rcMessageAgoraProperties配置信息:{}", JSON.toJSONString(rcMessageAgoraProperties));
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("https://api.sd-rtn.com/dev/v1/channel/user/" + rcMessageAgoraProperties.getAppId() + "/" + cname);
@ -122,6 +123,48 @@ public class AgoraClientServiceImpl implements AgoraClientService {
log.error("声网获取在线用户信息失败:{}", e.getMessage());
return CollectionUtils.newArrayList();
}
}
}
}
@Override
public AgoraChannelStateDTO channelState(String cname) {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("https://api.sd-rtn.com/dev/v1/channel/user/" + rcMessageAgoraProperties.getAppId() + "/" + cname);
httpGet.addHeader("Authorization", rcMessageAgoraProperties.getAuthKey());
try {
CloseableHttpResponse response = httpClient.execute(httpGet);
String result = EntityUtils.toString(response.getEntity());
log.info("声网频道状态返回信息 cname={}, result={}", cname, result);
JSONObject resultObject = JSON.parseObject(result);
Boolean success = resultObject.getBoolean("success");
AgoraChannelStateDTO state = new AgoraChannelStateDTO()
.setQuerySuccess(Boolean.TRUE.equals(success))
.setChannelExist(Boolean.FALSE)
.setRawResponse(result);
if (!Boolean.TRUE.equals(success)) {
return state;
}
JSONObject dataObject = resultObject.getJSONObject("data");
if (Objects.isNull(dataObject)) {
return state;
}
state.setChannelExist(Boolean.TRUE.equals(dataObject.getBoolean("channel_exist")));
if (!state.channelExist()) {
return state;
}
if (Objects.nonNull(dataObject.getJSONArray("broadcasters"))) {
state.setBroadcasters(dataObject.getJSONArray("broadcasters").toJavaList(Long.class));
}
if (Objects.nonNull(dataObject.getJSONArray("audience"))) {
state.setAudience(dataObject.getJSONArray("audience").toJavaList(Long.class));
}
return state;
} catch (Exception e) {
log.error("声网获取频道状态失败 cname={}:{}", cname, e.getMessage());
return new AgoraChannelStateDTO()
.setQuerySuccess(Boolean.FALSE)
.setChannelExist(Boolean.FALSE)
.setRawResponse(e.getMessage());
}
}
}

View File

@ -7,8 +7,9 @@ import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
import com.red.circle.live.domain.live.LiveMicrophone;
import com.red.circle.live.domain.live.LiveMusicStatus;
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
import com.red.circle.live.infra.database.cache.service.LiveMusicHeartbeatService;
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
import com.red.circle.live.infra.database.cache.service.LiveMusicHeartbeatService;
import com.red.circle.live.infra.database.cache.service.UserAgoraTokenCacheService;
import com.red.circle.other.inner.endpoint.live.ActiveVoiceRoomClient;
import com.red.circle.other.inner.model.dto.live.ActiveVoiceRoomCO;
import com.red.circle.tool.core.collection.CollectionUtils;
@ -33,8 +34,9 @@ public class LiveQuitRoomExe {
private final LiveMicCacheService liveMicCacheService;
private final LiveMicrophoneGateway liveMicrophoneGateway;
private final LiveMusicHeartbeatService liveMusicHeartbeatService;
private final ActiveVoiceRoomClient activeVoiceRoomClient;
private final RedisService redisService;
private final ActiveVoiceRoomClient activeVoiceRoomClient;
private final RedisService redisService;
private final UserAgoraTokenCacheService userAgoraTokenCacheService;
private static final String EMPTY_ROOM_CACHE_KEY = "empty_room_cache:";
private static final int CACHE_EXPIRE_MINUTES = 10;
@ -70,6 +72,9 @@ public class LiveQuitRoomExe {
liveMusicHeartbeatService.removeMickUser(roomId);
}
redisService.delete("IN_ROOM:" + userId);
userAgoraTokenCacheService.remove(userId, roomId);
boolean finalMicChanged = micChanged;
ThreadPoolManager.getInstance().execute(() -> {
if (finalMicChanged) {

View File

@ -5,11 +5,14 @@ import com.red.circle.component.mq.MessageEventProcessDescribe;
import com.red.circle.component.mq.config.RocketMqMessageListener;
import com.red.circle.component.mq.service.Action;
import com.red.circle.component.mq.service.ConsumerMessage;
import com.red.circle.component.mq.service.MessageListener;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.live.app.dto.cmd.HeartbeatStatusEnum;
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
import com.red.circle.component.mq.service.MessageListener;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.live.app.dto.cmd.HeartbeatStatusEnum;
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
import com.red.circle.live.app.service.AgoraRoomStateService;
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
import com.red.circle.live.domain.live.LiveMicrophone;
import com.red.circle.live.domain.online.LiveRoomUser;
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
@ -72,8 +75,10 @@ public class UserHeartbeatListener implements MessageListener {
private final LiveRoomCacheService liveRoomCacheService;
private final LiveMicUserCacheService liveMicUserCacheService;
private final RoomDailyTaskClient roomDailyTaskClient;
private final RedisService redisService;
private final RoomMemberClient roomMemberClient;
private final RedisService redisService;
private final RoomMemberClient roomMemberClient;
private final AgoraRoomStateService agoraRoomStateService;
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
private static final int HEARTBEAT_INTERVAL_SECONDS = 60;
@ -200,17 +205,25 @@ public class UserHeartbeatListener implements MessageListener {
return;
}
RoomProfileDTO roomProfile = getRoomProfileDto(roomId);
if (Objects.isNull(roomProfile.getRoomAccount())) {
log.warn("没有找到房间ID: {}", event.getSessionId());
return;
}
// 添加房间在线用户
addRoomOnlineUser(userProfile, roomProfile);
refreshMicActiveTime(event, roomProfile, userProfile);
}
RoomProfileDTO roomProfile = getRoomProfileDto(roomId);
if (Objects.isNull(roomProfile.getRoomAccount())) {
log.warn("没有找到房间ID: {}", event.getSessionId());
return;
}
UserProfileDTO actualUserProfile = ensureUserProfile(userProfile);
AgoraChannelStateDTO channelState = agoraRoomStateService.getChannelState(roomId);
if (channelState.querySuccess() && !isUserInAgoraChannel(channelState, actualUserProfile)) {
agoraMicStateCleanupService.cleanupUser(roomId, actualUserProfile.getId(),
"heartbeat agora user missing");
return;
}
// 添加房间在线用户
addRoomOnlineUser(actualUserProfile, roomProfile);
refreshMicActiveTime(event, roomProfile, actualUserProfile, channelState);
}
private RoomProfileDTO getRoomProfileDto(Long roomId) {
String roomAccount = liveRoomCacheService.getRoomAccount(roomId);
@ -258,9 +271,16 @@ public class UserHeartbeatListener implements MessageListener {
/**
* 刷新麦克风, 用户在线时间.
*/
private void refreshMicActiveTime(UserHeartbeatEvent event,
RoomProfileDTO roomProfile,
UserProfileDTO userProfile) {
private void refreshMicActiveTime(UserHeartbeatEvent event,
RoomProfileDTO roomProfile,
UserProfileDTO userProfile) {
refreshMicActiveTime(event, roomProfile, userProfile, null);
}
private void refreshMicActiveTime(UserHeartbeatEvent event,
RoomProfileDTO roomProfile,
UserProfileDTO userProfile,
AgoraChannelStateDTO checkedChannelState) {
// 如果不在麦克风, true&null兼容历史版本
if (Objects.equals(event.getUpMick(), Boolean.FALSE)) {
@ -276,9 +296,33 @@ public class UserHeartbeatListener implements MessageListener {
return;
}
LiveMicrophone liveMicrophone = liveMicrophones.get(0);
// 刷新活跃时间
LiveMicrophone liveMicrophone = liveMicrophones.get(0);
if (checkAccountNotAvailable(userProfile)) {
agoraMicStateCleanupService.cleanupUser(roomProfile.getId(), userProfile.getId(),
"heartbeat user account not available");
return;
}
AgoraChannelStateDTO channelState = Objects.nonNull(checkedChannelState)
? checkedChannelState
: agoraRoomStateService.getChannelState(roomProfile.getId());
if (!channelState.querySuccess()) {
log.warn("心跳刷新麦位跳过,声网查询失败 roomId={}, userId={}, response={}",
roomProfile.getId(), userProfile.getId(), channelState.getRawResponse());
liveMicrophoneGateway.refreshNotActiveUser(roomProfile);
return;
}
Long agoraUid = DataTypeUtils.toLong(userProfile.getAccount());
if (!channelState.channelExist() || Objects.isNull(agoraUid)
|| CollectionUtils.isEmpty(channelState.getBroadcasters())
|| !channelState.getBroadcasters().contains(agoraUid)) {
agoraMicStateCleanupService.cleanupUser(roomProfile.getId(), userProfile.getId(),
"heartbeat agora broadcaster missing");
return;
}
// 刷新活跃时间
if (liveMicCacheService.checkLockMic(roomProfile.getId(), liveMicrophone.getMicIndex())) {
event.setRetryRefreshMick(true);
event.setReqTrackId("retry-" + IdWorkerUtils.getIdStr());
@ -288,7 +332,39 @@ public class UserHeartbeatListener implements MessageListener {
liveMicrophone.updateLastActivityTime();
liveMicCacheService.goUp(roomProfile.getId(), liveMicrophone.getMicIndex(), liveMicrophone);
liveMicrophoneGateway.refreshNotActiveUser(roomProfile);
}
}
liveMicrophoneGateway.refreshNotActiveUser(roomProfile);
}
private UserProfileDTO ensureUserProfile(UserProfileDTO userProfile) {
if (Objects.nonNull(userProfile) && StringUtils.isNotBlank(userProfile.getAccount())) {
return userProfile;
}
if (Objects.isNull(userProfile) || Objects.isNull(userProfile.getId())) {
return userProfile;
}
try {
return userProfileClient.getByUserId(userProfile.getId()).getBody();
} catch (Exception e) {
log.error("获取用户信息失败 userId={}", userProfile.getId(), e);
return userProfile;
}
}
private boolean isUserInAgoraChannel(AgoraChannelStateDTO channelState, UserProfileDTO userProfile) {
if (Objects.isNull(userProfile) || StringUtils.isBlank(userProfile.getAccount())) {
return false;
}
if (!channelState.channelExist()) {
return false;
}
Long agoraUid = DataTypeUtils.toLong(userProfile.getAccount());
if (Objects.isNull(agoraUid)) {
return false;
}
return (CollectionUtils.isNotEmpty(channelState.getBroadcasters())
&& channelState.getBroadcasters().contains(agoraUid))
|| (CollectionUtils.isNotEmpty(channelState.getAudience())
&& channelState.getAudience().contains(agoraUid));
}
}

View File

@ -1,16 +1,25 @@
package com.red.circle.live.app.listener.agora;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelBase;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelUserJoin;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelUserLeave;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelUserRoleChange;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelWebhook;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelWebhookListener;
import com.red.circle.other.inner.model.dto.live.RoomProfileDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
package com.red.circle.live.app.listener.agora;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelBase;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelUserJoin;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelUserLeave;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelUserRoleChange;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelWebhook;
import com.red.circle.live.app.listener.agora.callback.AgoraChannelWebhookListener;
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
import com.red.circle.other.inner.endpoint.live.RoomManagerClient;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.live.RoomProfileDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.parse.DataTypeUtils;
import com.red.circle.tool.core.regex.RegexConstant;
import com.red.circle.tool.core.text.StringUtils;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -22,24 +31,26 @@ import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class AgoraChannelWebhookImplListener implements AgoraChannelWebhookListener {
// private final RedisService redisService;
// private final LiveMicCommon liveMicCommon;
// private final UserProfileClient userProfileClient;
// private final RoomManagerClient roomManagerClient;
// private final LiveMicCacheService liveMicCacheService;
// private final LiveMicUserCacheService liveMicUserCacheService;
public class AgoraChannelWebhookImplListener implements AgoraChannelWebhookListener {
private final RedisService redisService;
private final UserProfileClient userProfileClient;
private final RoomManagerClient roomManagerClient;
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
@Override
public void channelCreate(AgoraChannelWebhook webhook, AgoraChannelBase body) {
}
@Override
public void channelDestroy(AgoraChannelWebhook webhook, AgoraChannelBase body) {
}
@Override
public void channelDestroy(AgoraChannelWebhook webhook, AgoraChannelBase body) {
Long roomId = getRoomId(body);
if (Objects.isNull(roomId) || !checkProcess(webhook, body)) {
return;
}
agoraMicStateCleanupService.cleanupRoom(roomId, "agora channel destroy");
}
@Override
public void broadcasterJoinChannel(AgoraChannelWebhook webhook, AgoraChannelUserJoin body) {
@ -54,10 +65,10 @@ public class AgoraChannelWebhookImplListener implements AgoraChannelWebhookListe
// userRoomProfile.getUserProfile().getId());
}
@Override
public void broadcasterLeaveChannel(AgoraChannelWebhook webhook, AgoraChannelUserLeave body) {
// leaveChannelProcess(webhook, body);
}
@Override
public void broadcasterLeaveChannel(AgoraChannelWebhook webhook, AgoraChannelUserLeave body) {
leaveChannelProcess(webhook, body, body.getUid(), "agora broadcaster leave");
}
@Override
@ -65,10 +76,10 @@ public class AgoraChannelWebhookImplListener implements AgoraChannelWebhookListe
}
@Override
public void audienceLeaveChannel(AgoraChannelWebhook webhook, AgoraChannelUserLeave body) {
// leaveChannelProcess(webhook, body);
}
@Override
public void audienceLeaveChannel(AgoraChannelWebhook webhook, AgoraChannelUserLeave body) {
leaveChannelProcess(webhook, body, body.getUid(), "agora audience leave");
}
@Override
public void clientRoleChangeToBroadcaster(AgoraChannelWebhook webhook,
@ -77,105 +88,74 @@ public class AgoraChannelWebhookImplListener implements AgoraChannelWebhookListe
}
@Override
public void clientRoleChangeToAudience(AgoraChannelWebhook webhook,
AgoraChannelUserRoleChange body) {
}
/**
* 退出频道处理.
*/
// private void leaveChannelProcess(AgoraChannelWebhook webhook, AgoraChannelUserLeave body) {
//
// UserRoomProfile userRoomProfile = getUserRoomProfile(webhook, body, body.getUid());
//
// if (Objects.isNull(userRoomProfile)) {
// return;
// }
//
// RoomProfileDTO roomProfile = userRoomProfile.getRoomProfile();
// UserProfileDTO userProfile = userRoomProfile.getUserProfile();
// Long roomId = roomProfile.getId();
//
// liveMicCacheService.refreshNotActiveUser(roomId);
// liveMicUserCacheService.refreshNotActiveUser(roomId);
// liveMicUserCacheService.removeUser(roomProfile.getId(), userProfile.getId());
//
// List<LiveMicrophone> liveMicrophones = liveMicCacheService.listLiveMicrophone(
// roomId, userProfile.getId());
// if (CollectionUtils.isEmpty(liveMicrophones)) {
// return;
// }
//
// LiveMicrophone liveMicrophone = liveMicrophones.get(0);
//
// liveMicCommon.tryMicOpsLock(roomProfile.getId(), liveMicrophone.getMicIndex(),
// () -> {
// log.warn("踢下麦克风: roomId={}, micIndex={}", roomProfile.getId(),
// liveMicrophone.getMicIndex());
// liveMicCacheService.goDown(roomProfile.getId(), liveMicrophone.getMicIndex());
// liveMicCommon.micUserChangeNotice(roomProfile);
// });
// }
// private Long getRoomId(String channelName) {
//
// if (StringUtils.isBlank(channelName) || !channelName.matches(RegexConstant.NUMBER)) {
// log.warn("错误的频道名称: {}", channelName);
// return null;
// }
// return DataTypeUtils.toLong(channelName);
// }
//
// private UserRoomProfile getUserRoomProfile(AgoraChannelWebhook webhook, AgoraChannelBase body,
// Long uid) {
// if (Objects.isNull(webhook) || Objects.isNull(body)) {
// log.warn("数据错误: {}, {}",
// JacksonUtils.toJson(webhook),
// JacksonUtils.toJson(body)
// );
// return null;
// }
//
// if (Objects.isNull(uid)) {
// log.warn("数据错误, uid: {}", uid);
// return null;
// }
//
// Long roomId = getRoomId(body.getChannelName());
// if (Objects.isNull(roomId) || roomId <= 0) {
// return null;
// }
//
// if (!checkProcess(webhook, body.getTs())) {
// return null;
// }
//
// // log.warn("leaveChannelProcess: {}", JacksonUtils.toJson(webhook));
//
// RoomProfileDTO roomProfile = roomManagerClient.getById(roomId).getBody();
// if (Objects.isNull(roomProfile)) {
// return null;
// }
//
// UserProfileDTO userProfile = userProfileClient.getByAccount(roomProfile.getSysOrigin(),
// Objects.toString(uid)).getBody();
// if (Objects.isNull(userProfile)) {
// return null;
// }
//
// return new UserRoomProfile()
// .setRoomProfile(roomProfile)
// .setUserProfile(userProfile);
// }
// /**
// * true 未处理, false 已处理.
// */
// private boolean checkProcess(AgoraChannelWebhook webhook, Long ts) {
// return redisService.setIfAbsent("AGORA:" + webhook.getEventType() + ":" + ts, 1,
// TimeUnit.MINUTES);
// }
public void clientRoleChangeToAudience(AgoraChannelWebhook webhook,
AgoraChannelUserRoleChange body) {
leaveChannelProcess(webhook, body, body.getUid(), "agora role change to audience");
}
private void leaveChannelProcess(AgoraChannelWebhook webhook, AgoraChannelBase body, Long uid,
String reason) {
UserRoomProfile userRoomProfile = getUserRoomProfile(webhook, body, uid);
if (Objects.isNull(userRoomProfile)) {
return;
}
agoraMicStateCleanupService.cleanupUser(
userRoomProfile.getRoomProfile().getId(),
userRoomProfile.getUserProfile().getId(),
reason
);
}
private Long getRoomId(AgoraChannelBase body) {
if (Objects.isNull(body) || StringUtils.isBlank(body.getChannelName())
|| !body.getChannelName().matches(RegexConstant.NUMBER)) {
log.warn("错误的声网频道名称: {}", Objects.nonNull(body) ? body.getChannelName() : null);
return null;
}
return DataTypeUtils.toLong(body.getChannelName());
}
private UserRoomProfile getUserRoomProfile(AgoraChannelWebhook webhook, AgoraChannelBase body,
Long uid) {
Long roomId = getRoomId(body);
if (Objects.isNull(roomId) || roomId <= 0 || Objects.isNull(uid)) {
return null;
}
if (!checkProcess(webhook, body)) {
return null;
}
RoomProfileDTO roomProfile = roomManagerClient.getById(roomId).getBody();
if (Objects.isNull(roomProfile)) {
return null;
}
UserProfileDTO userProfile = userProfileClient.getByAccount(roomProfile.getSysOrigin(),
Objects.toString(uid)).getBody();
if (Objects.isNull(userProfile)) {
log.warn("声网回调未找到用户 roomId={}, uid={}", roomId, uid);
return null;
}
return new UserRoomProfile()
.setRoomProfile(roomProfile)
.setUserProfile(userProfile);
}
/**
* true 未处理, false 已处理.
*/
private boolean checkProcess(AgoraChannelWebhook webhook, AgoraChannelBase body) {
if (Objects.isNull(webhook)) {
return false;
}
String uniqueKey = StringUtils.isNotBlank(webhook.getNoticeId())
? webhook.getNoticeId()
: webhook.getEventType() + ":" + (Objects.nonNull(body) ? body.getTs() : webhook.getNotifyMs());
return redisService.setIfAbsent("AGORA:" + uniqueKey, 1, 1, TimeUnit.MINUTES);
}
@Data

View File

@ -0,0 +1,111 @@
package com.red.circle.live.app.scheduler;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
import com.red.circle.live.app.service.AgoraRoomStateService;
import com.red.circle.live.domain.live.LiveMicrophone;
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.parse.DataTypeUtils;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 声网真实状态和业务麦位状态补偿对账.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AgoraMicStateReconcileTask {
private static final String LOCK_KEY = "agora:mic:reconcile:lock";
private static final String SEAT_KEY_PREFIX = "LiveMick:SEAT:";
private final RedisService redisService;
private final RedisTemplate<String, Object> redisTemplate;
private final LiveMicCacheService liveMicCacheService;
private final AgoraRoomStateService agoraRoomStateService;
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
@Scheduled(fixedDelayString = "${red-circle.live.agora-mic-reconcile-delay-ms:30000}")
public void reconcileAgoraMicState() {
if (!redisService.setIfAbsent(LOCK_KEY, 1, 25, TimeUnit.SECONDS)) {
return;
}
try {
for (Long roomId : scanSeatRoomIds()) {
reconcileRoom(roomId);
}
} finally {
redisService.delete(LOCK_KEY);
}
}
private void reconcileRoom(Long roomId) {
if (Objects.isNull(roomId)) {
return;
}
AgoraChannelStateDTO state = agoraRoomStateService.getChannelState(roomId);
if (!state.querySuccess()) {
log.warn("声网麦位对账跳过,查询失败 roomId={}, response={}", roomId, state.getRawResponse());
return;
}
if (!state.channelExist()) {
agoraMicStateCleanupService.cleanupRoom(roomId, "reconcile agora channel not exist");
return;
}
List<LiveMicrophone> microphones = liveMicCacheService.listLiveMicrophone(roomId);
if (CollectionUtils.isEmpty(microphones)) {
return;
}
Set<Long> broadcasterUids = new HashSet<>(state.getBroadcasters());
for (LiveMicrophone microphone : microphones) {
if (Objects.isNull(microphone) || Objects.isNull(microphone.getUser())) {
continue;
}
Long agoraUid = DataTypeUtils.toLong(microphone.getUser().getAccount());
if (Objects.isNull(agoraUid) || !broadcasterUids.contains(agoraUid)) {
agoraMicStateCleanupService.cleanupUser(roomId, microphone.getUser().getId(),
"reconcile agora broadcaster missing");
}
}
}
private Set<Long> scanSeatRoomIds() {
Set<Long> roomIds = new HashSet<>();
ScanOptions options = ScanOptions.scanOptions().match(SEAT_KEY_PREFIX + "*").count(200).build();
try (var cursor = redisTemplate.scan(options)) {
while (cursor.hasNext()) {
Object key = cursor.next();
Long roomId = parseRoomId(Objects.toString(key, null));
if (Objects.nonNull(roomId)) {
roomIds.add(roomId);
}
}
} catch (Exception e) {
log.error("扫描声网麦位对账 Redis key 失败", e);
}
return roomIds;
}
private Long parseRoomId(String key) {
if (Objects.isNull(key) || !key.startsWith(SEAT_KEY_PREFIX)) {
return null;
}
return DataTypeUtils.toLong(key.substring(SEAT_KEY_PREFIX.length()));
}
}

View File

@ -0,0 +1,84 @@
package com.red.circle.live.app.service;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
import com.red.circle.live.domain.live.LiveMicrophone;
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
import com.red.circle.live.infra.database.cache.service.LiveMicUserCacheService;
import com.red.circle.live.infra.database.cache.service.LiveMusicHeartbeatService;
import com.red.circle.live.infra.database.cache.service.UserAgoraTokenCacheService;
import com.red.circle.tool.core.collection.CollectionUtils;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 声网状态反向清理业务麦位和房间在线缓存.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AgoraMicStateCleanupService {
private final LiveMicCacheService liveMicCacheService;
private final LiveMicUserCacheService liveMicUserCacheService;
private final LiveMusicHeartbeatService liveMusicHeartbeatService;
private final LiveMicrophoneGateway liveMicrophoneGateway;
private final UserAgoraTokenCacheService userAgoraTokenCacheService;
private final RedisService redisService;
public boolean cleanupUser(Long roomId, Long userId, String reason) {
if (Objects.isNull(roomId) || Objects.isNull(userId)) {
return false;
}
boolean micChanged = false;
List<LiveMicrophone> liveMicrophones = liveMicCacheService.listLiveMicrophone(roomId, userId);
if (CollectionUtils.isNotEmpty(liveMicrophones)) {
for (LiveMicrophone mic : liveMicrophones) {
if (Objects.isNull(mic) || Objects.isNull(mic.getMicIndex())) {
continue;
}
final boolean[] removed = {false};
liveMicrophoneGateway.tryMicOpsLock(roomId, mic.getMicIndex(), () -> {
liveMicCacheService.goDown(roomId, mic.getMicIndex());
removed[0] = true;
});
micChanged = micChanged || removed[0];
}
}
liveMicUserCacheService.removeUser(roomId, userId);
redisService.delete("IN_ROOM:" + userId);
userAgoraTokenCacheService.remove(userId, roomId);
if (micChanged) {
notifyMicChanged(roomId, reason);
}
log.warn("声网状态清理用户 roomId={}, userId={}, micChanged={}, reason={}",
roomId, userId, micChanged, reason);
return micChanged;
}
public void cleanupRoom(Long roomId, String reason) {
if (Objects.isNull(roomId)) {
return;
}
liveMicCacheService.goDownAll(roomId);
liveMicUserCacheService.remove(roomId);
liveMusicHeartbeatService.removeMickUser(roomId);
notifyMicChanged(roomId, reason);
log.warn("声网状态清理房间 roomId={}, reason={}", roomId, reason);
}
private void notifyMicChanged(Long roomId, String reason) {
try {
liveMicrophoneGateway.micUserChangeNotice(roomId);
} catch (Exception e) {
log.error("声网状态清理后推送麦位变更失败 roomId={}, reason={}", roomId, reason, e);
}
}
}

View File

@ -0,0 +1,65 @@
package com.red.circle.live.app.service;
import com.red.circle.external.inner.endpoint.message.AgoraClient;
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.tool.core.collection.CollectionUtils;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 声网房间状态查询.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AgoraRoomStateService {
private final AgoraClient agoraClient;
public AgoraChannelStateDTO getChannelState(Long roomId) {
if (Objects.isNull(roomId)) {
return queryFailed("roomId is null");
}
try {
ResultResponse<AgoraChannelStateDTO> response = agoraClient.channelState(roomId.toString());
if (Objects.isNull(response) || !response.checkSuccess()) {
return queryFailed("response not success");
}
AgoraChannelStateDTO body = response.getBody();
if (Objects.isNull(body)) {
return queryFailed("body is null");
}
return body;
} catch (Exception e) {
log.error("查询声网频道状态异常 roomId={}", roomId, e);
return queryFailed(e.getMessage());
}
}
public boolean isBroadcasterInChannel(Long roomId, String account) {
AgoraChannelStateDTO state = getChannelState(roomId);
if (!state.querySuccess() || !state.channelExist() || CollectionUtils.isEmpty(state.getBroadcasters())) {
return false;
}
Long agoraUid = parseAccount(account);
return Objects.nonNull(agoraUid) && state.getBroadcasters().contains(agoraUid);
}
private Long parseAccount(String account) {
try {
return Long.valueOf(account);
} catch (Exception e) {
return null;
}
}
private AgoraChannelStateDTO queryFailed(String reason) {
return new AgoraChannelStateDTO()
.setQuerySuccess(Boolean.FALSE)
.setChannelExist(Boolean.FALSE)
.setRawResponse(reason);
}
}

View File

@ -8,11 +8,12 @@ import com.red.circle.component.mq.MessageEventProcess;
import com.red.circle.component.mq.MessageEventProcessDescribe;
import com.red.circle.component.mq.config.RocketMqMessageListener;
import com.red.circle.component.mq.service.Action;
import com.red.circle.component.mq.service.ConsumerMessage;
import com.red.circle.component.mq.service.MessageListener;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.external.inner.model.cmd.message.CustomGroupMsgBodyCmd;
import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum;
import com.red.circle.component.mq.service.ConsumerMessage;
import com.red.circle.component.mq.service.MessageListener;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd;
import com.red.circle.external.inner.model.cmd.message.CustomGroupMsgBodyCmd;
import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.framework.web.util.ValidatorUtils;
import com.red.circle.live.inner.endpoint.api.LiveHeartbeatApi;
@ -61,9 +62,10 @@ import com.red.circle.other.inner.enums.team.TeamPolicyTypeEnum;
import com.red.circle.other.inner.model.dto.activity.props.ActivityRewardPropsDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
@ -372,14 +374,16 @@ public class GiveGiftsListener implements MessageListener {
Pair<GiftGiveRunningWater, Boolean> runningWaterBooleanPair = saveRunningWater(event);
GiftGiveRunningWater runningWater = runningWaterBooleanPair.getFirst();
if (!runningWaterBooleanPair.getSecond()) {
log.warn("保存礼物流水失败:{}", runningWater);
return;
}
if (!SysOriginPlatformEnum.isVoiceSystem(runningWater.getSysOrigin())) {
return;
}
if (!runningWaterBooleanPair.getSecond()) {
log.warn("保存礼物流水失败:{}", runningWater);
return;
}
sendNoticeGiftBroadcast(event, runningWater);
if (!SysOriginPlatformEnum.isVoiceSystem(runningWater.getSysOrigin())) {
return;
}
// 不是祝福礼物
if (Boolean.FALSE.equals(runningWater.getCheckBlessingsGift())) {
@ -400,11 +404,56 @@ public class GiveGiftsListener implements MessageListener {
// 排行榜相关
offlineProcessGiftEvent(OfflineStatisticsGiftEventType.RANK_COUNT, runningWater);
// 火箭统计相关
offlineProcessGiftEvent(OfflineStatisticsGiftEventType.ROCKET_COUNT, runningWater);
}
private Pair<GiftGiveRunningWater,Boolean> saveRunningWater(GiveAwayGiftBatchEvent event) {
// 火箭统计相关
offlineProcessGiftEvent(OfflineStatisticsGiftEventType.ROCKET_COUNT, runningWater);
}
void sendNoticeGiftBroadcast(GiveAwayGiftBatchEvent event,
GiftGiveRunningWater runningWater) {
if (Objects.isNull(event) || Objects.isNull(event.getGiftConfig())
|| Objects.isNull(runningWater)) {
return;
}
if (event.checkLuckyGift()) {
return;
}
String special = event.getGiftConfig().getSpecial();
if (StringUtils.isBlank(special) || !special.contains(GiftSpecialEnum.NOTICE.name())) {
return;
}
RoomProfile roomProfile = Objects.nonNull(event.getRoomId())
? roomProfileManagerService.getProfileById(event.getRoomId())
: null;
SendGiftNotifyCO notify = new SendGiftNotifyCO()
.setGlobalNews(Boolean.TRUE)
.setSysOrigin(event.getSysOrigin().name())
.setRoomId(event.getRoomId())
.setRoomAccount(Objects.nonNull(roomProfile) ? roomProfile.getRoomAccount() : null)
.setSendUserId(event.getSendUserId())
.setAcceptUserIds(event.getAccepts().stream()
.map(GiveAwayGiftRoomAccepts::getAcceptUserId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList()))
.setGiftId(event.getGiftConfig().getId())
.setGiftQuantity(event.getQuantity())
.setGiftCandy(event.getGiftConfig().getGiftCandy())
.setActualAmount(runningWater.getGiftValue().getActualAmount())
.setGiftType(runningWater.getGiftValue().getGiftType());
imGroupClient.sendMessageBroadcast(
BroadcastGroupMsgBodyCmd.builder()
.toPlatform(event.getSysOrigin())
.type(GroupMessageTypeEnum.SEND_GIFT)
.data(notify)
.build()
);
log.warn("NOTICE礼物全服通报成功, trackId={}, giftId={}, sendUserId={}",
event.getTrackId(), event.getGiftConfig().getId(), event.getSendUserId());
}
private Pair<GiftGiveRunningWater,Boolean> saveRunningWater(GiveAwayGiftBatchEvent event) {
List<Long> anchorUserIds = teamMemberService.listMemberIds(event.allUserId());
//直播间送礼后的心动值

View File

@ -0,0 +1,141 @@
package com.red.circle.other.app.listener.gift;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.mq.MessageEventProcess;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd;
import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftRoomAccepts;
import com.red.circle.mq.business.model.event.gift.GiveGiftConfig;
import com.red.circle.mq.rocket.business.producer.GameMqMessage;
import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
import com.red.circle.other.app.command.game.barrage.BarrageGameBaishunExe;
import com.red.circle.other.app.command.game.burstcrystal.GameBurstCrystalScheduleTwoFunQueryExe;
import com.red.circle.other.app.command.game.roompk.GameRoomPkUserCmdExe;
import com.red.circle.other.app.command.game.teampk.GameTeamPkBaseInfoCmdExe;
import com.red.circle.other.app.common.gift.GameLuckyGiftCommon;
import com.red.circle.other.app.dto.clientobject.gift.SendGiftNotifyCO;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
import com.red.circle.other.infra.database.cache.service.other.LiveHeartbeatCacheService;
import com.red.circle.other.infra.database.mongo.entity.gift.GiftGiveRunningWater;
import com.red.circle.other.infra.database.mongo.entity.gift.GiftValue;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
import com.red.circle.other.infra.database.mongo.service.gift.GiftCountAnchorDailyRunningWaterService;
import com.red.circle.other.infra.database.mongo.service.gift.GiftGiveRunningWaterService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamBillCycleService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamPolicyManagerService;
import com.red.circle.other.inner.enums.material.GiftSpecialEnum;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
class GiveGiftsListenerTest {
private final ImGroupClient imGroupClient = mock(ImGroupClient.class);
private final RoomProfileManagerService roomProfileManagerService = mock(
RoomProfileManagerService.class);
private final GiveGiftsListener listener = new GiveGiftsListener(
mock(GameMqMessage.class),
mock(GiftMqMessage.class),
mock(TeamMemberService.class),
mock(UserRegionGateway.class),
mock(MessageEventProcess.class),
mock(GameLuckyGiftCommon.class),
mock(TeamBillCycleService.class),
mock(TeamPolicyManagerService.class),
mock(EnumConfigCacheService.class),
mock(LiveHeartbeatCacheService.class),
mock(GiftGiveRunningWaterService.class),
imGroupClient,
roomProfileManagerService,
mock(GameTeamPkBaseInfoCmdExe.class),
mock(GameRoomPkUserCmdExe.class),
mock(BarrageGameBaishunExe.class),
mock(GameBurstCrystalScheduleTwoFunQueryExe.class),
mock(GiftCountAnchorDailyRunningWaterService.class)
);
@Test
void sendNoticeGiftBroadcastShouldBroadcastNoticeGift() {
RoomProfile roomProfile = new RoomProfile();
roomProfile.setRoomAccount("1002001");
when(roomProfileManagerService.getProfileById(2001L))
.thenReturn(roomProfile);
listener.sendNoticeGiftBroadcast(
event(GiftSpecialEnum.NOTICE.name(), "GOLD"),
runningWater()
);
ArgumentCaptor<BroadcastGroupMsgBodyCmd> captor = ArgumentCaptor.forClass(
BroadcastGroupMsgBodyCmd.class);
verify(imGroupClient).sendMessageBroadcast(captor.capture());
BroadcastGroupMsgBodyCmd message = captor.getValue();
assertEquals(GroupMessageTypeEnum.SEND_GIFT.name(), message.getType());
assertEquals(SysOriginPlatformEnum.LIKEI, message.getSysOrigin());
SendGiftNotifyCO data = (SendGiftNotifyCO) message.getData();
assertTrue(data.getGlobalNews());
assertEquals(2001L, data.getRoomId());
assertEquals("1002001", data.getRoomAccount());
assertEquals(3001L, data.getSendUserId());
assertEquals(List.of(4001L), data.getAcceptUserIds());
assertEquals(5001L, data.getGiftId());
assertEquals(2, data.getGiftQuantity());
assertEquals(new BigDecimal("20"), data.getGiftCandy());
assertEquals(new BigDecimal("40"), data.getActualAmount());
assertEquals("NORMAL", data.getGiftType());
}
@Test
void sendNoticeGiftBroadcastShouldIgnoreNonNoticeGift() {
listener.sendNoticeGiftBroadcast(event("ANIMATION", "GOLD"), runningWater());
verify(imGroupClient, never()).sendMessageBroadcast(any());
}
@Test
void sendNoticeGiftBroadcastShouldIgnoreLuckyGift() {
listener.sendNoticeGiftBroadcast(event(GiftSpecialEnum.NOTICE.name(), "LUCKY_GIFT"),
runningWater());
verify(imGroupClient, never()).sendMessageBroadcast(any());
}
private GiveAwayGiftBatchEvent event(String special, String giftTab) {
return new GiveAwayGiftBatchEvent()
.setSysOrigin(SysOriginPlatformEnum.LIKEI)
.setRoomId(2001L)
.setSendUserId(3001L)
.setAccepts(List.of(new GiveAwayGiftRoomAccepts().setAcceptUserId(4001L)))
.setQuantity(2)
.setTrackId(6001L)
.setGiftConfig(new GiveGiftConfig()
.setId(5001L)
.setSpecial(special)
.setType("GOLD")
.setGiftTab(giftTab)
.setGiftCandy(new BigDecimal("20")));
}
private GiftGiveRunningWater runningWater() {
return new GiftGiveRunningWater()
.setGiftValue(new GiftValue()
.setGiftType("NORMAL")
.setActualAmount(new BigDecimal("40")));
}
}

View File

@ -1,12 +1,15 @@
package com.red.circle.other.app.dto.clientobject.gift;
import com.red.circle.other.app.dto.clientobject.game.GameBurstCrystalScheduleCO;
import com.red.circle.other.app.dto.clientobject.game.GameIndoorTeamPkCO;
import com.red.circle.other.app.dto.clientobject.game.GameRoomPkUserCO;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
package com.red.circle.other.app.dto.clientobject.gift;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.other.app.dto.clientobject.game.GameBurstCrystalScheduleCO;
import com.red.circle.other.app.dto.clientobject.game.GameIndoorTeamPkCO;
import com.red.circle.other.app.dto.clientobject.game.GameRoomPkUserCO;
import java.math.BigDecimal;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 发送礼物通知.
@ -15,11 +18,69 @@ import java.util.List;
*/
@Data
@Accessors(chain = true)
public class SendGiftNotifyCO {
/**
* PK详细信息
*/
public class SendGiftNotifyCO {
/**
* 是否全局通报.
*/
private Boolean globalNews;
/**
* 系统.
*/
private String sysOrigin;
/**
* 房间ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long roomId;
/**
* 房间账号.
*/
private String roomAccount;
/**
* 送礼物人用户id.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long sendUserId;
/**
* 接收用户ID集合.
*/
private List<Long> acceptUserIds;
/**
* 礼物ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long giftId;
/**
* 礼物数量.
*/
private Integer giftQuantity;
/**
* 礼物价值/糖果.
*/
private BigDecimal giftCandy;
/**
* 本次实际消费.
*/
private BigDecimal actualAmount;
/**
* 礼物类型.
*/
private String giftType;
/**
* PK详细信息
*/
private GameIndoorTeamPkCO gameIndoorTeamPk;
/**