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 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-04-24 16:16:33 +08:00
父节点 37f34876be
当前提交 3285dfe79c
共有 10 个文件被更改,包括 178 次插入12 次删除

1
.java-version 普通文件
查看文件

@ -0,0 +1 @@
17

查看文件

@ -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<ApiResponse<Page<ImAccountEntity>>> 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<ApiResponse<ImAccountEntity>> updateUserStatus(
@RequestParam String appId,
@PathVariable String userId,
@RequestBody Map<String, String> 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<ApiResponse<List<ImGroupEntity>>> listGroups(@RequestParam String appId) {
return ResponseEntity.ok(ApiResponse.success(groupRepository.findByAppId(appId)));
}
/** Message statistics for the given appId. */
@GetMapping("/stats")
public ResponseEntity<ApiResponse<Map<String, Object>>> 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
)));
}
}

查看文件

@ -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<ImAccountEntity, String> {
Optional<ImAccountEntity> findByAppIdAndUserId(String appId, String userId);
boolean existsByAppIdAndUserId(String appId, String userId);
Page<ImAccountEntity> findByAppId(String appId, Pageable pageable);
long countByAppId(String appId);
}

查看文件

@ -6,4 +6,5 @@ import java.util.List;
public interface ImGroupRepository extends JpaRepository<ImGroupEntity, String> {
List<ImGroupEntity> findByAppId(String appId);
long countByAppId(String appId);
}

查看文件

@ -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<ImMessageEntity, String> {
Page<ImMessageEntity> findByAppIdAndToIdOrderByCreatedAtDesc(
@ -26,4 +27,13 @@ public interface ImMessageRepository extends JpaRepository<ImMessageEntity, Stri
@Param("userId") String userId,
@Param("peerId") String peerId,
Pageable pageable);
long countByAppId(String appId);
@Query("select count(m) from ImMessageEntity m where m.appId = :appId and m.createdAt >= :since")
long countByAppIdAndCreatedAtAfter(@Param("appId") String appId, @Param("since") LocalDateTime since);
default long countTodayByAppId(String appId) {
return countByAppIdAndCreatedAtAfter(appId, LocalDateTime.now().toLocalDate().atStartOfDay());
}
}

查看文件

@ -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<ApiResponse<AppVersionEntity>> 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<ApiResponse<AppVersionEntity>> 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<ApiResponse<AppVersionEntity>> gray(
@PathVariable String id,
@RequestBody Map<String, Object> 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)));
}

查看文件

@ -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<ApiResponse<List<RnBundleEntity>>> list(
@RequestParam String appId,
@RequestParam(required = false) String moduleId,
@RequestParam(required = false) String platform) {
List<RnBundleEntity> 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<ApiResponse<RnBundleEntity>> 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<ApiResponse<RnBundleEntity>> 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<ApiResponse<RnBundleEntity>> gray(
@PathVariable String id,
@RequestBody Map<String, Object> 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)));
}

查看文件

@ -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; }
}

查看文件

@ -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; }
}

查看文件

@ -9,6 +9,8 @@ import java.util.Optional;
public interface RnBundleRepository extends JpaRepository<RnBundleEntity, String> {
List<RnBundleEntity> findByAppIdAndModuleIdAndPlatformOrderByCreatedAtDesc(
String appId, String moduleId, RnBundleEntity.Platform platform);
List<RnBundleEntity> findByAppIdAndModuleIdOrderByCreatedAtDesc(String appId, String moduleId);
List<RnBundleEntity> findByAppIdOrderByCreatedAtDesc(String appId);
Optional<RnBundleEntity> findTopByAppIdAndModuleIdAndPlatformAndPublishStatusOrderByCreatedAtDesc(
String appId, String moduleId, RnBundleEntity.Platform platform, RnBundleEntity.PublishStatus status);
}