|
|
|
@ -1,14 +1,5 @@ |
|
|
|
package cc.hiver.mall.ie.service.impl; |
|
|
|
|
|
|
|
import cc.hiver.core.common.constant.OssConstant; |
|
|
|
import cc.hiver.core.common.constant.SettingConstant; |
|
|
|
import cc.hiver.core.dao.UserDao; |
|
|
|
import cc.hiver.core.entity.Setting; |
|
|
|
import cc.hiver.core.entity.User; |
|
|
|
import cc.hiver.core.service.SettingService; |
|
|
|
import cc.hiver.core.vo.OssSetting; |
|
|
|
import cc.hiver.file.manage.FileManageFactory; |
|
|
|
import cc.hiver.file.service.FileService; |
|
|
|
import cc.hiver.mall.ie.constant.IeChatEventType; |
|
|
|
import cc.hiver.mall.ie.constant.IeConstants; |
|
|
|
import cc.hiver.mall.ie.dto.IePresenceDTO; |
|
|
|
@ -18,17 +9,16 @@ import cc.hiver.mall.ie.entity.*; |
|
|
|
import cc.hiver.mall.ie.mapper.*; |
|
|
|
import cc.hiver.mall.ie.mq.IeChatEventProducer; |
|
|
|
import cc.hiver.mall.ie.service.*; |
|
|
|
import cc.hiver.mall.ie.vo.IeAliyunModerationResult; |
|
|
|
import cc.hiver.mall.ie.vo.IeAuditResult; |
|
|
|
import cc.hiver.mall.ie.vo.IeChatEvent; |
|
|
|
import cc.hiver.mall.ie.vo.IeMessageAckVO; |
|
|
|
import cc.hiver.mall.ie.vo.IeWechatMediaSecResult; |
|
|
|
import cc.hiver.mall.utils.ByteArrayMultipartFile; |
|
|
|
import cn.hutool.core.text.CharSequenceUtil; |
|
|
|
import cn.hutool.json.JSONUtil; |
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|
|
|
import com.google.gson.Gson; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate; |
|
|
|
import org.springframework.messaging.simp.SimpMessagingTemplate; |
|
|
|
@ -36,17 +26,11 @@ import org.springframework.stereotype.Service; |
|
|
|
import org.springframework.transaction.annotation.Transactional; |
|
|
|
import org.springframework.transaction.support.TransactionSynchronization; |
|
|
|
import org.springframework.transaction.support.TransactionSynchronizationManager; |
|
|
|
import org.springframework.web.multipart.MultipartFile; |
|
|
|
|
|
|
|
import java.io.IOException; |
|
|
|
import java.io.InputStream; |
|
|
|
import java.net.URL; |
|
|
|
import java.nio.file.Files; |
|
|
|
import java.nio.file.Path; |
|
|
|
import java.util.*; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
@lombok.extern.slf4j.Slf4j |
|
|
|
@Service |
|
|
|
public class IeChatServiceImpl implements IeChatService { |
|
|
|
|
|
|
|
@ -81,39 +65,30 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
private IeSecurityAuditService auditService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private IeWechatSecurityService wechatSecurityService; |
|
|
|
private IeAliyunModerationService aliyunModerationService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private SimpMessagingTemplate messagingTemplate; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private UserDao userDao; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private IeChatEventProducer chatEventProducer; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private StringRedisTemplate stringRedisTemplate; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private FileManageFactory fileManageFactory; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private FileService fileService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private SettingService settingService; |
|
|
|
|
|
|
|
private static final String STREAK_REWARD_KEY = "ie:streak:reward:"; |
|
|
|
private static final int STREAK_LOOKBACK_DAYS = 60; |
|
|
|
private static final int STREAK_REWARD_INTERVAL = 7; |
|
|
|
|
|
|
|
private static final String MEDIA_AUDIT_TRACE_KEY = "ie:media_audit:trace:"; |
|
|
|
private static final String MEDIA_AUDIT_PENDING_KEY = "ie:media_audit:pending:"; |
|
|
|
private static final String MEDIA_AUDIT_CALLBACK_KEY = "ie:media_audit:callback:"; |
|
|
|
private static final long WX_MEDIA_CHECK_MAX_BYTES = 10L * 1024 * 1024; |
|
|
|
private static final long VIDEO_PROCESS_MAX_BYTES = 50L * 1024 * 1024; |
|
|
|
private static final long MEDIA_MAX_BYTES = 10L * 1024 * 1024; |
|
|
|
private static final long VIDEO_MAX_BYTES = 100L * 1024 * 1024; |
|
|
|
private static final int USER_PENDING_MEDIA_LIMIT = 3; |
|
|
|
private static final int ROOM_PENDING_MEDIA_LIMIT = 6; |
|
|
|
private static final long MEDIA_AUDIT_TIMEOUT_MILLIS = 10L * 60 * 1000; |
|
|
|
/** 阿里云异步审核结果轮询间隔 */ |
|
|
|
private static final long MEDIA_AUDIT_POLL_MILLIS = 10L * 1000; |
|
|
|
/** 提交审核失败后的延迟重试间隔 */ |
|
|
|
private static final long MEDIA_AUDIT_SUBMIT_RETRY_MILLIS = 15L * 1000; |
|
|
|
|
|
|
|
@Override |
|
|
|
public IeRoom getRoom(Long roomId) { |
|
|
|
@ -141,9 +116,23 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
ensureMessageVisibleRecords(room, senderId, receiverId); |
|
|
|
assertReplyWindow(senderId, receiverId, room.getId()); |
|
|
|
Integer messageType = dto.getMessageType() == null ? 1 : dto.getMessageType(); |
|
|
|
assertMediaCapacity(senderId, room.getId(), messageType); |
|
|
|
validateMediaPayload(messageType, dto); |
|
|
|
IeAuditResult audit = auditMessage(room.getId(), senderId, messageType, dto); |
|
|
|
|
|
|
|
// 图片走阿里云同步审核,毫秒级出结论,无需异步等待
|
|
|
|
boolean imageBlocked = false; |
|
|
|
if (messageType == 2 && !Boolean.TRUE.equals(audit.getBlocked())) { |
|
|
|
IeAliyunModerationResult imageResult = aliyunModerationService.checkImage( |
|
|
|
firstNotBlank(dto.getMediaCheckUrl(), dto.getContent())); |
|
|
|
if (imageResult.isBlock()) { |
|
|
|
imageBlocked = true; |
|
|
|
} else if (!imageResult.isPass()) { |
|
|
|
// 审核服务异常时不放行也不落库,让用户重试
|
|
|
|
throw new RuntimeException("图片审核服务繁忙,请稍后再试"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
IeRoomMessage message = new IeRoomMessage(); |
|
|
|
message.setRoomId(room.getId()); |
|
|
|
message.setSenderId(senderId); |
|
|
|
@ -154,14 +143,27 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
message.setAuditStatus(audit.getAuditStatus()); |
|
|
|
message.setRiskLevel(audit.getRiskLevel()); |
|
|
|
message.setHitWords(String.join(",", audit.getHitWords())); |
|
|
|
message.setIsBlocked(Boolean.TRUE.equals(audit.getBlocked()) ? 1 : 0); |
|
|
|
message.setIsBlocked(Boolean.TRUE.equals(audit.getBlocked()) || imageBlocked ? 1 : 0); |
|
|
|
if (imageBlocked) { |
|
|
|
message.setAuditStatus(IeConstants.AUDIT_BLOCK); |
|
|
|
message.setRiskLevel(IeConstants.RISK_HIGH); |
|
|
|
} |
|
|
|
message.setMediaDuration(safeMediaDuration(dto.getMediaDuration())); |
|
|
|
message.setMediaSize(dto.getMediaSize()); |
|
|
|
message.setMediaFormat(dto.getMediaFormat()); |
|
|
|
message.setMediaCheckUrl(dto.getMediaCheckUrl()); |
|
|
|
message.setMediaCheckType(dto.getMediaCheckType()); |
|
|
|
boolean needMediaAudit = message.getIsBlocked() == 0 && isMediaMessage(messageType); |
|
|
|
message.setMediaAuditStatus(needMediaAudit ? IeConstants.MEDIA_AUDIT_PENDING : IeConstants.MEDIA_AUDIT_NONE); |
|
|
|
// 图片已同步审完;只有语音/视频还需要异步任务
|
|
|
|
boolean needMediaAudit = message.getIsBlocked() == 0 && (messageType == 4 || messageType == 5); |
|
|
|
if (needMediaAudit) { |
|
|
|
message.setMediaAuditStatus(IeConstants.MEDIA_AUDIT_PENDING); |
|
|
|
} else if (messageType == 2 && message.getIsBlocked() == 0) { |
|
|
|
message.setMediaAuditStatus(IeConstants.MEDIA_AUDIT_PASS); |
|
|
|
} else if (imageBlocked) { |
|
|
|
message.setMediaAuditStatus(IeConstants.MEDIA_AUDIT_BLOCK); |
|
|
|
} else { |
|
|
|
message.setMediaAuditStatus(IeConstants.MEDIA_AUDIT_NONE); |
|
|
|
} |
|
|
|
message.setExpireTime(cleanupTime()); |
|
|
|
message.setCreateTime(new Date()); |
|
|
|
roomMessageMapper.insert(message); |
|
|
|
@ -201,6 +203,27 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
return messageType != null && (messageType == 2 || messageType == 4 || messageType == 5); |
|
|
|
} |
|
|
|
|
|
|
|
private void assertMediaCapacity(Long senderId, Long roomId, Integer messageType) { |
|
|
|
if (!isMediaMessage(messageType)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!redisService.allowMediaMessage(senderId, roomId)) { |
|
|
|
throw new RuntimeException("消息太火爆了,稍后再试"); |
|
|
|
} |
|
|
|
Long userPending = roomMessageMapper.selectCount(new LambdaQueryWrapper<IeRoomMessage>() |
|
|
|
.eq(IeRoomMessage::getSenderId, senderId) |
|
|
|
.eq(IeRoomMessage::getMediaAuditStatus, IeConstants.MEDIA_AUDIT_PENDING)); |
|
|
|
if (userPending != null && userPending >= USER_PENDING_MEDIA_LIMIT) { |
|
|
|
throw new RuntimeException("还有媒体正在处理,稍后再试"); |
|
|
|
} |
|
|
|
Long roomPending = roomMessageMapper.selectCount(new LambdaQueryWrapper<IeRoomMessage>() |
|
|
|
.eq(IeRoomMessage::getRoomId, roomId) |
|
|
|
.eq(IeRoomMessage::getMediaAuditStatus, IeConstants.MEDIA_AUDIT_PENDING)); |
|
|
|
if (roomPending != null && roomPending >= ROOM_PENDING_MEDIA_LIMIT) { |
|
|
|
throw new RuntimeException("当前聊天太火爆了,稍后再试"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void validateMediaPayload(Integer messageType, IeRoomMessageDTO dto) { |
|
|
|
if (!isMediaMessage(messageType)) { |
|
|
|
return; |
|
|
|
@ -212,11 +235,11 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
if (mediaSize == null || mediaSize <= 0) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if ((messageType == 2 || messageType == 4) && mediaSize > WX_MEDIA_CHECK_MAX_BYTES) { |
|
|
|
if ((messageType == 2 || messageType == 4) && mediaSize > MEDIA_MAX_BYTES) { |
|
|
|
throw new RuntimeException("图片/语音不能超过10MB"); |
|
|
|
} |
|
|
|
if (messageType == 5 && mediaSize > VIDEO_PROCESS_MAX_BYTES) { |
|
|
|
throw new RuntimeException("视频不能超过50MB"); |
|
|
|
if (messageType == 5 && mediaSize > VIDEO_MAX_BYTES) { |
|
|
|
throw new RuntimeException("视频不能超过100MB,请先剪辑后再发送"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -236,6 +259,14 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
ack.setMediaSize(message.getMediaSize()); |
|
|
|
ack.setMediaFormat(message.getMediaFormat()); |
|
|
|
ack.setMediaAuditStatus(message.getMediaAuditStatus()); |
|
|
|
if (message.getMessageType() != null && message.getMessageType() == 5) { |
|
|
|
ack.setPoster(message.getMediaCheckUrl()); |
|
|
|
} |
|
|
|
if (Integer.valueOf(IeConstants.MEDIA_AUDIT_FAILED).equals(message.getMediaAuditStatus())) { |
|
|
|
ack.setAuditTip("审核服务繁忙,消息没有送出,请重新发送"); |
|
|
|
} else if (message.getIsBlocked() != null && message.getIsBlocked() == 1) { |
|
|
|
ack.setAuditTip("内容未通过审核,请换一个更合适的内容"); |
|
|
|
} |
|
|
|
return ack; |
|
|
|
} |
|
|
|
|
|
|
|
@ -280,7 +311,7 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
result.setBlocked(false); |
|
|
|
return result; |
|
|
|
} |
|
|
|
return auditService.audit("message", roomId, senderId, senderOpenid(senderId), content); |
|
|
|
return auditService.audit("message", roomId, senderId, content); |
|
|
|
} |
|
|
|
|
|
|
|
private Integer safeMediaDuration(Integer mediaDuration) { |
|
|
|
@ -295,11 +326,20 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
IeRoom room = assertActiveRoom(senderId, dto.getRoomId()); |
|
|
|
Long receiverId = room.getUserAId().equals(senderId) ? room.getUserBId() : room.getUserAId(); |
|
|
|
assertNotBlocked(senderId, receiverId); |
|
|
|
// 轻互动文案会实时推给对方,带文字时同样要过审
|
|
|
|
String eventText = dto.getEventText(); |
|
|
|
if (CharSequenceUtil.isNotBlank(eventText)) { |
|
|
|
IeAuditResult audit = auditService.audit("presence", room.getId(), senderId, eventText); |
|
|
|
if (Boolean.TRUE.equals(audit.getBlocked())) { |
|
|
|
throw new RuntimeException("这条互动内容不太合适,换一个吧"); |
|
|
|
} |
|
|
|
eventText = audit.getFilteredContent(); |
|
|
|
} |
|
|
|
IePresenceEvent event = new IePresenceEvent(); |
|
|
|
event.setRoomId(room.getId()); |
|
|
|
event.setSenderId(senderId); |
|
|
|
event.setEventType(dto.getEventType()); |
|
|
|
event.setEventText(dto.getEventText()); |
|
|
|
event.setEventText(eventText); |
|
|
|
event.setCreateTime(new Date()); |
|
|
|
presenceEventMapper.insert(event); |
|
|
|
messagingTemplate.convertAndSendToUser(String.valueOf(receiverId), "/queue/ie/presence", event); |
|
|
|
@ -348,9 +388,10 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
throw new RuntimeException("无法举报:你不在该陪伴房间中"); |
|
|
|
} |
|
|
|
Long otherId = reporterId.equals(room.getUserAId()) ? room.getUserBId() : room.getUserAId(); |
|
|
|
if (dto.getReportedUserId() == null || !dto.getReportedUserId().equals(otherId)) { |
|
|
|
if (dto.getReportedUserId() != null && !dto.getReportedUserId().equals(otherId)) { |
|
|
|
throw new RuntimeException("无法举报:被举报人与房间不匹配"); |
|
|
|
} |
|
|
|
dto.setReportedUserId(otherId); |
|
|
|
if (dto.getMessageId() != null) { |
|
|
|
Long count = roomMessageMapper.selectCount(new LambdaQueryWrapper<IeRoomMessage>() |
|
|
|
.eq(IeRoomMessage::getId, dto.getMessageId()) |
|
|
|
@ -361,13 +402,17 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
} |
|
|
|
} |
|
|
|
IeReport report = new IeReport(); |
|
|
|
IeUserProfile reportedProfile = findProfile(dto.getReportedUserId()); |
|
|
|
report.setReporterId(reporterId); |
|
|
|
report.setReportedUserId(dto.getReportedUserId()); |
|
|
|
report.setRoomId(dto.getRoomId()); |
|
|
|
report.setMessageId(dto.getMessageId()); |
|
|
|
report.setRegionId(reportedProfile == null || reportedProfile.getRegionId() == null ? dto.getRegionId() : reportedProfile.getRegionId()); |
|
|
|
report.setRegionName(reportedProfile == null || reportedProfile.getRegionName() == null ? dto.getRegionName() : reportedProfile.getRegionName()); |
|
|
|
report.setReasonType(dto.getReasonType()); |
|
|
|
report.setReasonText(dto.getReasonText()); |
|
|
|
report.setStatus(0); |
|
|
|
report.setBlockedIe(0); |
|
|
|
report.setCreateTime(new Date()); |
|
|
|
reportMapper.insert(report); |
|
|
|
publishEvent(IeChatEventType.REPORT_CREATED, dto.getRoomId(), dto.getMessageId(), reporterId, dto.getReportedUserId(), 0, dto.getReasonType()); |
|
|
|
@ -492,10 +537,25 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
.orderByDesc(IeRoomMessage::getCreateTime)); |
|
|
|
for (IeRoomMessage message : result.getRecords()) { |
|
|
|
message.setMine(message.getSenderId() != null && message.getSenderId().equals(userId)); |
|
|
|
fillAuditTip(message); |
|
|
|
} |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
private void fillAuditTip(IeRoomMessage message) { |
|
|
|
if (message == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (Integer.valueOf(IeConstants.MEDIA_AUDIT_FAILED).equals(message.getMediaAuditStatus())) { |
|
|
|
message.setAuditTip("审核服务繁忙,消息没有送出,请重新发送"); |
|
|
|
} else if (message.getIsBlocked() != null && message.getIsBlocked() == 1) { |
|
|
|
message.setAuditTip("内容未通过审核,请换一个更合适的内容"); |
|
|
|
} |
|
|
|
if (message.getMessageType() != null && message.getMessageType() == 5) { |
|
|
|
message.setPoster(message.getMediaCheckUrl()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void submitMediaAudit(Long messageId) { |
|
|
|
IeRoomMessage message = roomMessageMapper.selectById(messageId); |
|
|
|
@ -505,79 +565,88 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
if (!Integer.valueOf(IeConstants.MEDIA_AUDIT_PENDING).equals(message.getMediaAuditStatus())) { |
|
|
|
return; |
|
|
|
} |
|
|
|
List<MediaAuditItem> items; |
|
|
|
try { |
|
|
|
items = buildMediaAuditItems(message); |
|
|
|
} catch (Exception e) { |
|
|
|
failMediaAudit(message, e.getMessage()); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (items.isEmpty()) { |
|
|
|
failMediaAudit(message, "media audit material missing"); |
|
|
|
return; |
|
|
|
} |
|
|
|
List<String> traceIds = new ArrayList<>(); |
|
|
|
String openid = senderOpenid(message.getSenderId()); |
|
|
|
stringRedisTemplate.opsForValue().set(MEDIA_AUDIT_PENDING_KEY + message.getId(), String.valueOf(items.size()), 24, TimeUnit.HOURS); |
|
|
|
for (MediaAuditItem item : items) { |
|
|
|
IeWechatMediaSecResult result = wechatSecurityService.mediaCheckAsync(openid, item.url, item.type); |
|
|
|
if (result == null || !result.isSubmitted() || CharSequenceUtil.isBlank(result.getTraceId())) { |
|
|
|
failMediaAudit(message, "media audit submit failed"); |
|
|
|
Integer type = message.getMessageType(); |
|
|
|
// 视频封面是用户上传的图片,先同步审一道,违规直接拦截省一次视频任务
|
|
|
|
if (type != null && type == 5 && message.getMediaCheckType() != null && message.getMediaCheckType() == 2 |
|
|
|
&& CharSequenceUtil.isNotBlank(message.getMediaCheckUrl())) { |
|
|
|
IeAliyunModerationResult coverResult = aliyunModerationService.checkImage(message.getMediaCheckUrl()); |
|
|
|
if (coverResult.isBlock()) { |
|
|
|
blockMediaMessage(message, "aliyun video cover blocked: " + coverResult.getDetail()); |
|
|
|
return; |
|
|
|
} |
|
|
|
traceIds.add(result.getTraceId()); |
|
|
|
stringRedisTemplate.opsForValue().set(MEDIA_AUDIT_TRACE_KEY + result.getTraceId(), String.valueOf(message.getId()), 24, TimeUnit.HOURS); |
|
|
|
} |
|
|
|
message.setMediaAuditTraceIds(String.join(",", traceIds)); |
|
|
|
IeAliyunModerationResult submitResult; |
|
|
|
if (type != null && type == 5) { |
|
|
|
submitResult = aliyunModerationService.submitVideo(message.getRawContent()); |
|
|
|
} else { |
|
|
|
submitResult = aliyunModerationService.submitVoice(firstNotBlank(message.getMediaCheckUrl(), message.getRawContent())); |
|
|
|
} |
|
|
|
if (!submitResult.isPending() || CharSequenceUtil.isBlank(submitResult.getTaskId())) { |
|
|
|
// 提交失败多为网络抖动/限流等瞬时问题,延迟重试而不是立刻判失败;
|
|
|
|
// 发送时已埋了 10 分钟超时兜底消息,持续失败最终会标记未通过
|
|
|
|
log.warn("i/e媒体审核提交未成功,{}秒后重试 messageId={}, detail={}", |
|
|
|
MEDIA_AUDIT_SUBMIT_RETRY_MILLIS / 1000, message.getId(), submitResult.getDetail()); |
|
|
|
chatEventProducer.sendMediaAuditRetry(message.getId(), MEDIA_AUDIT_SUBMIT_RETRY_MILLIS); |
|
|
|
return; |
|
|
|
} |
|
|
|
message.setMediaAuditTraceIds(submitResult.getTaskId()); |
|
|
|
message.setUpdateTime(new Date()); |
|
|
|
roomMessageMapper.updateById(message); |
|
|
|
chatEventProducer.sendMediaAuditPoll(message.getId(), MEDIA_AUDIT_POLL_MILLIS); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void handleMediaAuditCallback(String traceId, boolean risky, Integer statusCode, String payload) { |
|
|
|
if (CharSequenceUtil.isBlank(traceId)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
String cachedMessageId = stringRedisTemplate.opsForValue().get(MEDIA_AUDIT_TRACE_KEY + traceId); |
|
|
|
IeRoomMessage message = null; |
|
|
|
if (CharSequenceUtil.isNotBlank(cachedMessageId)) { |
|
|
|
message = roomMessageMapper.selectById(Long.valueOf(cachedMessageId)); |
|
|
|
} |
|
|
|
if (message == null) { |
|
|
|
message = roomMessageMapper.selectOne(new LambdaQueryWrapper<IeRoomMessage>() |
|
|
|
.like(IeRoomMessage::getMediaAuditTraceIds, traceId) |
|
|
|
.last("limit 1")); |
|
|
|
} |
|
|
|
public void pollMediaAudit(Long messageId) { |
|
|
|
IeRoomMessage message = roomMessageMapper.selectById(messageId); |
|
|
|
if (message == null || !Integer.valueOf(IeConstants.MEDIA_AUDIT_PENDING).equals(message.getMediaAuditStatus())) { |
|
|
|
return; |
|
|
|
} |
|
|
|
Boolean firstCallback = stringRedisTemplate.opsForValue().setIfAbsent(MEDIA_AUDIT_CALLBACK_KEY + traceId, "1", 24, TimeUnit.HOURS); |
|
|
|
if (Boolean.FALSE.equals(firstCallback)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (risky) { |
|
|
|
blockMediaMessage(message, "wechat media risky traceId=" + traceId + ", payload=" + payload); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (statusCode != null && statusCode != 0) { |
|
|
|
failMediaAudit(message, "wechat media callback status=" + statusCode + ", traceId=" + traceId); |
|
|
|
String taskId = message.getMediaAuditTraceIds(); |
|
|
|
if (CharSequenceUtil.isBlank(taskId)) { |
|
|
|
failMediaAudit(message, "media audit taskId missing"); |
|
|
|
return; |
|
|
|
} |
|
|
|
String pendingValue = stringRedisTemplate.opsForValue().get(MEDIA_AUDIT_PENDING_KEY + message.getId()); |
|
|
|
if (CharSequenceUtil.isBlank(pendingValue)) { |
|
|
|
failMediaAudit(message, "media audit pending counter missing, traceId=" + traceId); |
|
|
|
Integer type = message.getMessageType(); |
|
|
|
IeAliyunModerationResult result = type != null && type == 5 |
|
|
|
? aliyunModerationService.queryVideoResult(taskId) |
|
|
|
: aliyunModerationService.queryVoiceResult(taskId); |
|
|
|
if (result.isPending()) { |
|
|
|
// 还在审,继续轮询;10 分钟超时兜底会把卡死的任务标记失败
|
|
|
|
chatEventProducer.sendMediaAuditPoll(messageId, MEDIA_AUDIT_POLL_MILLIS); |
|
|
|
return; |
|
|
|
} |
|
|
|
Long remaining = stringRedisTemplate.opsForValue().decrement(MEDIA_AUDIT_PENDING_KEY + message.getId()); |
|
|
|
if (remaining == null || remaining > 0) { |
|
|
|
if (result.isBlock()) { |
|
|
|
blockMediaMessage(message, "aliyun media blocked: " + result.getDetail()); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (remaining < 0) { |
|
|
|
if (result.isPass()) { |
|
|
|
passMediaAudit(message); |
|
|
|
return; |
|
|
|
} |
|
|
|
// 结果查询失败同样按瞬时问题处理,继续轮询,由超时兜底收口
|
|
|
|
log.warn("i/e媒体审核结果查询未成功,继续轮询 messageId={}, detail={}", messageId, result.getDetail()); |
|
|
|
chatEventProducer.sendMediaAuditPoll(messageId, MEDIA_AUDIT_POLL_MILLIS); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 审核定论只能发生一次(CAS:仅当仍处于审核中时才允许写入终态)。 |
|
|
|
* 防止 MQ 重投/提交重试/并发轮询导致“先提示不通过、几分钟后又变成通过”的状态翻转。 |
|
|
|
* |
|
|
|
* @return true 表示本次调用赢得定论权,可以继续发 ack/事件 |
|
|
|
*/ |
|
|
|
private boolean finalizeMediaAudit(IeRoomMessage message) { |
|
|
|
return roomMessageMapper.update(message, new LambdaUpdateWrapper<IeRoomMessage>() |
|
|
|
.eq(IeRoomMessage::getId, message.getId()) |
|
|
|
.eq(IeRoomMessage::getMediaAuditStatus, IeConstants.MEDIA_AUDIT_PENDING)) > 0; |
|
|
|
} |
|
|
|
|
|
|
|
private void passMediaAudit(IeRoomMessage message) { |
|
|
|
message.setMediaAuditStatus(IeConstants.MEDIA_AUDIT_PASS); |
|
|
|
message.setUpdateTime(new Date()); |
|
|
|
roomMessageMapper.updateById(message); |
|
|
|
if (!finalizeMediaAudit(message)) { |
|
|
|
log.warn("i/e媒体审核已有定论,忽略本次通过结果 messageId={}", message.getId()); |
|
|
|
return; |
|
|
|
} |
|
|
|
IeMessageAckVO ack = buildAck(message, null); |
|
|
|
deliverMessage(message, ack); |
|
|
|
messagingTemplate.convertAndSendToUser(String.valueOf(message.getSenderId()), "/queue/ie/ack", ack); |
|
|
|
@ -595,189 +664,6 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
failMediaAudit(message, "media audit callback timeout"); |
|
|
|
} |
|
|
|
|
|
|
|
private List<MediaAuditItem> buildMediaAuditItems(IeRoomMessage message) { |
|
|
|
Integer type = message.getMessageType(); |
|
|
|
String content = message.getRawContent(); |
|
|
|
if (type == null) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
if (type == 2) { |
|
|
|
return singletonAuditItem(firstNotBlank(message.getMediaCheckUrl(), content), 2); |
|
|
|
} |
|
|
|
if (type == 4) { |
|
|
|
return singletonAuditItem(firstNotBlank(message.getMediaCheckUrl(), content), 1); |
|
|
|
} |
|
|
|
if (type == 5) { |
|
|
|
List<MediaAuditItem> items = new ArrayList<>(); |
|
|
|
String coverUrl = message.getMediaCheckType() != null && message.getMediaCheckType() == 2 ? message.getMediaCheckUrl() : null; |
|
|
|
if (CharSequenceUtil.isNotBlank(coverUrl)) { |
|
|
|
items.add(new MediaAuditItem(coverUrl, 2)); |
|
|
|
} |
|
|
|
items.addAll(extractVideoAuditItems(content, message.getId())); |
|
|
|
return items; |
|
|
|
} |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* SSRF 防护:服务端要主动拉取的媒体地址只允许公网 http(s), |
|
|
|
* 拒绝回环/内网/链路本地地址,防止借审核流程探测内网。 |
|
|
|
*/ |
|
|
|
private void assertSafeMediaUrl(String url) { |
|
|
|
try { |
|
|
|
URL parsed = new URL(url); |
|
|
|
String protocol = parsed.getProtocol(); |
|
|
|
if (!"http".equalsIgnoreCase(protocol) && !"https".equalsIgnoreCase(protocol)) { |
|
|
|
throw new RuntimeException("非法的媒体地址协议"); |
|
|
|
} |
|
|
|
java.net.InetAddress address = java.net.InetAddress.getByName(parsed.getHost()); |
|
|
|
if (address.isLoopbackAddress() || address.isSiteLocalAddress() |
|
|
|
|| address.isAnyLocalAddress() || address.isLinkLocalAddress()) { |
|
|
|
throw new RuntimeException("非法的媒体地址"); |
|
|
|
} |
|
|
|
} catch (RuntimeException e) { |
|
|
|
throw e; |
|
|
|
} catch (Exception e) { |
|
|
|
throw new RuntimeException("媒体地址解析失败"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private List<MediaAuditItem> singletonAuditItem(String url, Integer type) { |
|
|
|
if (CharSequenceUtil.isBlank(url) || type == null) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
return Collections.singletonList(new MediaAuditItem(url, type)); |
|
|
|
} |
|
|
|
|
|
|
|
private List<MediaAuditItem> extractVideoAuditItems(String videoUrl, Long messageId) { |
|
|
|
if (CharSequenceUtil.isBlank(videoUrl)) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
assertSafeMediaUrl(videoUrl); |
|
|
|
Path dir = null; |
|
|
|
try { |
|
|
|
dir = Files.createTempDirectory("ie-media-audit-" + messageId + "-"); |
|
|
|
Path videoPath = dir.resolve("source.mp4"); |
|
|
|
java.net.URLConnection connection = new URL(videoUrl).openConnection(); |
|
|
|
connection.setConnectTimeout(5000); |
|
|
|
connection.setReadTimeout(30000); |
|
|
|
try (InputStream inputStream = connection.getInputStream()) { |
|
|
|
Files.copy(inputStream, videoPath); |
|
|
|
} |
|
|
|
Path framePattern = dir.resolve("frame-%03d.jpg"); |
|
|
|
runFfmpeg(Arrays.asList("ffmpeg", "-y", "-v", "error", "-i", videoPath.toString(), "-vf", "fps=1/3", "-vframes", "3", framePattern.toString())); |
|
|
|
Path audioPath = dir.resolve("audio.mp3"); |
|
|
|
boolean audioExtracted = runFfmpegQuiet(Arrays.asList("ffmpeg", "-y", "-v", "error", "-i", videoPath.toString(), "-t", "60", "-vn", "-ac", "1", "-ar", "16000", audioPath.toString())); |
|
|
|
List<MediaAuditItem> items = new ArrayList<>(); |
|
|
|
List<Path> frames; |
|
|
|
try (java.util.stream.Stream<Path> stream = Files.list(dir)) { |
|
|
|
frames = stream |
|
|
|
.filter(path -> path.getFileName().toString().startsWith("frame-") && path.getFileName().toString().endsWith(".jpg")) |
|
|
|
.sorted() |
|
|
|
.collect(Collectors.toList()); |
|
|
|
} |
|
|
|
for (Path frame : frames) { |
|
|
|
items.add(new MediaAuditItem(uploadAuditFile(frame, "image/jpeg"), 2)); |
|
|
|
} |
|
|
|
if (audioExtracted && Files.exists(audioPath) && Files.size(audioPath) > 0) { |
|
|
|
items.add(new MediaAuditItem(uploadAuditFile(audioPath, "audio/mpeg"), 1)); |
|
|
|
} |
|
|
|
return items; |
|
|
|
} catch (Exception e) { |
|
|
|
throw new RuntimeException("视频审核素材处理失败", e); |
|
|
|
} finally { |
|
|
|
cleanupTempDirectory(dir); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void runFfmpeg(List<String> command) throws IOException, InterruptedException { |
|
|
|
boolean success = runFfmpegQuiet(command); |
|
|
|
if (!success) { |
|
|
|
throw new RuntimeException("ffmpeg command failed: " + String.join(" ", command)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private boolean runFfmpegQuiet(List<String> command) throws IOException, InterruptedException { |
|
|
|
Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); |
|
|
|
Thread drainThread = new Thread(() -> drainProcessOutput(process)); |
|
|
|
drainThread.setDaemon(true); |
|
|
|
drainThread.start(); |
|
|
|
boolean finished = process.waitFor(60, TimeUnit.SECONDS); |
|
|
|
if (!finished) { |
|
|
|
process.destroyForcibly(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
return process.exitValue() == 0; |
|
|
|
} |
|
|
|
|
|
|
|
private void drainProcessOutput(Process process) { |
|
|
|
try (InputStream inputStream = process.getInputStream()) { |
|
|
|
byte[] buffer = new byte[1024]; |
|
|
|
while (inputStream.read(buffer) != -1) { |
|
|
|
// drain only
|
|
|
|
} |
|
|
|
} catch (Exception ignored) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private String uploadAuditFile(Path path, String contentType) throws Exception { |
|
|
|
byte[] bytes = Files.readAllBytes(path); |
|
|
|
MultipartFile multipartFile = new ByteArrayMultipartFile(bytes, path.getFileName().toString(), contentType); |
|
|
|
Setting setting = settingService.get(SettingConstant.OSS_USED); |
|
|
|
if (setting == null || CharSequenceUtil.isBlank(setting.getValue())) { |
|
|
|
throw new RuntimeException("OSS存储服务未配置"); |
|
|
|
} |
|
|
|
String fKey = UUID.randomUUID().toString().replace("-", "") + "-" + path.getFileName(); |
|
|
|
String url; |
|
|
|
try (InputStream inputStream = multipartFile.getInputStream()) { |
|
|
|
url = fileManageFactory.getFileManage(null).inputStreamUpload(inputStream, fKey, multipartFile); |
|
|
|
} |
|
|
|
cc.hiver.file.entity.File file = new cc.hiver.file.entity.File(); |
|
|
|
file.setLocation(getOssType(setting.getValue())) |
|
|
|
.setTitle(path.getFileName().toString()) |
|
|
|
.setSize(multipartFile.getSize()) |
|
|
|
.setType(contentType) |
|
|
|
.setFKey(fKey) |
|
|
|
.setUrl(url) |
|
|
|
.setCategoryId("0"); |
|
|
|
fileService.save(file); |
|
|
|
if (setting.getValue().equals(SettingConstant.LOCAL_OSS)) { |
|
|
|
OssSetting os = new Gson().fromJson(settingService.get(SettingConstant.LOCAL_OSS).getValue(), OssSetting.class); |
|
|
|
return os.getHttp() + os.getEndpoint() + "/" + file.getId(); |
|
|
|
} |
|
|
|
return url; |
|
|
|
} |
|
|
|
|
|
|
|
private Integer getOssType(String type) { |
|
|
|
switch (type) { |
|
|
|
case SettingConstant.QINIU_OSS: |
|
|
|
return OssConstant.OSS_QINIU; |
|
|
|
case SettingConstant.ALI_OSS: |
|
|
|
return OssConstant.OSS_ALI; |
|
|
|
case SettingConstant.TENCENT_OSS: |
|
|
|
return OssConstant.OSS_TENCENT; |
|
|
|
case SettingConstant.MINIO_OSS: |
|
|
|
return OssConstant.OSS_MINIO; |
|
|
|
case SettingConstant.LOCAL_OSS: |
|
|
|
return OssConstant.OSS_LOCAL; |
|
|
|
default: |
|
|
|
return -1; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void cleanupTempDirectory(Path dir) { |
|
|
|
if (dir == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
try { |
|
|
|
List<Path> paths = Files.walk(dir).sorted(Comparator.reverseOrder()).collect(Collectors.toList()); |
|
|
|
for (Path path : paths) { |
|
|
|
Files.deleteIfExists(path); |
|
|
|
} |
|
|
|
} catch (Exception ignored) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private String firstNotBlank(String first, String second) { |
|
|
|
return CharSequenceUtil.isNotBlank(first) ? first : second; |
|
|
|
} |
|
|
|
@ -788,7 +674,10 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
message.setAuditStatus(IeConstants.AUDIT_BLOCK); |
|
|
|
message.setRiskLevel(IeConstants.RISK_HIGH); |
|
|
|
message.setUpdateTime(new Date()); |
|
|
|
roomMessageMapper.updateById(message); |
|
|
|
if (!finalizeMediaAudit(message)) { |
|
|
|
log.warn("i/e媒体审核已有定论,忽略本次失败结果 messageId={}, reason={}", message.getId(), reason); |
|
|
|
return; |
|
|
|
} |
|
|
|
long riskCount = redisService.increaseRisk(message.getSenderId(), 3600); |
|
|
|
IeMessageAckVO ack = buildAck(message, null); |
|
|
|
ack.setIsBlocked(1); |
|
|
|
@ -802,7 +691,10 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
message.setRiskLevel(IeConstants.RISK_HIGH); |
|
|
|
message.setMediaAuditStatus(IeConstants.MEDIA_AUDIT_BLOCK); |
|
|
|
message.setUpdateTime(new Date()); |
|
|
|
roomMessageMapper.updateById(message); |
|
|
|
if (!finalizeMediaAudit(message)) { |
|
|
|
log.warn("i/e媒体审核已有定论,忽略本次拦截结果 messageId={}, reason={}", message.getId(), reason); |
|
|
|
return; |
|
|
|
} |
|
|
|
long riskCount = redisService.increaseRisk(message.getSenderId(), 3600); |
|
|
|
IeMessageAckVO ack = buildAck(message, null); |
|
|
|
ack.setIsBlocked(1); |
|
|
|
@ -810,16 +702,6 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
publishEvent(IeChatEventType.MESSAGE_BLOCKED, message.getRoomId(), message.getId(), message.getSenderId(), message.getReceiverId(), message.getRiskLevel(), reason + ", riskCount=" + riskCount); |
|
|
|
} |
|
|
|
|
|
|
|
private static class MediaAuditItem { |
|
|
|
private final String url; |
|
|
|
private final Integer type; |
|
|
|
|
|
|
|
private MediaAuditItem(String url, Integer type) { |
|
|
|
this.url = url; |
|
|
|
this.type = type; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void ensureRoomVisibleRecords(Long userId, Long roomId) { |
|
|
|
IeRoom room = getRoom(roomId); |
|
|
|
// 任意一方进入房间都补齐双方记录,保证被匹配方的消息列表同样可见
|
|
|
|
@ -895,11 +777,15 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
if (room == null || (!room.getUserAId().equals(userId) && !room.getUserBId().equals(userId))) { |
|
|
|
throw new RuntimeException("你不在这个陪伴房间中"); |
|
|
|
} |
|
|
|
return calculateRoomStreak(room, true); |
|
|
|
} |
|
|
|
|
|
|
|
private Map<String, Object> calculateRoomStreak(IeRoom room, boolean rewardEnabled) { |
|
|
|
Calendar since = Calendar.getInstance(); |
|
|
|
since.add(Calendar.DAY_OF_YEAR, -STREAK_LOOKBACK_DAYS); |
|
|
|
List<IeRoomMessage> messages = roomMessageMapper.selectList(new LambdaQueryWrapper<IeRoomMessage>() |
|
|
|
.select(IeRoomMessage::getSenderId, IeRoomMessage::getCreateTime) |
|
|
|
.eq(IeRoomMessage::getRoomId, roomId) |
|
|
|
.eq(IeRoomMessage::getRoomId, room.getId()) |
|
|
|
.eq(IeRoomMessage::getIsBlocked, 0) |
|
|
|
.ge(IeRoomMessage::getCreateTime, since.getTime())); |
|
|
|
java.text.SimpleDateFormat dayFormat = new java.text.SimpleDateFormat("yyyyMMdd"); |
|
|
|
@ -928,8 +814,8 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
} |
|
|
|
// 昨天有火花但今天还没续上 → 即将熄灭,提醒用户今天互发一句
|
|
|
|
boolean expiring = !bothToday && streak > 0; |
|
|
|
if (bothToday && streak > 0 && streak % STREAK_REWARD_INTERVAL == 0) { |
|
|
|
String rewardKey = STREAK_REWARD_KEY + roomId + ":" + streak; |
|
|
|
if (rewardEnabled && bothToday && streak > 0 && streak % STREAK_REWARD_INTERVAL == 0) { |
|
|
|
String rewardKey = STREAK_REWARD_KEY + room.getId() + ":" + streak; |
|
|
|
Boolean first = stringRedisTemplate.opsForValue().setIfAbsent(rewardKey, "1", 40, TimeUnit.DAYS); |
|
|
|
if (Boolean.TRUE.equals(first)) { |
|
|
|
matchService.rewardMatchQuota(room.getUserAId(), 1); |
|
|
|
@ -950,17 +836,64 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
|
|
|
|
@Override |
|
|
|
public IPage<IeRecord> pageRecords(Long userId, Integer pageNumber, Integer pageSize) { |
|
|
|
Page<IeRecord> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); |
|
|
|
IPage<IeRecord> result = recordMapper.selectPage(page, new LambdaQueryWrapper<IeRecord>() |
|
|
|
long current = safePageNumber(pageNumber); |
|
|
|
long size = safePageSize(pageSize); |
|
|
|
Page<IeRecord> result = new Page<>(current, size); |
|
|
|
List<IeRecord> records = recordMapper.selectList(new LambdaQueryWrapper<IeRecord>() |
|
|
|
.eq(IeRecord::getUserId, userId) |
|
|
|
.orderByDesc(IeRecord::getUpdateTime) |
|
|
|
.orderByDesc(IeRecord::getCreateTime)); |
|
|
|
for (IeRecord record : result.getRecords()) { |
|
|
|
for (IeRecord record : records) { |
|
|
|
record.setUnreadCount(unreadMessageCount(userId, record.getRoomId(), record.getTargetUserId())); |
|
|
|
fillLatestTargetProfile(record); |
|
|
|
fillRoomStreak(record); |
|
|
|
} |
|
|
|
records.sort((left, right) -> { |
|
|
|
int unreadCompare = Integer.compare(right.getUnreadCount() == null ? 0 : right.getUnreadCount(), |
|
|
|
left.getUnreadCount() == null ? 0 : left.getUnreadCount()); |
|
|
|
if (unreadCompare != 0) { |
|
|
|
return unreadCompare; |
|
|
|
} |
|
|
|
return compareRecordTimeDesc(left, right); |
|
|
|
}); |
|
|
|
int fromIndex = (int) Math.min((current - 1) * size, (long) records.size()); |
|
|
|
int toIndex = (int) Math.min(fromIndex + size, (long) records.size()); |
|
|
|
result.setRecords(records.subList(fromIndex, toIndex)); |
|
|
|
result.setTotal(records.size()); |
|
|
|
result.setPages((records.size() + size - 1L) / size); |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
private int compareRecordTimeDesc(IeRecord left, IeRecord right) { |
|
|
|
Date leftTime = left.getUpdateTime() == null ? left.getCreateTime() : left.getUpdateTime(); |
|
|
|
Date rightTime = right.getUpdateTime() == null ? right.getCreateTime() : right.getUpdateTime(); |
|
|
|
if (leftTime == null && rightTime == null) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
if (leftTime == null) { |
|
|
|
return 1; |
|
|
|
} |
|
|
|
if (rightTime == null) { |
|
|
|
return -1; |
|
|
|
} |
|
|
|
return rightTime.compareTo(leftTime); |
|
|
|
} |
|
|
|
|
|
|
|
private void fillRoomStreak(IeRecord record) { |
|
|
|
if (record == null || record.getRoomId() == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
IeRoom room = getRoom(record.getRoomId()); |
|
|
|
if (room == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
Map<String, Object> streak = calculateRoomStreak(room, false); |
|
|
|
record.setStreakDays((Integer) streak.get("streakDays")); |
|
|
|
record.setStreakBothToday((Boolean) streak.get("bothToday")); |
|
|
|
record.setStreakExpiring((Boolean) streak.get("expiring")); |
|
|
|
record.setStreakRewardInterval((Integer) streak.get("rewardInterval")); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
@Transactional |
|
|
|
public void deleteRecord(Long userId, Long recordId) { |
|
|
|
@ -1024,6 +957,58 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
.orderByDesc(IeReport::getCreateTime)); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public IPage<IeReport> adminPageReports(String regionId, Integer status, Integer pageNumber, Integer pageSize) { |
|
|
|
Page<IeReport> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); |
|
|
|
return reportMapper.selectPage(page, new LambdaQueryWrapper<IeReport>() |
|
|
|
.eq(regionId != null && !regionId.trim().isEmpty(), IeReport::getRegionId, regionId) |
|
|
|
.eq(status != null, IeReport::getStatus, status) |
|
|
|
.orderByAsc(IeReport::getStatus) |
|
|
|
.orderByDesc(IeReport::getCreateTime)); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
@Transactional |
|
|
|
public IeReport handleReport(Long handlerId, Long reportId, Integer status, String handleResult, Boolean blockIe) { |
|
|
|
IeReport report = reportMapper.selectById(reportId); |
|
|
|
if (report == null) { |
|
|
|
throw new RuntimeException("举报记录不存在"); |
|
|
|
} |
|
|
|
Date now = new Date(); |
|
|
|
report.setStatus(status == null ? 1 : status); |
|
|
|
report.setHandleResult(handleResult == null || handleResult.trim().isEmpty() ? "已处理" : handleResult.trim()); |
|
|
|
report.setBlockedIe(Boolean.TRUE.equals(blockIe) ? 1 : 0); |
|
|
|
report.setHandlerId(handlerId); |
|
|
|
report.setUpdateTime(now); |
|
|
|
reportMapper.updateById(report); |
|
|
|
if (Boolean.TRUE.equals(blockIe) && report.getReportedUserId() != null) { |
|
|
|
IeUserProfile profile = findProfile(report.getReportedUserId()); |
|
|
|
if (profile != null) { |
|
|
|
profile.setIsDeleted(1); |
|
|
|
profile.setUpdateTime(now); |
|
|
|
userProfileMapper.updateById(profile); |
|
|
|
} |
|
|
|
} |
|
|
|
return report; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void deleteReport(Long reportId) { |
|
|
|
if (reportId == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
reportMapper.deleteById(reportId); |
|
|
|
} |
|
|
|
|
|
|
|
private IeUserProfile findProfile(Long userId) { |
|
|
|
if (userId == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
return userProfileMapper.selectOne(new LambdaQueryWrapper<IeUserProfile>() |
|
|
|
.eq(IeUserProfile::getUserId, userId) |
|
|
|
.last("limit 1")); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public IPage<IeBlock> pageBlocks(Long userId, Integer pageNumber, Integer pageSize) { |
|
|
|
Page<IeBlock> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); |
|
|
|
@ -1110,11 +1095,6 @@ public class IeChatServiceImpl implements IeChatService { |
|
|
|
return calendar.getTime(); |
|
|
|
} |
|
|
|
|
|
|
|
private String senderOpenid(Long senderId) { |
|
|
|
User user = userDao.findById(String.valueOf(senderId)).orElse(null); |
|
|
|
return user == null ? null : user.getMiniProgramOpenid(); |
|
|
|
} |
|
|
|
|
|
|
|
private void createRecord(IeRoom room, Long userId, Long targetUserId) { |
|
|
|
IeRecord exists = recordMapper.selectOne(new LambdaQueryWrapper<IeRecord>() |
|
|
|
.eq(IeRecord::getRoomId, room.getId()) |
|
|
|
|