package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.Message; import com.kaidi.oa.domain.SysUser; import com.kaidi.oa.repository.MessageRepository; import com.kaidi.oa.service.AuthorizationService; import jakarta.servlet.http.HttpServletRequest; 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.RestController; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 统一消息中心 API. Identity comes from the auth token (server-authoritative): a user * only ever sees / clears their own messages. Drives the 顶栏铃铛 unread badge + the * message list page. */ @RestController @RequestMapping("/api/oa/messages") public class MessageController { private final MessageRepository messageRepo; private final CurrentUserResolver currentUser; private final AuthorizationService authz; public MessageController(MessageRepository messageRepo, CurrentUserResolver currentUser, AuthorizationService authz) { this.messageRepo = messageRepo; this.currentUser = currentUser; this.authz = authz; } /** * GET /messages — my latest messages (newest first). 按名投递查询后再按稳定 recipientId 过滤: * recipientId 命中本人才返回;recipientId 为空(旧行/未解析)时回退按名(查询已按名命中,自然保留)。 * 这样可堵住"改名后新用户占用旧名、把旧用户站内信连同正文读走"的改名重用泄漏。 */ @GetMapping public ApiResp> list(HttpServletRequest request) { SysUser su = currentUser.resolve(request); String me = currentUser.resolveLabel(request); return ApiResp.ok(messageRepo.findTop200ByRecipientOrderByIdDesc(me).stream() .filter(m -> authz.isOwner(su, m.getRecipientId(), m.getRecipient())) .toList()); } /** GET /messages/unread-count — my unread count (for the bell badge). 同 list:按 recipientId 过滤后计数。 */ @GetMapping("/unread-count") public ApiResp> unreadCount(HttpServletRequest request) { SysUser su = currentUser.resolve(request); String me = currentUser.resolveLabel(request); long count = messageRepo.findByRecipientAndReadFalse(me).stream() .filter(m -> authz.isOwner(su, m.getRecipientId(), m.getRecipient())) .count(); Map m = new LinkedHashMap<>(); m.put("count", count); return ApiResp.ok(m); } /** POST /messages/{id}/read — mark one of my messages read. */ @PostMapping("/{id}/read") public ApiResp read(@PathVariable Long id, HttpServletRequest request) { Message msg = messageRepo.findById(id) .orElseThrow(() -> new NotFoundException("message not found: " + id)); // 对象级读授权:仅收件人本人或 ADMIN 可标记已读并取回正文,杜绝任意登录用户按 id // 枚举把他人站内信(含审批正文/退回理由等)正文回吐(读 IDOR)。 // 身份按稳定 recipientId 比对(空时回退 recipient 名),根治改名重用冒充。 SysUser su = currentUser.resolve(request); boolean isRecipient = authz.isOwner(su, msg.getRecipientId(), msg.getRecipient()); if (!authz.isAdmin(su) && !isRecipient) { throw new ApiException(403, "无权访问:仅收件人本人或管理员可读取/标记该消息"); } if (isRecipient && !msg.isRead()) { msg.setRead(true); messageRepo.save(msg); } return ApiResp.ok(msg); } /** POST /messages/read-all — mark all my messages read. */ @PostMapping("/read-all") public ApiResp> readAll(HttpServletRequest request) { SysUser su = currentUser.resolve(request); String me = currentUser.resolveLabel(request); // 仅标记本人(按 recipientId 命中,空回退名)的未读,杜绝改名重用场景下误标他人未读为已读。 List unread = messageRepo.findByRecipientAndReadFalse(me).stream() .filter(m -> authz.isOwner(su, m.getRecipientId(), m.getRecipient())) .toList(); for (Message msg : unread) { msg.setRead(true); } messageRepo.saveAll(unread); Map m = new LinkedHashMap<>(); m.put("cleared", unread.size()); return ApiResp.ok(m); } }