From 3285dfe79ca3597ae24dcbf7475e1d951ad73225 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Fri, 24 Apr 2026 16:16:33 +0800 Subject: [PATCH] feat: admin APIs for IM management and version management - ImAdminController: list users (paged), ban/unban, list groups, message stats - ImMessageRepository: countByAppId, countTodayByAppId - AppVersionController: unpublish + gray release (grayEnabled, grayPercent) - RnBundleController: list, unpublish, gray release - AppVersionEntity/RnBundleEntity: add grayEnabled, grayPercent fields Co-Authored-By: Claude Sonnet 4.6 --- .java-version | 1 + .../xuqm/im/controller/ImAdminController.java | 76 +++++++++++++++++++ .../im/repository/ImAccountRepository.java | 4 + .../xuqm/im/repository/ImGroupRepository.java | 1 + .../im/repository/ImMessageRepository.java | 10 +++ .../controller/AppVersionController.java | 27 +++++-- .../update/controller/RnBundleController.java | 45 +++++++++-- .../xuqm/update/entity/AppVersionEntity.java | 12 +++ .../xuqm/update/entity/RnBundleEntity.java | 12 +++ .../update/repository/RnBundleRepository.java | 2 + 10 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 .java-version create mode 100644 im-service/src/main/java/com/xuqm/im/controller/ImAdminController.java diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..98d9bcb --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17 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 new file mode 100644 index 0000000..e60080b --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/controller/ImAdminController.java @@ -0,0 +1,76 @@ +package com.xuqm.im.controller; + +import com.xuqm.common.model.ApiResponse; +import com.xuqm.im.entity.ImAccountEntity; +import com.xuqm.im.entity.ImGroupEntity; +import com.xuqm.im.repository.ImAccountRepository; +import com.xuqm.im.repository.ImGroupRepository; +import com.xuqm.im.repository.ImMessageRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/im/admin") +public class ImAdminController { + + private final ImAccountRepository accountRepository; + private final ImGroupRepository groupRepository; + private final ImMessageRepository messageRepository; + + public ImAdminController(ImAccountRepository accountRepository, + ImGroupRepository groupRepository, + ImMessageRepository messageRepository) { + this.accountRepository = accountRepository; + this.groupRepository = groupRepository; + this.messageRepository = messageRepository; + } + + /** List all registered IM users for the given appId. */ + @GetMapping("/users") + public ResponseEntity>> listUsers( + @RequestParam String appId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.success( + accountRepository.findByAppId(appId, PageRequest.of(page, size)))); + } + + /** Ban or unban a user. */ + @PutMapping("/users/{userId}/status") + public ResponseEntity> updateUserStatus( + @RequestParam String appId, + @PathVariable String userId, + @RequestBody Map body) { + ImAccountEntity account = accountRepository.findByAppIdAndUserId(appId, userId) + .orElseThrow(() -> new RuntimeException("User not found")); + account.setStatus(ImAccountEntity.Status.valueOf(body.get("status").toUpperCase())); + return ResponseEntity.ok(ApiResponse.success(accountRepository.save(account))); + } + + /** List all groups for the given appId. */ + @GetMapping("/groups") + public ResponseEntity>> listGroups(@RequestParam String appId) { + return ResponseEntity.ok(ApiResponse.success(groupRepository.findByAppId(appId))); + } + + /** Message statistics for the given appId. */ + @GetMapping("/stats") + public ResponseEntity>> stats(@RequestParam String appId) { + long totalMessages = messageRepository.countByAppId(appId); + long totalUsers = accountRepository.countByAppId(appId); + long totalGroups = groupRepository.countByAppId(appId); + long todayMessages = messageRepository.countTodayByAppId(appId); + + return ResponseEntity.ok(ApiResponse.success(Map.of( + "totalMessages", totalMessages, + "totalUsers", totalUsers, + "totalGroups", totalGroups, + "todayMessages", todayMessages + ))); + } +} diff --git a/im-service/src/main/java/com/xuqm/im/repository/ImAccountRepository.java b/im-service/src/main/java/com/xuqm/im/repository/ImAccountRepository.java index 3cceb19..3e7e889 100644 --- a/im-service/src/main/java/com/xuqm/im/repository/ImAccountRepository.java +++ b/im-service/src/main/java/com/xuqm/im/repository/ImAccountRepository.java @@ -1,10 +1,14 @@ package com.xuqm.im.repository; import com.xuqm.im.entity.ImAccountEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface ImAccountRepository extends JpaRepository { Optional findByAppIdAndUserId(String appId, String userId); boolean existsByAppIdAndUserId(String appId, String userId); + Page findByAppId(String appId, Pageable pageable); + long countByAppId(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 39b9a34..634313b 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 @@ -6,4 +6,5 @@ import java.util.List; public interface ImGroupRepository extends JpaRepository { List findByAppId(String appId); + long countByAppId(String appId); } 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 48dbdda..6249636 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 @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; public interface ImMessageRepository extends JpaRepository { Page findByAppIdAndToIdOrderByCreatedAtDesc( @@ -26,4 +27,13 @@ public interface ImMessageRepository extends JpaRepository= :since") + long countByAppIdAndCreatedAtAfter(@Param("appId") String appId, @Param("since") LocalDateTime since); + + default long countTodayByAppId(String appId) { + return countByAppIdAndCreatedAtAfter(appId, LocalDateTime.now().toLocalDate().atStartOfDay()); + } } diff --git a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java index 46b1ceb..eb4e29d 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java @@ -5,12 +5,7 @@ import com.xuqm.update.entity.AppVersionEntity; import com.xuqm.update.repository.AppVersionRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -104,6 +99,26 @@ public class AppVersionController { public ResponseEntity> publish(@PathVariable String id) { AppVersionEntity entity = versionRepository.findById(id).orElseThrow(); entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED); + entity.setGrayEnabled(false); + entity.setGrayPercent(0); + return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity))); + } + + @PostMapping("/app/{id}/unpublish") + public ResponseEntity> unpublish(@PathVariable String id) { + AppVersionEntity entity = versionRepository.findById(id).orElseThrow(); + entity.setPublishStatus(AppVersionEntity.PublishStatus.DEPRECATED); + return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity))); + } + + @PostMapping("/app/{id}/gray") + public ResponseEntity> gray( + @PathVariable String id, + @RequestBody Map body) { + AppVersionEntity entity = versionRepository.findById(id).orElseThrow(); + entity.setGrayEnabled(Boolean.TRUE.equals(body.get("enabled"))); + entity.setGrayPercent(body.get("percent") instanceof Number n ? n.intValue() : 0); + entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED); return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity))); } diff --git a/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java b/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java index e11670a..4b5e59e 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java @@ -5,12 +5,7 @@ import com.xuqm.update.entity.RnBundleEntity; import com.xuqm.update.repository.RnBundleRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -21,6 +16,7 @@ import java.security.DigestInputStream; import java.security.MessageDigest; import java.time.LocalDateTime; import java.util.HexFormat; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -102,10 +98,47 @@ public class RnBundleController { return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); } + @GetMapping("/list") + public ResponseEntity>> list( + @RequestParam String appId, + @RequestParam(required = false) String moduleId, + @RequestParam(required = false) String platform) { + List result; + if (moduleId != null && platform != null) { + RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase()); + result = bundleRepository.findByAppIdAndModuleIdAndPlatformOrderByCreatedAtDesc(appId, moduleId, p); + } else if (moduleId != null) { + result = bundleRepository.findByAppIdAndModuleIdOrderByCreatedAtDesc(appId, moduleId); + } else { + result = bundleRepository.findByAppIdOrderByCreatedAtDesc(appId); + } + return ResponseEntity.ok(ApiResponse.success(result)); + } + @PostMapping("/{id}/publish") public ResponseEntity> publish(@PathVariable String id) { RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); entity.setPublishStatus(RnBundleEntity.PublishStatus.PUBLISHED); + entity.setGrayEnabled(false); + entity.setGrayPercent(0); + return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); + } + + @PostMapping("/{id}/unpublish") + public ResponseEntity> unpublish(@PathVariable String id) { + RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); + entity.setPublishStatus(RnBundleEntity.PublishStatus.DEPRECATED); + return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); + } + + @PostMapping("/{id}/gray") + public ResponseEntity> gray( + @PathVariable String id, + @RequestBody Map body) { + RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); + entity.setGrayEnabled(Boolean.TRUE.equals(body.get("enabled"))); + entity.setGrayPercent(body.get("percent") instanceof Number n ? n.intValue() : 0); + entity.setPublishStatus(RnBundleEntity.PublishStatus.PUBLISHED); return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); } diff --git a/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java b/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java index 474960e..00a9588 100644 --- a/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java +++ b/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java @@ -50,6 +50,12 @@ public class AppVersionEntity { @Column(length = 256) private String marketUrl; + @Column(nullable = false) + private boolean grayEnabled = false; + + @Column(nullable = false) + private int grayPercent = 0; + @Column(nullable = false) private LocalDateTime createdAt; @@ -86,6 +92,12 @@ public class AppVersionEntity { public String getMarketUrl() { return marketUrl; } public void setMarketUrl(String marketUrl) { this.marketUrl = marketUrl; } + public boolean isGrayEnabled() { return grayEnabled; } + public void setGrayEnabled(boolean grayEnabled) { this.grayEnabled = grayEnabled; } + + public int getGrayPercent() { return grayPercent; } + public void setGrayPercent(int grayPercent) { this.grayPercent = grayPercent; } + public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } } diff --git a/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java b/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java index 85fefc0..c063569 100644 --- a/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java +++ b/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java @@ -47,6 +47,12 @@ public class RnBundleEntity { @Column(nullable = false, length = 16) private PublishStatus publishStatus; + @Column(nullable = false) + private boolean grayEnabled = false; + + @Column(nullable = false) + private int grayPercent = 0; + @Column(nullable = false) private LocalDateTime createdAt; @@ -80,6 +86,12 @@ public class RnBundleEntity { public PublishStatus getPublishStatus() { return publishStatus; } public void setPublishStatus(PublishStatus publishStatus) { this.publishStatus = publishStatus; } + public boolean isGrayEnabled() { return grayEnabled; } + public void setGrayEnabled(boolean grayEnabled) { this.grayEnabled = grayEnabled; } + + public int getGrayPercent() { return grayPercent; } + public void setGrayPercent(int grayPercent) { this.grayPercent = grayPercent; } + public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } } diff --git a/update-service/src/main/java/com/xuqm/update/repository/RnBundleRepository.java b/update-service/src/main/java/com/xuqm/update/repository/RnBundleRepository.java index 0a888e0..1601f16 100644 --- a/update-service/src/main/java/com/xuqm/update/repository/RnBundleRepository.java +++ b/update-service/src/main/java/com/xuqm/update/repository/RnBundleRepository.java @@ -9,6 +9,8 @@ import java.util.Optional; public interface RnBundleRepository extends JpaRepository { List findByAppIdAndModuleIdAndPlatformOrderByCreatedAtDesc( String appId, String moduleId, RnBundleEntity.Platform platform); + List findByAppIdAndModuleIdOrderByCreatedAtDesc(String appId, String moduleId); + List findByAppIdOrderByCreatedAtDesc(String appId); Optional findTopByAppIdAndModuleIdAndPlatformAndPublishStatusOrderByCreatedAtDesc( String appId, String moduleId, RnBundleEntity.Platform platform, RnBundleEntity.PublishStatus status); }