package com.xuqm.im.controller; import com.xuqm.common.model.ApiResponse; import com.xuqm.common.exception.BusinessException; import com.xuqm.im.entity.ImBlacklistEntity; import com.xuqm.im.entity.ImAccountEntity; import com.xuqm.im.entity.ImGlobalMuteEntity; import com.xuqm.im.entity.ImGroupEntity; import com.xuqm.im.entity.ImFriendRequestEntity; import com.xuqm.im.entity.ImGroupJoinRequestEntity; 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.BlacklistService; import com.xuqm.im.service.FriendRequestService; 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.OperationLogService; import com.xuqm.im.service.WebhookConfigService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; 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; private final ImAccountService accountService; private final FriendRequestService friendRequestService; private final BlacklistService blacklistService; private final ImGroupService groupService; private final MessageService messageService; private final WebhookConfigService webhookConfigService; private final KeywordFilterService keywordFilterService; private final GlobalMuteService globalMuteService; private final OperationLogService operationLogService; public ImAdminController(ImAccountRepository accountRepository, ImGroupRepository groupRepository, ImMessageRepository messageRepository, ImAccountService accountService, FriendRequestService friendRequestService, BlacklistService blacklistService, ImGroupService groupService, MessageService messageService, WebhookConfigService webhookConfigService, KeywordFilterService keywordFilterService, GlobalMuteService globalMuteService, OperationLogService operationLogService) { this.accountRepository = accountRepository; this.groupRepository = groupRepository; this.messageRepository = messageRepository; this.accountService = accountService; this.friendRequestService = friendRequestService; this.blacklistService = blacklistService; this.groupService = groupService; this.messageService = messageService; this.webhookConfigService = webhookConfigService; this.keywordFilterService = keywordFilterService; this.globalMuteService = globalMuteService; this.operationLogService = operationLogService; } /** 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, @AuthenticationPrincipal String operatorId, @RequestBody Map body) { ImAccountEntity account = accountRepository.findByAppIdAndUserId(appId, userId) .orElseThrow(() -> new BusinessException(404, "账号不存在")); String status = body.get("status"); if (status == null || status.isBlank()) { throw new BusinessException(400, "状态不能为空"); } account.setStatus(ImAccountEntity.Status.valueOf(status.toUpperCase())); ImAccountEntity saved = accountRepository.save(account); operationLogService.record(appId, operatorId, "UPDATE_USER_STATUS", "ACCOUNT", userId, status); return ResponseEntity.ok(ApiResponse.success(saved)); } /** Update a registered IM user profile without changing userId. */ @PutMapping("/users/{userId}") public ResponseEntity> updateUser( @RequestParam String appId, @PathVariable String userId, @AuthenticationPrincipal String operatorId, @RequestBody UpdateUserRequest req) { ImAccountEntity saved = accountService.updateAccount( appId, userId, req.nickname(), req.avatar(), req.gender(), req.status()); operationLogService.record(appId, operatorId, "UPDATE_USER", "ACCOUNT", userId, req.nickname()); return ResponseEntity.ok(ApiResponse.success(saved)); } /** List all groups for the given appId. */ @GetMapping("/groups") public ResponseEntity>> listGroups(@RequestParam String appId) { return ResponseEntity.ok(ApiResponse.success(groupRepository.findByAppId(appId))); } /** Admin registers a new IM user (or returns existing). */ @PostMapping("/users") public ResponseEntity> registerUser( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestBody RegisterUserRequest req) { ImAccountEntity account = accountService.importAccount( appId, req.userId(), req.nickname(), req.avatar(), req.gender(), req.status()); operationLogService.record(appId, operatorId, "REGISTER_USER", "ACCOUNT", req.userId(), req.nickname()); return ResponseEntity.ok(ApiResponse.success(account)); } /** Admin creates a group. */ @PostMapping("/groups") public ResponseEntity> createGroup( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestBody CreateGroupRequest req) { ImGroupEntity group = groupService.create( appId, req.name(), req.creatorId(), req.memberIds(), req.groupType(), req.announcement()); operationLogService.record(appId, operatorId, "CREATE_GROUP", "GROUP", group.getId(), group.getName()); return ResponseEntity.ok(ApiResponse.success(group)); } /** Admin updates a group without changing its id. */ @PutMapping("/groups/{groupId}") public ResponseEntity> updateGroup( @RequestParam String appId, @PathVariable String groupId, @AuthenticationPrincipal String operatorId, @RequestBody UpdateGroupRequest req) { ImGroupEntity group = groupService.update( groupId, operatorId, req.name(), req.groupType(), req.announcement()); operationLogService.record(appId, operatorId, "UPDATE_GROUP", "GROUP", groupId, req.name()); return ResponseEntity.ok(ApiResponse.success(group)); } /** Fuzzy search users by userId or nickname. */ @GetMapping("/users/search") public ResponseEntity>> searchUsers( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestParam String keyword, @RequestParam(defaultValue = "20") int size) { List results = accountRepository.searchByKeyword(appId, keyword, PageRequest.of(0, size)); operationLogService.record(appId, operatorId, "SEARCH_USERS", "ACCOUNT", null, keyword); return ResponseEntity.ok(ApiResponse.success(results)); } /** Fuzzy search groups by id, name, creator or announcement. */ @GetMapping("/groups/search") public ResponseEntity>> searchGroups( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestParam String keyword, @RequestParam(defaultValue = "20") int size) { List results = groupRepository.searchByKeyword(appId, keyword, PageRequest.of(0, size)); operationLogService.record(appId, operatorId, "SEARCH_GROUPS", "GROUP", null, keyword); return ResponseEntity.ok(ApiResponse.success(results)); } /** Search messages across the application. */ @GetMapping("/messages/search") public ResponseEntity>> searchMessages( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @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) { operationLogService.record(appId, operatorId, "SEARCH_MESSAGES", "MESSAGE", null, keyword); 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, @AuthenticationPrincipal String operatorId) { long totalMessages = messageRepository.countByAppId(appId); long totalUsers = accountRepository.countByAppId(appId); long totalGroups = groupRepository.countByAppId(appId); long todayMessages = messageRepository.countTodayByAppId(appId); operationLogService.record(appId, operatorId, "VIEW_STATS", "STATS", null, "summary"); return ResponseEntity.ok(ApiResponse.success(Map.of( "totalMessages", totalMessages, "totalUsers", totalUsers, "totalGroups", totalGroups, "todayMessages", todayMessages ))); } /** Admin queries conversation history between any two users. */ @GetMapping("/messages") public ResponseEntity>> history( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestParam String userA, @RequestParam String userB, @RequestParam(required = false) com.xuqm.im.entity.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) { operationLogService.record(appId, operatorId, "VIEW_HISTORY", "MESSAGE", userA + "," + userB, keyword); return ResponseEntity.ok(ApiResponse.success( messageRepository.findSingleConversationFiltered( appId, userA, userB, msgType, keyword, startTime, endTime, PageRequest.of(page, size)))); } /** Admin revokes an arbitrary message. */ @PostMapping("/messages/{messageId}/revoke") public ResponseEntity> adminRevoke( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @PathVariable String messageId) { ImMessageEntity revoked = messageService.adminRevoke(appId, messageId); operationLogService.record(appId, operatorId, "ADMIN_REVOKE_MESSAGE", "MESSAGE", messageId, null); return ResponseEntity.ok(ApiResponse.success(revoked)); } /** Admin force dismisses a group. */ @DeleteMapping("/groups/{groupId}") public ResponseEntity> adminDismissGroup( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @PathVariable String groupId) { groupService.adminDismiss(groupId); operationLogService.record(appId, operatorId, "ADMIN_DISMISS_GROUP", "GROUP", groupId, null); return ResponseEntity.ok(ApiResponse.ok()); } @GetMapping("/friend-requests") public ResponseEntity>> listFriendRequests(@RequestParam String appId) { return ResponseEntity.ok(ApiResponse.success(friendRequestService.listByApp(appId))); } @PostMapping("/friend-requests") public ResponseEntity> createFriendRequest( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestBody FriendRequestCreateRequest req) { ImFriendRequestEntity saved = friendRequestService.send(appId, req.fromUserId(), req.toUserId(), req.remark()); operationLogService.record(appId, operatorId, "CREATE_FRIEND_REQUEST", "FRIEND_REQUEST", saved.getId(), req.toUserId()); return ResponseEntity.ok(ApiResponse.success(saved)); } @PostMapping("/friend-requests/{requestId}/accept") public ResponseEntity> acceptFriendRequest( @RequestParam String appId, @PathVariable String requestId, @AuthenticationPrincipal String operatorId) { ImFriendRequestEntity saved = friendRequestService.adminAccept(appId, requestId); operationLogService.record(appId, operatorId, "ACCEPT_FRIEND_REQUEST", "FRIEND_REQUEST", requestId, null); return ResponseEntity.ok(ApiResponse.success(saved)); } @PostMapping("/friend-requests/{requestId}/reject") public ResponseEntity> rejectFriendRequest( @RequestParam String appId, @PathVariable String requestId, @AuthenticationPrincipal String operatorId) { ImFriendRequestEntity saved = friendRequestService.adminReject(appId, requestId); operationLogService.record(appId, operatorId, "REJECT_FRIEND_REQUEST", "FRIEND_REQUEST", requestId, null); return ResponseEntity.ok(ApiResponse.success(saved)); } @GetMapping("/blacklist") public ResponseEntity>> listBlacklist(@RequestParam String appId) { return ResponseEntity.ok(ApiResponse.success(blacklistService.listByApp(appId))); } @PostMapping("/blacklist") public ResponseEntity> addBlacklist( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestBody BlacklistRequest req) { ImBlacklistEntity saved = blacklistService.add(appId, req.userId(), req.blockedUserId()); operationLogService.record(appId, operatorId, "ADD_BLACKLIST", "BLACKLIST", saved.getId(), req.blockedUserId()); return ResponseEntity.ok(ApiResponse.success(saved)); } @DeleteMapping("/blacklist") public ResponseEntity> removeBlacklist( @RequestParam String appId, @RequestParam String userId, @RequestParam String blockedUserId, @AuthenticationPrincipal String operatorId) { blacklistService.remove(appId, userId, blockedUserId); operationLogService.record(appId, operatorId, "REMOVE_BLACKLIST", "BLACKLIST", userId, blockedUserId); return ResponseEntity.ok(ApiResponse.ok()); } @GetMapping("/groups/{groupId}/members") public ResponseEntity>> listGroupMembers( @RequestParam String appId, @PathVariable String groupId) { return ResponseEntity.ok(ApiResponse.success(groupService.adminListMembers(appId, groupId))); } @GetMapping("/groups/{groupId}/members/search") public ResponseEntity>> searchGroupMembers( @RequestParam String appId, @PathVariable String groupId, @RequestParam String keyword, @RequestParam(defaultValue = "20") int size) { return ResponseEntity.ok(ApiResponse.success(groupService.adminSearchMembers(appId, groupId, keyword, size))); } @PostMapping("/groups/{groupId}/members") public ResponseEntity> addGroupMember( @RequestParam String appId, @PathVariable String groupId, @AuthenticationPrincipal String operatorId, @RequestBody MemberRequest req) { ImGroupEntity saved = groupService.adminAddMember(appId, groupId, req.userId()); operationLogService.record(appId, operatorId, "ADMIN_ADD_GROUP_MEMBER", "GROUP", groupId, req.userId()); return ResponseEntity.ok(ApiResponse.success(saved)); } @DeleteMapping("/groups/{groupId}/members/{userId}") public ResponseEntity> removeGroupMember( @RequestParam String appId, @PathVariable String groupId, @PathVariable String userId, @AuthenticationPrincipal String operatorId) { ImGroupEntity saved = groupService.adminRemoveMember(appId, groupId, userId); operationLogService.record(appId, operatorId, "ADMIN_REMOVE_GROUP_MEMBER", "GROUP", groupId, userId); return ResponseEntity.ok(ApiResponse.success(saved)); } @PostMapping("/groups/{groupId}/roles") public ResponseEntity> setGroupRole( @RequestParam String appId, @PathVariable String groupId, @AuthenticationPrincipal String operatorId, @RequestBody SetRoleRequest req) { ImGroupEntity saved = groupService.adminSetRole(appId, groupId, req.userId(), req.role()); operationLogService.record(appId, operatorId, "ADMIN_SET_GROUP_ROLE", "GROUP", groupId, req.userId() + ":" + req.role()); return ResponseEntity.ok(ApiResponse.success(saved)); } @PostMapping("/groups/{groupId}/mute") public ResponseEntity> muteGroupMember( @RequestParam String appId, @PathVariable String groupId, @AuthenticationPrincipal String operatorId, @RequestBody MuteMemberRequest req) { ImGroupEntity saved = groupService.adminMuteMember(appId, groupId, req.userId(), req.minutes()); operationLogService.record(appId, operatorId, "ADMIN_MUTE_GROUP_MEMBER", "GROUP", groupId, req.userId() + ":" + req.minutes()); return ResponseEntity.ok(ApiResponse.success(saved)); } @GetMapping("/groups/{groupId}/join-requests") public ResponseEntity>> listGroupJoinRequests( @RequestParam String appId, @PathVariable String groupId) { return ResponseEntity.ok(ApiResponse.success(groupService.adminListJoinRequests(appId, groupId))); } @PostMapping("/groups/{groupId}/join-requests/{requestId}/accept") public ResponseEntity> acceptGroupJoinRequest( @RequestParam String appId, @PathVariable String groupId, @PathVariable String requestId, @AuthenticationPrincipal String operatorId) { ImGroupJoinRequestEntity saved = groupService.adminAcceptJoinRequest(appId, groupId, requestId); operationLogService.record(appId, operatorId, "ADMIN_ACCEPT_GROUP_JOIN_REQUEST", "GROUP_JOIN_REQUEST", requestId, null); return ResponseEntity.ok(ApiResponse.success(saved)); } @PostMapping("/groups/{groupId}/join-requests/{requestId}/reject") public ResponseEntity> rejectGroupJoinRequest( @RequestParam String appId, @PathVariable String groupId, @PathVariable String requestId, @AuthenticationPrincipal String operatorId) { ImGroupJoinRequestEntity saved = groupService.adminRejectJoinRequest(appId, groupId, requestId); operationLogService.record(appId, operatorId, "ADMIN_REJECT_GROUP_JOIN_REQUEST", "GROUP_JOIN_REQUEST", requestId, null); return ResponseEntity.ok(ApiResponse.success(saved)); } @GetMapping("/webhooks") public ResponseEntity>> listWebhooks(@RequestParam String appId) { return ResponseEntity.ok(ApiResponse.success(webhookConfigService.list(appId))); } @PostMapping("/webhooks") public ResponseEntity> createWebhook( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @RequestBody WebhookConfigRequest req) { WebhookConfigEntity saved = webhookConfigService.create(appId, req.url(), req.secret(), req.enabled()); operationLogService.record(appId, operatorId, "CREATE_WEBHOOK", "WEBHOOK", saved.getId(), req.url()); return ResponseEntity.ok(ApiResponse.success(saved)); } @PutMapping("/webhooks/{id}") public ResponseEntity> updateWebhook( @RequestParam String appId, @PathVariable String id, @AuthenticationPrincipal String operatorId, @RequestBody WebhookConfigRequest req) { WebhookConfigEntity saved = webhookConfigService.update(appId, id, req.url(), req.secret(), req.enabled()); operationLogService.record(appId, operatorId, "UPDATE_WEBHOOK", "WEBHOOK", id, req.url()); return ResponseEntity.ok(ApiResponse.success(saved)); } @DeleteMapping("/webhooks/{id}") public ResponseEntity> deleteWebhook( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @PathVariable String id) { webhookConfigService.delete(appId, id); operationLogService.record(appId, operatorId, "DELETE_WEBHOOK", "WEBHOOK", id, null); 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, @AuthenticationPrincipal String operatorId, @RequestBody KeywordFilterRequest req) { KeywordFilterEntity saved = keywordFilterService.add(appId, req.pattern(), req.replacement(), req.action()); operationLogService.record(appId, operatorId, "CREATE_KEYWORD_FILTER", "KEYWORD_FILTER", saved.getId(), req.pattern()); return ResponseEntity.ok(ApiResponse.success(saved)); } @PutMapping("/keyword-filters/{id}") public ResponseEntity> updateKeywordFilter( @RequestParam String appId, @PathVariable String id, @AuthenticationPrincipal String operatorId, @RequestBody KeywordFilterRequest req) { KeywordFilterEntity saved = keywordFilterService.update(appId, id, req.pattern(), req.replacement(), req.action(), req.enabled()); operationLogService.record(appId, operatorId, "UPDATE_KEYWORD_FILTER", "KEYWORD_FILTER", id, req.pattern()); return ResponseEntity.ok(ApiResponse.success(saved)); } @DeleteMapping("/keyword-filters/{id}") public ResponseEntity> deleteKeywordFilter( @RequestParam String appId, @AuthenticationPrincipal String operatorId, @PathVariable String id) { keywordFilterService.delete(appId, id); operationLogService.record(appId, operatorId, "DELETE_KEYWORD_FILTER", "KEYWORD_FILTER", id, null); 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, @AuthenticationPrincipal String operatorId, @RequestParam boolean enabled) { ImGlobalMuteEntity saved = globalMuteService.setEnabled(appId, enabled); operationLogService.record(appId, operatorId, "SET_GLOBAL_MUTE", "GLOBAL_MUTE", saved.getId(), String.valueOf(enabled)); return ResponseEntity.ok(ApiResponse.success(saved)); } @GetMapping("/operation-logs") public ResponseEntity>> operationLogs( @RequestParam String appId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { return ResponseEntity.ok(ApiResponse.success( operationLogService.list(appId, PageRequest.of(page, size)))); } public record RegisterUserRequest( String userId, String nickname, String avatar, ImAccountEntity.Gender gender, ImAccountEntity.Status status) {} public record UpdateUserRequest( String nickname, String avatar, ImAccountEntity.Gender gender, ImAccountEntity.Status status) {} public record CreateGroupRequest(String name, String creatorId, List memberIds, String groupType, String announcement) {} public record UpdateGroupRequest(String name, String groupType, String announcement) {} public record WebhookConfigRequest(String url, String secret, Boolean enabled) {} public record KeywordFilterRequest(String pattern, String replacement, String action, Boolean enabled) {} public record FriendRequestCreateRequest(String fromUserId, String toUserId, String remark) {} public record BlacklistRequest(String userId, String blockedUserId) {} public record MemberRequest(String userId) {} public record SetRoleRequest(String userId, String role) {} public record MuteMemberRequest(String userId, long minutes) {} }