feat(chat): 添加聊天界面和文件更新SDK功能
- 实现完整的聊天界面UI组件,支持文本、图片、视频、音频、文件等多种消息类型 - 集成IM消息收发功能,实现消息气泡显示和用户头像占位符 - 添加媒体文件选择和拍摄功能,支持相册图片、视频及相机拍照录像 - 实现语音录制和播放功能,包含按住说话交互和权限处理 - 添加群组提及功能,支持@用户和提及候选列表显示 - 实现消息回复和引用功能,支持消息长按回复操作 - 添加本地消息搜索功能,支持搜索当前会话的历史消息 - 实现文件上传下载功能,集成FileSDK进行文件传输管理 - 添加应用更新检查功能,集成UpdateSDK支持版本更新 - 实现消息状态显示,包括发送、送达、已读等状态标识 - 添加群组已读人数统计,显示消息在群聊中的阅读情况 - 实现草稿保存和恢复功能,支持断点续聊体验 - 添加连接状态横幅,实时显示IM服务连接状态 - 实现滚动加载更多历史消息,优化大量消息的性能表现 - 添加多媒体文件下载保存功能,支持保存到应用专属目录
这个提交包含在:
父节点
c217e96482
当前提交
1e395171a3
@ -3,6 +3,7 @@ package com.xuqm.im.sdk;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
@ -18,16 +19,20 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public final class XuqmImServerSdk {
|
||||
|
||||
@ -103,6 +108,17 @@ public final class XuqmImServerSdk {
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public ImMessage editMessage(String messageId, String content) {
|
||||
ApiResponse<ImMessage> response = request(
|
||||
"PUT",
|
||||
buildUri("/api/im/messages/" + encode(messageId), Map.of("appId", appId)),
|
||||
Map.of("content", content),
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public List<ConversationView> listConversations(int size) {
|
||||
ApiResponse<List<ConversationView>> response = request(
|
||||
"GET",
|
||||
@ -268,6 +284,42 @@ public final class XuqmImServerSdk {
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public boolean verifyCallbackSignature(String timestamp, String nonce, String body, String signature) {
|
||||
long ts;
|
||||
try {
|
||||
ts = Long.parseLong(timestamp);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (Math.abs(now - ts) > 5 * 60 * 1000L) {
|
||||
return false;
|
||||
}
|
||||
String payload = appId + "\n" + timestamp + "\n" + normalize(nonce) + "\n" + sha256Hex(body);
|
||||
String expected = hmacSha256Hex(appSecret, payload);
|
||||
return MessageDigest.isEqual(
|
||||
expected.getBytes(StandardCharsets.UTF_8),
|
||||
normalize(signature).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
}
|
||||
|
||||
public WebhookCallbackEnvelope parseCallbackEnvelope(String body) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
return new WebhookCallbackEnvelope(
|
||||
text(root, "callbackId"),
|
||||
text(root, "callbackType"),
|
||||
text(root, "callbackEvent"),
|
||||
longValue(root, "requestTime"),
|
||||
root.get("payload"),
|
||||
text(root, "signature"),
|
||||
text(root, "appId")
|
||||
);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new ImSdkException("Invalid callback body", e);
|
||||
}
|
||||
}
|
||||
|
||||
public PageResult<ImMessage> searchMessages(
|
||||
String keyword,
|
||||
String chatType,
|
||||
@ -1030,6 +1082,44 @@ public final class XuqmImServerSdk {
|
||||
);
|
||||
}
|
||||
|
||||
private static String sha256Hex(String value) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(normalize(value).getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(hash);
|
||||
} catch (Exception e) {
|
||||
throw new ImSdkException("Failed to hash callback body", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String hmacSha256Hex(String secret, String payload) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(hash);
|
||||
} catch (Exception e) {
|
||||
throw new ImSdkException("Failed to verify callback signature", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
JsonNode value = node == null ? null : node.get(field);
|
||||
return value == null || value.isNull() ? null : value.asText();
|
||||
}
|
||||
|
||||
private static long longValue(JsonNode node, String field) {
|
||||
JsonNode value = node == null ? null : node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return 0L;
|
||||
}
|
||||
return value.asLong();
|
||||
}
|
||||
|
||||
private Map<String, String> authorizedHeaders() {
|
||||
String token = bearerTokenSupplier == null ? null : bearerTokenSupplier.get();
|
||||
if (token == null || token.isBlank()) {
|
||||
@ -1411,6 +1501,16 @@ public final class XuqmImServerSdk {
|
||||
Long updatedAt
|
||||
) {}
|
||||
|
||||
public record WebhookCallbackEnvelope(
|
||||
String callbackId,
|
||||
String callbackType,
|
||||
String callbackEvent,
|
||||
long requestTime,
|
||||
JsonNode payload,
|
||||
String signature,
|
||||
String appId
|
||||
) {}
|
||||
|
||||
public record AppVersionView(
|
||||
String id,
|
||||
String appId,
|
||||
@ -1480,7 +1580,8 @@ public final class XuqmImServerSdk {
|
||||
String status,
|
||||
String mentionedUserIds,
|
||||
Integer groupReadCount,
|
||||
Long createdAt
|
||||
Long createdAt,
|
||||
Long editedAt
|
||||
) {}
|
||||
|
||||
public record SendMessageRequest(
|
||||
|
||||
@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@ -47,4 +48,12 @@ public class AccountController {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
accountService.updateAccount(appId, userId, nickname, avatar, gender)));
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<ApiResponse<List<ImAccountEntity>>> search(
|
||||
@RequestParam String appId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return ResponseEntity.ok(ApiResponse.success(accountService.searchAccounts(appId, keyword, size)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,14 @@ public class GroupController {
|
||||
return ResponseEntity.ok(ApiResponse.success(groupService.listPublicGroups(appId, keyword)));
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<ApiResponse<List<ImGroupEntity>>> search(
|
||||
@RequestParam String appId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return ResponseEntity.ok(ApiResponse.success(groupService.searchGroups(appId, keyword, size)));
|
||||
}
|
||||
|
||||
@PutMapping("/{groupId}")
|
||||
public ResponseEntity<ApiResponse<ImGroupEntity>> update(
|
||||
@PathVariable String groupId,
|
||||
|
||||
@ -2,6 +2,7 @@ package com.xuqm.im.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.model.EditMessageRequest;
|
||||
import com.xuqm.im.model.SendMessageRequest;
|
||||
import com.xuqm.im.service.MessageService;
|
||||
import jakarta.validation.Valid;
|
||||
@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@ -44,6 +46,15 @@ public class MessageController {
|
||||
return ResponseEntity.ok(ApiResponse.success(messageService.revoke(appId, id, userId)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<ImMessageEntity>> edit(
|
||||
@PathVariable String id,
|
||||
@Valid @RequestBody EditMessageRequest req,
|
||||
@AuthenticationPrincipal String userId,
|
||||
@RequestParam String appId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(messageService.edit(appId, id, userId, req)));
|
||||
}
|
||||
|
||||
@GetMapping("/history/{toId}")
|
||||
public ResponseEntity<ApiResponse<?>> history(
|
||||
@PathVariable String toId,
|
||||
|
||||
@ -61,6 +61,11 @@ public class ImMessageEntity {
|
||||
@Transient
|
||||
private Integer groupReadCount;
|
||||
|
||||
@Column
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = EpochMillisLocalDateTimeDeserializer.class)
|
||||
private LocalDateTime editedAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = EpochMillisLocalDateTimeDeserializer.class)
|
||||
@ -96,6 +101,11 @@ public class ImMessageEntity {
|
||||
public Integer getGroupReadCount() { return groupReadCount; }
|
||||
public void setGroupReadCount(Integer groupReadCount) { this.groupReadCount = groupReadCount; }
|
||||
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = EpochMillisLocalDateTimeDeserializer.class)
|
||||
public LocalDateTime getEditedAt() { return editedAt; }
|
||||
public void setEditedAt(LocalDateTime editedAt) { this.editedAt = editedAt; }
|
||||
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = EpochMillisLocalDateTimeDeserializer.class)
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package com.xuqm.im.model;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record EditMessageRequest(
|
||||
@NotBlank String content
|
||||
) {}
|
||||
@ -5,11 +5,13 @@ import com.xuqm.common.security.AppRequestSignatureUtil;
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import com.xuqm.im.entity.ImAccountEntity;
|
||||
import com.xuqm.im.repository.ImAccountRepository;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@ -87,4 +89,8 @@ public class ImAccountService {
|
||||
if (gender != null) account.setGender(gender);
|
||||
return accountRepository.save(account);
|
||||
}
|
||||
|
||||
public List<ImAccountEntity> searchAccounts(String appId, String keyword, int size) {
|
||||
return accountRepository.searchByKeyword(appId, keyword, PageRequest.of(0, Math.max(size, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,18 +2,24 @@ package com.xuqm.im.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.im.cluster.ImClusterPublisher;
|
||||
import com.xuqm.im.entity.ImGroupEntity;
|
||||
import com.xuqm.im.entity.ImGroupJoinRequestEntity;
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.entity.ImGroupMuteEntity;
|
||||
import com.xuqm.im.repository.ImGroupJoinRequestRepository;
|
||||
import com.xuqm.im.repository.ImGroupRepository;
|
||||
import com.xuqm.im.repository.ImGroupMuteRepository;
|
||||
import com.xuqm.im.repository.ImMessageRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@ -24,15 +30,21 @@ public class ImGroupService {
|
||||
private final ImGroupRepository groupRepository;
|
||||
private final ImGroupMuteRepository muteRepository;
|
||||
private final ImGroupJoinRequestRepository joinRequestRepository;
|
||||
private final ImMessageRepository messageRepository;
|
||||
private final ImClusterPublisher clusterPublisher;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ImGroupService(ImGroupRepository groupRepository,
|
||||
ImGroupMuteRepository muteRepository,
|
||||
ImGroupJoinRequestRepository joinRequestRepository,
|
||||
ImMessageRepository messageRepository,
|
||||
ImClusterPublisher clusterPublisher,
|
||||
ObjectMapper objectMapper) {
|
||||
this.groupRepository = groupRepository;
|
||||
this.muteRepository = muteRepository;
|
||||
this.joinRequestRepository = joinRequestRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
this.clusterPublisher = clusterPublisher;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@ -189,6 +201,10 @@ public class ImGroupService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<ImGroupEntity> searchGroups(String appId, String keyword, int size) {
|
||||
return groupRepository.searchByKeyword(appId, keyword, PageRequest.of(0, Math.max(size, 1)));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ImGroupJoinRequestEntity sendJoinRequest(String appId, String groupId, String requesterId, String remark) {
|
||||
ImGroupEntity group = get(groupId);
|
||||
@ -211,7 +227,17 @@ public class ImGroupService {
|
||||
entity.setRemark(remark);
|
||||
entity.setStatus(ImGroupJoinRequestEntity.Status.PENDING.name());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return joinRequestRepository.save(entity);
|
||||
ImGroupJoinRequestEntity saved = joinRequestRepository.save(entity);
|
||||
publishJoinRequestNotification(
|
||||
group,
|
||||
requesterId,
|
||||
uniqueRecipients(group),
|
||||
"GROUP_JOIN_REQUEST",
|
||||
"入群申请",
|
||||
buildDescription("入群申请", remark),
|
||||
saved
|
||||
);
|
||||
return saved;
|
||||
});
|
||||
}
|
||||
|
||||
@ -228,9 +254,18 @@ public class ImGroupService {
|
||||
ensureCanManage(group, operatorId);
|
||||
request.setStatus(ImGroupJoinRequestEntity.Status.ACCEPTED.name());
|
||||
request.setReviewedAt(LocalDateTime.now());
|
||||
joinRequestRepository.save(request);
|
||||
ImGroupJoinRequestEntity saved = joinRequestRepository.save(request);
|
||||
addMemberInternal(group, request.getRequesterId());
|
||||
return request;
|
||||
publishJoinRequestNotification(
|
||||
group,
|
||||
operatorId,
|
||||
List.of(request.getRequesterId()),
|
||||
"GROUP_JOIN_REQUEST_STATUS",
|
||||
"入群申请已通过",
|
||||
buildDescription("入群申请已通过", null),
|
||||
saved
|
||||
);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@ -240,7 +275,17 @@ public class ImGroupService {
|
||||
ensureCanManage(group, operatorId);
|
||||
request.setStatus(ImGroupJoinRequestEntity.Status.REJECTED.name());
|
||||
request.setReviewedAt(LocalDateTime.now());
|
||||
return joinRequestRepository.save(request);
|
||||
ImGroupJoinRequestEntity saved = joinRequestRepository.save(request);
|
||||
publishJoinRequestNotification(
|
||||
group,
|
||||
operatorId,
|
||||
List.of(request.getRequesterId()),
|
||||
"GROUP_JOIN_REQUEST_STATUS",
|
||||
"入群申请已拒绝",
|
||||
buildDescription("入群申请已拒绝", null),
|
||||
saved
|
||||
);
|
||||
return saved;
|
||||
}
|
||||
|
||||
private String toJson(List<String> list) {
|
||||
@ -258,6 +303,67 @@ public class ImGroupService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> uniqueRecipients(ImGroupEntity group) {
|
||||
LinkedHashSet<String> recipients = new LinkedHashSet<>(fromJson(group.getAdminIds()));
|
||||
recipients.add(group.getCreatorId());
|
||||
return new ArrayList<>(recipients);
|
||||
}
|
||||
|
||||
private void publishJoinRequestNotification(
|
||||
ImGroupEntity group,
|
||||
String fromUserId,
|
||||
List<String> recipients,
|
||||
String type,
|
||||
String title,
|
||||
String content,
|
||||
ImGroupJoinRequestEntity request
|
||||
) {
|
||||
for (String recipient : recipients) {
|
||||
if (recipient == null || recipient.isBlank() || recipient.equals(fromUserId)) continue;
|
||||
ImMessageEntity message = new ImMessageEntity();
|
||||
message.setId(UUID.randomUUID().toString());
|
||||
message.setAppId(group.getAppId());
|
||||
message.setFromUserId(fromUserId);
|
||||
message.setToId(recipient);
|
||||
message.setChatType(ImMessageEntity.ChatType.SINGLE);
|
||||
message.setMsgType(ImMessageEntity.MsgType.NOTIFY);
|
||||
message.setContent(buildNotificationContent(type, title, content, request, group));
|
||||
message.setStatus(ImMessageEntity.MsgStatus.SENT);
|
||||
message.setCreatedAt(LocalDateTime.now());
|
||||
ImMessageEntity saved = messageRepository.save(message);
|
||||
clusterPublisher.publish("/user/" + recipient + "/queue/messages", saved);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildNotificationContent(
|
||||
String type,
|
||||
String title,
|
||||
String content,
|
||||
ImGroupJoinRequestEntity request,
|
||||
ImGroupEntity group
|
||||
) {
|
||||
ObjectNode node = objectMapper.createObjectNode();
|
||||
node.put("type", type);
|
||||
node.put("title", title);
|
||||
node.put("content", content);
|
||||
node.put("requestId", request.getId());
|
||||
node.put("groupId", request.getGroupId());
|
||||
node.put("groupName", group.getName());
|
||||
node.put("requesterId", request.getRequesterId());
|
||||
node.put("status", request.getStatus());
|
||||
if (request.getRemark() != null) {
|
||||
node.put("remark", request.getRemark());
|
||||
}
|
||||
return node.toString();
|
||||
}
|
||||
|
||||
private String buildDescription(String prefix, String remark) {
|
||||
if (remark == null || remark.isBlank()) {
|
||||
return prefix;
|
||||
}
|
||||
return prefix + ":" + remark;
|
||||
}
|
||||
|
||||
private void addMemberInternal(ImGroupEntity group, String userId) {
|
||||
List<String> members = fromJson(group.getMemberIds());
|
||||
if (!members.contains(userId)) {
|
||||
|
||||
@ -7,6 +7,7 @@ import com.xuqm.im.entity.ImGroupEntity;
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.entity.WebhookConfigEntity;
|
||||
import com.xuqm.im.model.ConversationView;
|
||||
import com.xuqm.im.model.EditMessageRequest;
|
||||
import com.xuqm.im.model.SendMessageRequest;
|
||||
import com.xuqm.im.repository.ImFriendRepository;
|
||||
import com.xuqm.im.repository.WebhookConfigRepository;
|
||||
@ -190,6 +191,61 @@ public class MessageService {
|
||||
return saved;
|
||||
}
|
||||
|
||||
public ImMessageEntity edit(String appId, String messageId, String requestUserId, EditMessageRequest req) {
|
||||
ImMessageEntity message = messageRepository.findById(messageId)
|
||||
.orElseThrow(() -> new BusinessException(404, "消息不存在"));
|
||||
if (!message.getAppId().equals(appId)) {
|
||||
throw new BusinessException(403, "无权操作");
|
||||
}
|
||||
if (!message.getFromUserId().equals(requestUserId)) {
|
||||
throw new BusinessException(403, "只能编辑自己发送的消息");
|
||||
}
|
||||
if (message.getStatus() == ImMessageEntity.MsgStatus.REVOKED || message.getMsgType() == ImMessageEntity.MsgType.REVOKED) {
|
||||
throw new BusinessException(400, "已撤回消息不能编辑");
|
||||
}
|
||||
if (message.getMsgType() != ImMessageEntity.MsgType.TEXT) {
|
||||
throw new BusinessException(400, "仅支持编辑文本消息");
|
||||
}
|
||||
|
||||
String content = keywordFilterService.filter(appId, req.content());
|
||||
if (content == null) {
|
||||
throw new BusinessException("消息包含违禁内容");
|
||||
}
|
||||
|
||||
message.setContent(content);
|
||||
message.setEditedAt(LocalDateTime.now());
|
||||
ImMessageEntity saved = messageRepository.save(message);
|
||||
|
||||
if (saved.getChatType() == ImMessageEntity.ChatType.SINGLE) {
|
||||
clusterPublisher.publish("/user/" + saved.getToId() + "/queue/messages", saved);
|
||||
if (!saved.getFromUserId().equals(saved.getToId())) {
|
||||
clusterPublisher.publish("/user/" + saved.getFromUserId() + "/queue/messages", saved);
|
||||
}
|
||||
pushBridgeClient.notifyUsers(
|
||||
appId,
|
||||
List.of(saved.getToId()),
|
||||
"消息已编辑",
|
||||
saved.getContent(),
|
||||
buildPushPayload(saved)
|
||||
);
|
||||
} else {
|
||||
clusterPublisher.publish("/topic/group/" + saved.getToId(), saved);
|
||||
List<String> memberIds = groupService.memberIds(groupService.get(saved.getToId()));
|
||||
pushBridgeClient.notifyUsers(
|
||||
appId,
|
||||
memberIds.stream()
|
||||
.filter(memberId -> !memberId.equals(saved.getFromUserId()))
|
||||
.toList(),
|
||||
"群消息已编辑",
|
||||
saved.getContent(),
|
||||
buildPushPayload(saved)
|
||||
);
|
||||
}
|
||||
|
||||
dispatchWebhooks(appId, saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public ImMessageEntity adminRevoke(String appId, String messageId) {
|
||||
ImMessageEntity message = messageRepository.findById(messageId)
|
||||
.orElseThrow(() -> new BusinessException(404, "消息不存在"));
|
||||
@ -371,7 +427,8 @@ public class MessageService {
|
||||
"fromUserId", message.getFromUserId(),
|
||||
"toId", message.getToId(),
|
||||
"chatType", message.getChatType().name(),
|
||||
"msgType", message.getMsgType().name()
|
||||
"msgType", message.getMsgType().name(),
|
||||
"editedAt", message.getEditedAt() == null ? null : message.getEditedAt().toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
return "{}";
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户