From bbf1fd0769980d0957cf0730d3a5318525215abb Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 28 Apr 2026 17:29:17 +0800 Subject: [PATCH] =?UTF-8?q?docs(server):=20=E6=B7=BB=E5=8A=A0=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=96=87=E6=A1=A3=E6=80=BB=E8=A7=88=E5=B9=B6=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20IM=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PLATFORM_OVERVIEW.md 文档,包含仓库索引、整体架构、核心概念等 - 实现 XuqmImServerSdk 类,提供完整的 IM 功能接口 - 添加登录认证、消息发送、会话管理、好友关系等核心功能 - 实现群组管理、关键词过滤、全局禁言等高级功能 - 提供配置管理和统计查询等管理功能 --- docs/PLATFORM_OVERVIEW.md | 2 + .../java/com/xuqm/im/sdk/XuqmImServerSdk.java | 195 ++++++++++++++++++ .../xuqm/im/controller/ImAdminController.java | 118 ++++++++++- .../xuqm/im/entity/ImGlobalMuteEntity.java | 49 +++++ .../xuqm/im/entity/KeywordFilterEntity.java | 4 + .../xuqm/im/entity/WebhookConfigEntity.java | 4 + .../im/repository/ImGlobalMuteRepository.java | 10 + .../xuqm/im/repository/ImGroupRepository.java | 16 ++ .../im/repository/ImMessageRepository.java | 23 +++ .../repository/KeywordFilterRepository.java | 2 + .../repository/WebhookConfigRepository.java | 2 + .../xuqm/im/service/GlobalMuteService.java | 47 +++++ .../xuqm/im/service/KeywordFilterService.java | 27 ++- .../com/xuqm/im/service/MessageService.java | 6 + .../xuqm/im/service/WebhookConfigService.java | 56 +++++ 15 files changed, 557 insertions(+), 4 deletions(-) create mode 100644 im-service/src/main/java/com/xuqm/im/entity/ImGlobalMuteEntity.java create mode 100644 im-service/src/main/java/com/xuqm/im/repository/ImGlobalMuteRepository.java create mode 100644 im-service/src/main/java/com/xuqm/im/service/GlobalMuteService.java create mode 100644 im-service/src/main/java/com/xuqm/im/service/WebhookConfigService.java diff --git a/docs/PLATFORM_OVERVIEW.md b/docs/PLATFORM_OVERVIEW.md index 6d66226..a4270c8 100644 --- a/docs/PLATFORM_OVERVIEW.md +++ b/docs/PLATFORM_OVERVIEW.md @@ -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 | ## 整体架构 diff --git a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java index bd84561..3bee52b 100644 --- a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java +++ b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java @@ -250,6 +250,161 @@ public final class XuqmImServerSdk { return response.data(); } + public List searchGroups(String keyword, int size) { + ApiResponse> response = request( + "GET", + buildUri("/api/im/admin/groups/search", queryWithSize("keyword", keyword, size)), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public PageResult searchMessages( + String keyword, + String chatType, + String msgType, + LocalDateTime startTime, + LocalDateTime endTime, + int page, + int size) { + Map 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> response = request( + "GET", + buildUri("/api/im/admin/messages/search", query), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public List listWebhookConfigs() { + ApiResponse> 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 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 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>() {} + ); + } + + public List listKeywordFilters() { + ApiResponse> 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 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 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>() {} + ); + } + + public GlobalMuteView getGlobalMute() { + ApiResponse response = request( + "GET", + buildUri("/api/im/admin/global-mute", appQuery()), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public GlobalMuteView setGlobalMute(boolean enabled) { + ApiResponse 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 listFriends() { ApiResponse> 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 memberIds, diff --git a/im-service/src/main/java/com/xuqm/im/controller/ImAdminController.java b/im-service/src/main/java/com/xuqm/im/controller/ImAdminController.java index 552a64f..19936bb 100644 --- a/im-service/src/main/java/com/xuqm/im/controller/ImAdminController.java +++ b/im-service/src/main/java/com/xuqm/im/controller/ImAdminController.java @@ -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>> searchGroups( + @RequestParam String appId, + @RequestParam String keyword, + @RequestParam(defaultValue = "20") int size) { + List 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>> 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>> stats(@RequestParam String appId) { @@ -149,6 +191,80 @@ public class ImAdminController { return ResponseEntity.ok(ApiResponse.ok()); } + @GetMapping("/webhooks") + public ResponseEntity>> listWebhooks(@RequestParam String appId) { + return ResponseEntity.ok(ApiResponse.success(webhookConfigService.list(appId))); + } + + @PostMapping("/webhooks") + public ResponseEntity> 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> 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> deleteWebhook( + @RequestParam String appId, + @PathVariable String id) { + webhookConfigService.delete(appId, id); + return ResponseEntity.ok(ApiResponse.ok()); + } + + @GetMapping("/keyword-filters") + public ResponseEntity>> listKeywordFilters(@RequestParam String appId) { + return ResponseEntity.ok(ApiResponse.success(keywordFilterService.list(appId))); + } + + @PostMapping("/keyword-filters") + public ResponseEntity> 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> 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> deleteKeywordFilter( + @RequestParam String appId, + @PathVariable String id) { + keywordFilterService.delete(appId, id); + return ResponseEntity.ok(ApiResponse.ok()); + } + + @GetMapping("/global-mute") + public ResponseEntity> getGlobalMute(@RequestParam String appId) { + return ResponseEntity.ok(ApiResponse.success(globalMuteService.get(appId))); + } + + @PutMapping("/global-mute") + public ResponseEntity> 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 memberIds) {} + public record WebhookConfigRequest(String url, String secret, Boolean enabled) {} + public record KeywordFilterRequest(String pattern, String replacement, String action, Boolean enabled) {} } diff --git a/im-service/src/main/java/com/xuqm/im/entity/ImGlobalMuteEntity.java b/im-service/src/main/java/com/xuqm/im/entity/ImGlobalMuteEntity.java new file mode 100644 index 0000000..6d050cd --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/entity/ImGlobalMuteEntity.java @@ -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; } +} diff --git a/im-service/src/main/java/com/xuqm/im/entity/KeywordFilterEntity.java b/im-service/src/main/java/com/xuqm/im/entity/KeywordFilterEntity.java index b69ef83..f24ad82 100644 --- a/im-service/src/main/java/com/xuqm/im/entity/KeywordFilterEntity.java +++ b/im-service/src/main/java/com/xuqm/im/entity/KeywordFilterEntity.java @@ -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; } } diff --git a/im-service/src/main/java/com/xuqm/im/entity/WebhookConfigEntity.java b/im-service/src/main/java/com/xuqm/im/entity/WebhookConfigEntity.java index 65d9106..89d6c7a 100644 --- a/im-service/src/main/java/com/xuqm/im/entity/WebhookConfigEntity.java +++ b/im-service/src/main/java/com/xuqm/im/entity/WebhookConfigEntity.java @@ -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; } } diff --git a/im-service/src/main/java/com/xuqm/im/repository/ImGlobalMuteRepository.java b/im-service/src/main/java/com/xuqm/im/repository/ImGlobalMuteRepository.java new file mode 100644 index 0000000..b84b575 --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/repository/ImGlobalMuteRepository.java @@ -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 { + Optional findByAppId(String appId); +} diff --git a/im-service/src/main/java/com/xuqm/im/repository/ImGroupRepository.java b/im-service/src/main/java/com/xuqm/im/repository/ImGroupRepository.java index 1c73761..0fbfe4e 100644 --- a/im-service/src/main/java/com/xuqm/im/repository/ImGroupRepository.java +++ b/im-service/src/main/java/com/xuqm/im/repository/ImGroupRepository.java @@ -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 List 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 searchByKeyword( + @Param("appId") String appId, + @Param("keyword") String keyword, + Pageable pageable); } diff --git a/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java b/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java index b14e06b..22668e4 100644 --- a/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java +++ b/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java @@ -137,6 +137,29 @@ public interface ImMessageRepository extends JpaRepository= :startTime) + and (:endTime is null or m.createdAt <= :endTime) + order by m.createdAt desc + """) + Page 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 diff --git a/im-service/src/main/java/com/xuqm/im/repository/KeywordFilterRepository.java b/im-service/src/main/java/com/xuqm/im/repository/KeywordFilterRepository.java index 37ba8b3..578614f 100644 --- a/im-service/src/main/java/com/xuqm/im/repository/KeywordFilterRepository.java +++ b/im-service/src/main/java/com/xuqm/im/repository/KeywordFilterRepository.java @@ -6,4 +6,6 @@ import java.util.List; public interface KeywordFilterRepository extends JpaRepository { List findByAppIdAndEnabledTrue(String appId); + List findByAppId(String appId); + java.util.Optional findByIdAndAppId(String id, String appId); } diff --git a/im-service/src/main/java/com/xuqm/im/repository/WebhookConfigRepository.java b/im-service/src/main/java/com/xuqm/im/repository/WebhookConfigRepository.java index 58231e0..3a06cd4 100644 --- a/im-service/src/main/java/com/xuqm/im/repository/WebhookConfigRepository.java +++ b/im-service/src/main/java/com/xuqm/im/repository/WebhookConfigRepository.java @@ -6,4 +6,6 @@ import java.util.List; public interface WebhookConfigRepository extends JpaRepository { List findByAppIdAndEnabledTrue(String appId); + List findByAppId(String appId); + java.util.Optional findByIdAndAppId(String id, String appId); } diff --git a/im-service/src/main/java/com/xuqm/im/service/GlobalMuteService.java b/im-service/src/main/java/com/xuqm/im/service/GlobalMuteService.java new file mode 100644 index 0000000..e23c859 --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/service/GlobalMuteService.java @@ -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); + } +} diff --git a/im-service/src/main/java/com/xuqm/im/service/KeywordFilterService.java b/im-service/src/main/java/com/xuqm/im/service/KeywordFilterService.java index d9e1309..3f00cdd 100644 --- a/im-service/src/main/java/com/xuqm/im/service/KeywordFilterService.java +++ b/im-service/src/main/java/com/xuqm/im/service/KeywordFilterService.java @@ -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 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); } } diff --git a/im-service/src/main/java/com/xuqm/im/service/MessageService.java b/im-service/src/main/java/com/xuqm/im/service/MessageService.java index f645e1e..fe024c6 100644 --- a/im-service/src/main/java/com/xuqm/im/service/MessageService.java +++ b/im-service/src/main/java/com/xuqm/im/service/MessageService.java @@ -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); diff --git a/im-service/src/main/java/com/xuqm/im/service/WebhookConfigService.java b/im-service/src/main/java/com/xuqm/im/service/WebhookConfigService.java new file mode 100644 index 0000000..562a349 --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/service/WebhookConfigService.java @@ -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 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); + } +}