docs(server): 添加平台文档总览并实现 IM 服务端 SDK
- 新增 PLATFORM_OVERVIEW.md 文档,包含仓库索引、整体架构、核心概念等 - 实现 XuqmImServerSdk 类,提供完整的 IM 功能接口 - 添加登录认证、消息发送、会话管理、好友关系等核心功能 - 实现群组管理、关键词过滤、全局禁言等高级功能 - 提供配置管理和统计查询等管理功能
这个提交包含在:
父节点
ec23b9890b
当前提交
bbf1fd0769
@ -13,6 +13,8 @@
|
||||
| [XuqmGroup-RNSDK](./rn-sdk/README.md) | TypeScript / RN 0.76+ | https://xuqinmin.com/xuqinmin12/XuqmGroup-RNSDK | React Native SDK |
|
||||
| [XuqmGroup-Vue3SDK](./vue3-sdk/README.md) | TypeScript / Vue 3.5 | https://xuqinmin.com/xuqinmin12/XuqmGroup-Vue3SDK | Vue3 Web SDK |
|
||||
| [XuqmGroup-HarmonySDK](./harmony-sdk/README.md) | ArkTS / HarmonyOS 5 | https://xuqinmin.com/xuqinmin12/XuqmGroup-HarmonySDK | 鸿蒙 SDK |
|
||||
| [XuqmGroup-ServerSDK-Python](../XuqmGroup-ServerSDK-Python/README.md) | Python 3.11+ | https://xuqinmin.com/xuqinmin12/XuqmGroup-ServerSDK-Python | Python 服务端 SDK |
|
||||
| [XuqmGroup-ServerSDK-Go](../XuqmGroup-ServerSDK-Go/README.md) | Go 1.22+ | https://xuqinmin.com/xuqinmin12/XuqmGroup-ServerSDK-Go | Go 服务端 SDK |
|
||||
|
||||
## 整体架构
|
||||
|
||||
|
||||
@ -250,6 +250,161 @@ public final class XuqmImServerSdk {
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public List<GroupView> searchGroups(String keyword, int size) {
|
||||
ApiResponse<List<GroupView>> response = request(
|
||||
"GET",
|
||||
buildUri("/api/im/admin/groups/search", queryWithSize("keyword", keyword, size)),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public PageResult<ImMessage> searchMessages(
|
||||
String keyword,
|
||||
String chatType,
|
||||
String msgType,
|
||||
LocalDateTime startTime,
|
||||
LocalDateTime endTime,
|
||||
int page,
|
||||
int size) {
|
||||
Map<String, String> query = appQuery();
|
||||
if (keyword != null) {
|
||||
query.put("keyword", keyword);
|
||||
}
|
||||
if (chatType != null) {
|
||||
query.put("chatType", chatType);
|
||||
}
|
||||
if (msgType != null) {
|
||||
query.put("msgType", msgType);
|
||||
}
|
||||
if (startTime != null) {
|
||||
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
|
||||
}
|
||||
if (endTime != null) {
|
||||
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
|
||||
}
|
||||
query.put("page", String.valueOf(page));
|
||||
query.put("size", String.valueOf(size));
|
||||
ApiResponse<PageResult<ImMessage>> response = request(
|
||||
"GET",
|
||||
buildUri("/api/im/admin/messages/search", query),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public List<WebhookConfigView> listWebhookConfigs() {
|
||||
ApiResponse<List<WebhookConfigView>> response = request(
|
||||
"GET",
|
||||
buildUri("/api/im/admin/webhooks", appQuery()),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public WebhookConfigView createWebhookConfig(String url, String secret, Boolean enabled) {
|
||||
ApiResponse<WebhookConfigView> response = request(
|
||||
"POST",
|
||||
buildUri("/api/im/admin/webhooks", appQuery()),
|
||||
new WebhookConfigRequest(url, secret, enabled),
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public WebhookConfigView updateWebhookConfig(String id, String url, String secret, Boolean enabled) {
|
||||
ApiResponse<WebhookConfigView> response = request(
|
||||
"PUT",
|
||||
buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()),
|
||||
new WebhookConfigRequest(url, secret, enabled),
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public void deleteWebhookConfig(String id) {
|
||||
request(
|
||||
"DELETE",
|
||||
buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<ApiResponse<Void>>() {}
|
||||
);
|
||||
}
|
||||
|
||||
public List<KeywordFilterView> listKeywordFilters() {
|
||||
ApiResponse<List<KeywordFilterView>> response = request(
|
||||
"GET",
|
||||
buildUri("/api/im/admin/keyword-filters", appQuery()),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public KeywordFilterView createKeywordFilter(String pattern, String replacement, String action, Boolean enabled) {
|
||||
ApiResponse<KeywordFilterView> response = request(
|
||||
"POST",
|
||||
buildUri("/api/im/admin/keyword-filters", appQuery()),
|
||||
new KeywordFilterRequest(pattern, replacement, action, enabled),
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public KeywordFilterView updateKeywordFilter(String id, String pattern, String replacement, String action, Boolean enabled) {
|
||||
ApiResponse<KeywordFilterView> response = request(
|
||||
"PUT",
|
||||
buildUri("/api/im/admin/keyword-filters/" + encode(id), appQuery()),
|
||||
new KeywordFilterRequest(pattern, replacement, action, enabled),
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public void deleteKeywordFilter(String id) {
|
||||
request(
|
||||
"DELETE",
|
||||
buildUri("/api/im/admin/keyword-filters/" + encode(id), appQuery()),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<ApiResponse<Void>>() {}
|
||||
);
|
||||
}
|
||||
|
||||
public GlobalMuteView getGlobalMute() {
|
||||
ApiResponse<GlobalMuteView> response = request(
|
||||
"GET",
|
||||
buildUri("/api/im/admin/global-mute", appQuery()),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public GlobalMuteView setGlobalMute(boolean enabled) {
|
||||
ApiResponse<GlobalMuteView> response = request(
|
||||
"PUT",
|
||||
buildUri("/api/im/admin/global-mute", Map.of("appId", appId, "enabled", String.valueOf(enabled))),
|
||||
null,
|
||||
authorizedHeaders(),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
return response.data();
|
||||
}
|
||||
|
||||
public List<String> listFriends() {
|
||||
ApiResponse<List<String>> response = request(
|
||||
"GET",
|
||||
@ -916,6 +1071,33 @@ public final class XuqmImServerSdk {
|
||||
long todayMessages
|
||||
) {}
|
||||
|
||||
public record WebhookConfigView(
|
||||
String id,
|
||||
String appId,
|
||||
String url,
|
||||
String secret,
|
||||
boolean enabled,
|
||||
Long createdAt
|
||||
) {}
|
||||
|
||||
public record KeywordFilterView(
|
||||
String id,
|
||||
String appId,
|
||||
String pattern,
|
||||
String replacement,
|
||||
String action,
|
||||
boolean enabled,
|
||||
Long createdAt
|
||||
) {}
|
||||
|
||||
public record GlobalMuteView(
|
||||
String id,
|
||||
String appId,
|
||||
boolean enabled,
|
||||
Long createdAt,
|
||||
Long updatedAt
|
||||
) {}
|
||||
|
||||
public record HistoryQuery(
|
||||
String msgType,
|
||||
String keyword,
|
||||
@ -964,6 +1146,19 @@ public final class XuqmImServerSdk {
|
||||
String mentionedUserIds
|
||||
) {}
|
||||
|
||||
public record WebhookConfigRequest(
|
||||
String url,
|
||||
String secret,
|
||||
Boolean enabled
|
||||
) {}
|
||||
|
||||
public record KeywordFilterRequest(
|
||||
String pattern,
|
||||
String replacement,
|
||||
String action,
|
||||
Boolean enabled
|
||||
) {}
|
||||
|
||||
public record CreateGroupRequest(
|
||||
String name,
|
||||
List<String> memberIds,
|
||||
|
||||
@ -2,13 +2,20 @@ package com.xuqm.im.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.im.entity.ImAccountEntity;
|
||||
import com.xuqm.im.entity.ImGlobalMuteEntity;
|
||||
import com.xuqm.im.entity.ImGroupEntity;
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.entity.KeywordFilterEntity;
|
||||
import com.xuqm.im.entity.WebhookConfigEntity;
|
||||
import com.xuqm.im.repository.ImAccountRepository;
|
||||
import com.xuqm.im.repository.ImGroupRepository;
|
||||
import com.xuqm.im.repository.ImMessageRepository;
|
||||
import com.xuqm.im.service.ImAccountService;
|
||||
import com.xuqm.im.service.ImGroupService;
|
||||
import com.xuqm.im.service.GlobalMuteService;
|
||||
import com.xuqm.im.service.KeywordFilterService;
|
||||
import com.xuqm.im.service.MessageService;
|
||||
import com.xuqm.im.service.WebhookConfigService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -28,19 +35,28 @@ public class ImAdminController {
|
||||
private final ImAccountService accountService;
|
||||
private final ImGroupService groupService;
|
||||
private final MessageService messageService;
|
||||
private final WebhookConfigService webhookConfigService;
|
||||
private final KeywordFilterService keywordFilterService;
|
||||
private final GlobalMuteService globalMuteService;
|
||||
|
||||
public ImAdminController(ImAccountRepository accountRepository,
|
||||
ImGroupRepository groupRepository,
|
||||
ImMessageRepository messageRepository,
|
||||
ImAccountService accountService,
|
||||
ImGroupService groupService,
|
||||
MessageService messageService) {
|
||||
MessageService messageService,
|
||||
WebhookConfigService webhookConfigService,
|
||||
KeywordFilterService keywordFilterService,
|
||||
GlobalMuteService globalMuteService) {
|
||||
this.accountRepository = accountRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
this.accountService = accountService;
|
||||
this.groupService = groupService;
|
||||
this.messageService = messageService;
|
||||
this.webhookConfigService = webhookConfigService;
|
||||
this.keywordFilterService = keywordFilterService;
|
||||
this.globalMuteService = globalMuteService;
|
||||
}
|
||||
|
||||
/** List all registered IM users for the given appId. */
|
||||
@ -101,6 +117,32 @@ public class ImAdminController {
|
||||
return ResponseEntity.ok(ApiResponse.success(results));
|
||||
}
|
||||
|
||||
/** Fuzzy search groups by id, name, creator or announcement. */
|
||||
@GetMapping("/groups/search")
|
||||
public ResponseEntity<ApiResponse<List<ImGroupEntity>>> searchGroups(
|
||||
@RequestParam String appId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
List<ImGroupEntity> results = groupRepository.searchByKeyword(appId, keyword, PageRequest.of(0, size));
|
||||
return ResponseEntity.ok(ApiResponse.success(results));
|
||||
}
|
||||
|
||||
/** Search messages across the application. */
|
||||
@GetMapping("/messages/search")
|
||||
public ResponseEntity<ApiResponse<Page<ImMessageEntity>>> searchMessages(
|
||||
@RequestParam String appId,
|
||||
@RequestParam(required = false) ImMessageEntity.ChatType chatType,
|
||||
@RequestParam(required = false) ImMessageEntity.MsgType msgType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) LocalDateTime startTime,
|
||||
@RequestParam(required = false) LocalDateTime endTime,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
messageRepository.searchByKeyword(
|
||||
appId, chatType, msgType, keyword, startTime, endTime, PageRequest.of(page, size))));
|
||||
}
|
||||
|
||||
/** Message statistics for the given appId. */
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> stats(@RequestParam String appId) {
|
||||
@ -149,6 +191,80 @@ public class ImAdminController {
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@GetMapping("/webhooks")
|
||||
public ResponseEntity<ApiResponse<List<WebhookConfigEntity>>> listWebhooks(@RequestParam String appId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(webhookConfigService.list(appId)));
|
||||
}
|
||||
|
||||
@PostMapping("/webhooks")
|
||||
public ResponseEntity<ApiResponse<WebhookConfigEntity>> createWebhook(
|
||||
@RequestParam String appId,
|
||||
@RequestBody WebhookConfigRequest req) {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
webhookConfigService.create(appId, req.url(), req.secret(), req.enabled())));
|
||||
}
|
||||
|
||||
@PutMapping("/webhooks/{id}")
|
||||
public ResponseEntity<ApiResponse<WebhookConfigEntity>> updateWebhook(
|
||||
@RequestParam String appId,
|
||||
@PathVariable String id,
|
||||
@RequestBody WebhookConfigRequest req) {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
webhookConfigService.update(appId, id, req.url(), req.secret(), req.enabled())));
|
||||
}
|
||||
|
||||
@DeleteMapping("/webhooks/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteWebhook(
|
||||
@RequestParam String appId,
|
||||
@PathVariable String id) {
|
||||
webhookConfigService.delete(appId, id);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@GetMapping("/keyword-filters")
|
||||
public ResponseEntity<ApiResponse<List<KeywordFilterEntity>>> listKeywordFilters(@RequestParam String appId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(keywordFilterService.list(appId)));
|
||||
}
|
||||
|
||||
@PostMapping("/keyword-filters")
|
||||
public ResponseEntity<ApiResponse<KeywordFilterEntity>> createKeywordFilter(
|
||||
@RequestParam String appId,
|
||||
@RequestBody KeywordFilterRequest req) {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
keywordFilterService.add(appId, req.pattern(), req.replacement(), req.action())));
|
||||
}
|
||||
|
||||
@PutMapping("/keyword-filters/{id}")
|
||||
public ResponseEntity<ApiResponse<KeywordFilterEntity>> updateKeywordFilter(
|
||||
@RequestParam String appId,
|
||||
@PathVariable String id,
|
||||
@RequestBody KeywordFilterRequest req) {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
keywordFilterService.update(appId, id, req.pattern(), req.replacement(), req.action(), req.enabled())));
|
||||
}
|
||||
|
||||
@DeleteMapping("/keyword-filters/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteKeywordFilter(
|
||||
@RequestParam String appId,
|
||||
@PathVariable String id) {
|
||||
keywordFilterService.delete(appId, id);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@GetMapping("/global-mute")
|
||||
public ResponseEntity<ApiResponse<ImGlobalMuteEntity>> getGlobalMute(@RequestParam String appId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(globalMuteService.get(appId)));
|
||||
}
|
||||
|
||||
@PutMapping("/global-mute")
|
||||
public ResponseEntity<ApiResponse<ImGlobalMuteEntity>> setGlobalMute(
|
||||
@RequestParam String appId,
|
||||
@RequestParam boolean enabled) {
|
||||
return ResponseEntity.ok(ApiResponse.success(globalMuteService.setEnabled(appId, enabled)));
|
||||
}
|
||||
|
||||
public record RegisterUserRequest(String userId, String nickname, String avatar) {}
|
||||
public record CreateGroupRequest(String name, String creatorId, List<String> memberIds) {}
|
||||
public record WebhookConfigRequest(String url, String secret, Boolean enabled) {}
|
||||
public record KeywordFilterRequest(String pattern, String replacement, String action, Boolean enabled) {}
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.xuqm.im.json.EpochMillisLocalDateTimeSerializer;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "im_global_mute")
|
||||
public class ImGlobalMuteEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64, unique = true)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.xuqm.im.json.EpochMillisLocalDateTimeSerializer;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
@ -31,6 +33,7 @@ public class KeywordFilterEntity {
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
@ -51,6 +54,7 @@ public class KeywordFilterEntity {
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.xuqm.im.json.EpochMillisLocalDateTimeSerializer;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
@ -26,6 +28,7 @@ public class WebhookConfigEntity {
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
@ -43,6 +46,7 @@ public class WebhookConfigEntity {
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.ImGlobalMuteEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ImGlobalMuteRepository extends JpaRepository<ImGlobalMuteEntity, String> {
|
||||
Optional<ImGlobalMuteEntity> findByAppId(String appId);
|
||||
}
|
||||
@ -2,6 +2,7 @@ package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.ImGroupEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import java.util.List;
|
||||
@ -19,4 +20,19 @@ public interface ImGroupRepository extends JpaRepository<ImGroupEntity, String>
|
||||
List<ImGroupEntity> findUserGroups(
|
||||
@Param("appId") String appId,
|
||||
@Param("userId") String userId);
|
||||
|
||||
@Query("""
|
||||
select g from ImGroupEntity g
|
||||
where g.appId = :appId
|
||||
and (:keyword is null or :keyword = '' or
|
||||
lower(g.id) like lower(concat('%', :keyword, '%')) or
|
||||
lower(g.name) like lower(concat('%', :keyword, '%')) or
|
||||
lower(g.creatorId) like lower(concat('%', :keyword, '%')) or
|
||||
lower(coalesce(g.announcement, '')) like lower(concat('%', :keyword, '%')))
|
||||
order by g.createdAt desc
|
||||
""")
|
||||
List<ImGroupEntity> searchByKeyword(
|
||||
@Param("appId") String appId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
}
|
||||
|
||||
@ -137,6 +137,29 @@ public interface ImMessageRepository extends JpaRepository<ImMessageEntity, Stri
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("""
|
||||
select m from ImMessageEntity m
|
||||
where m.appId = :appId
|
||||
and (:chatType is null or m.chatType = :chatType)
|
||||
and (:msgType is null or m.msgType = :msgType)
|
||||
and (:keyword is null or :keyword = '' or
|
||||
lower(m.content) like lower(concat('%', :keyword, '%')) or
|
||||
lower(coalesce(m.mentionedUserIds, '')) like lower(concat('%', :keyword, '%')) or
|
||||
lower(m.fromUserId) like lower(concat('%', :keyword, '%')) or
|
||||
lower(m.toId) like lower(concat('%', :keyword, '%')))
|
||||
and (:startTime is null or m.createdAt >= :startTime)
|
||||
and (:endTime is null or m.createdAt <= :endTime)
|
||||
order by m.createdAt desc
|
||||
""")
|
||||
Page<ImMessageEntity> searchByKeyword(
|
||||
@Param("appId") String appId,
|
||||
@Param("chatType") ImMessageEntity.ChatType chatType,
|
||||
@Param("msgType") ImMessageEntity.MsgType msgType,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("""
|
||||
select count(m) from ImMessageEntity m
|
||||
where m.appId = :appId
|
||||
|
||||
@ -6,4 +6,6 @@ import java.util.List;
|
||||
|
||||
public interface KeywordFilterRepository extends JpaRepository<KeywordFilterEntity, String> {
|
||||
List<KeywordFilterEntity> findByAppIdAndEnabledTrue(String appId);
|
||||
List<KeywordFilterEntity> findByAppId(String appId);
|
||||
java.util.Optional<KeywordFilterEntity> findByIdAndAppId(String id, String appId);
|
||||
}
|
||||
|
||||
@ -6,4 +6,6 @@ import java.util.List;
|
||||
|
||||
public interface WebhookConfigRepository extends JpaRepository<WebhookConfigEntity, String> {
|
||||
List<WebhookConfigEntity> findByAppIdAndEnabledTrue(String appId);
|
||||
List<WebhookConfigEntity> findByAppId(String appId);
|
||||
java.util.Optional<WebhookConfigEntity> findByIdAndAppId(String id, String appId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.xuqm.im.entity.ImGlobalMuteEntity;
|
||||
import com.xuqm.im.repository.ImGlobalMuteRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class GlobalMuteService {
|
||||
|
||||
private final ImGlobalMuteRepository repository;
|
||||
|
||||
public GlobalMuteService(ImGlobalMuteRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public boolean isEnabled(String appId) {
|
||||
return repository.findByAppId(appId).map(ImGlobalMuteEntity::isEnabled).orElse(false);
|
||||
}
|
||||
|
||||
public ImGlobalMuteEntity get(String appId) {
|
||||
return repository.findByAppId(appId).orElseGet(() -> {
|
||||
ImGlobalMuteEntity entity = new ImGlobalMuteEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppId(appId);
|
||||
entity.setEnabled(false);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
public ImGlobalMuteEntity setEnabled(String appId, boolean enabled) {
|
||||
ImGlobalMuteEntity entity = repository.findByAppId(appId).orElseGet(() -> {
|
||||
ImGlobalMuteEntity created = new ImGlobalMuteEntity();
|
||||
created.setId(UUID.randomUUID().toString());
|
||||
created.setAppId(appId);
|
||||
created.setCreatedAt(LocalDateTime.now());
|
||||
return created;
|
||||
});
|
||||
entity.setEnabled(enabled);
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
return repository.save(entity);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.im.entity.KeywordFilterEntity;
|
||||
import com.xuqm.im.repository.KeywordFilterRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -51,10 +52,30 @@ public class KeywordFilterService {
|
||||
}
|
||||
|
||||
public List<KeywordFilterEntity> list(String appId) {
|
||||
return repository.findByAppIdAndEnabledTrue(appId);
|
||||
return repository.findByAppId(appId);
|
||||
}
|
||||
|
||||
public void delete(String id) {
|
||||
repository.deleteById(id);
|
||||
public KeywordFilterEntity update(String appId, String id, String pattern, String replacement, String action, Boolean enabled) {
|
||||
KeywordFilterEntity entity = repository.findByIdAndAppId(id, appId)
|
||||
.orElseThrow(() -> new BusinessException(404, "关键词过滤规则不存在"));
|
||||
if (pattern != null) {
|
||||
entity.setPattern(pattern);
|
||||
}
|
||||
if (replacement != null) {
|
||||
entity.setReplacement(replacement);
|
||||
}
|
||||
if (action != null) {
|
||||
entity.setAction(action);
|
||||
}
|
||||
if (enabled != null) {
|
||||
entity.setEnabled(enabled);
|
||||
}
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
public void delete(String appId, String id) {
|
||||
KeywordFilterEntity entity = repository.findByIdAndAppId(id, appId)
|
||||
.orElseThrow(() -> new BusinessException(404, "关键词过滤规则不存在"));
|
||||
repository.delete(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ public class MessageService {
|
||||
private final ImMessageRepository messageRepository;
|
||||
private final WebhookConfigRepository webhookRepository;
|
||||
private final KeywordFilterService keywordFilterService;
|
||||
private final GlobalMuteService globalMuteService;
|
||||
private final ImClusterPublisher clusterPublisher;
|
||||
private final ImGroupService groupService;
|
||||
private final BlacklistService blacklistService;
|
||||
@ -53,6 +54,7 @@ public class MessageService {
|
||||
public MessageService(ImMessageRepository messageRepository,
|
||||
WebhookConfigRepository webhookRepository,
|
||||
KeywordFilterService keywordFilterService,
|
||||
GlobalMuteService globalMuteService,
|
||||
ImClusterPublisher clusterPublisher,
|
||||
ImGroupService groupService,
|
||||
BlacklistService blacklistService,
|
||||
@ -64,6 +66,7 @@ public class MessageService {
|
||||
this.messageRepository = messageRepository;
|
||||
this.webhookRepository = webhookRepository;
|
||||
this.keywordFilterService = keywordFilterService;
|
||||
this.globalMuteService = globalMuteService;
|
||||
this.clusterPublisher = clusterPublisher;
|
||||
this.groupService = groupService;
|
||||
this.blacklistService = blacklistService;
|
||||
@ -75,6 +78,9 @@ public class MessageService {
|
||||
}
|
||||
|
||||
public ImMessageEntity send(String appId, String fromUserId, SendMessageRequest req) {
|
||||
if (globalMuteService.isEnabled(appId)) {
|
||||
throw new BusinessException(403, "当前应用已开启全局禁言");
|
||||
}
|
||||
String content = req.content();
|
||||
if (req.msgType() == ImMessageEntity.MsgType.TEXT) {
|
||||
content = keywordFilterService.filter(appId, content);
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.im.entity.WebhookConfigEntity;
|
||||
import com.xuqm.im.repository.WebhookConfigRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class WebhookConfigService {
|
||||
|
||||
private final WebhookConfigRepository repository;
|
||||
|
||||
public WebhookConfigService(WebhookConfigRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public List<WebhookConfigEntity> list(String appId) {
|
||||
return repository.findByAppId(appId);
|
||||
}
|
||||
|
||||
public WebhookConfigEntity create(String appId, String url, String secret, Boolean enabled) {
|
||||
WebhookConfigEntity entity = new WebhookConfigEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppId(appId);
|
||||
entity.setUrl(url);
|
||||
entity.setSecret(secret);
|
||||
entity.setEnabled(enabled == null || enabled);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
public WebhookConfigEntity update(String appId, String id, String url, String secret, Boolean enabled) {
|
||||
WebhookConfigEntity entity = repository.findByIdAndAppId(id, appId)
|
||||
.orElseThrow(() -> new BusinessException(404, "回调配置不存在"));
|
||||
if (url != null) {
|
||||
entity.setUrl(url);
|
||||
}
|
||||
if (secret != null) {
|
||||
entity.setSecret(secret);
|
||||
}
|
||||
if (enabled != null) {
|
||||
entity.setEnabled(enabled);
|
||||
}
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
public void delete(String appId, String id) {
|
||||
WebhookConfigEntity entity = repository.findByIdAndAppId(id, appId)
|
||||
.orElseThrow(() -> new BusinessException(404, "回调配置不存在"));
|
||||
repository.delete(entity);
|
||||
}
|
||||
}
|
||||
正在加载...
在新工单中引用
屏蔽一个用户