package com.mydreamjob.messaging; import com.mydreamjob.common.api.ApiResponse; import com.mydreamjob.messaging.dto.MessageResponse; import com.mydreamjob.security.CurrentUserAccessor; import com.mydreamjob.common.logging.RequestIdAccessor; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/conversations") @RequiredArgsConstructor @Tag(name="消息") public class ChatAttachmentController { private final ChatAttachmentService service; private final CurrentUserAccessor current; private final RequestIdAccessor requestIds; @Operation(summary="上传并发送图片或文件消息",description="仅有当前沟通权限的 DIRECT 成员。multipart 字段 file、kind=IMAGE/FILE、clientMessageId 必填;同一重试必须保持三者不变。单文件 10 MiB;IMAGE 支持 JPEG/PNG、最多 1600 万像素且边长≤8192,去除元数据并生成最长边320的JPEG缩略图;FILE支持 PDF/DOC/DOCX/UTF-8 TXT。默认会话有效附件总量100 MiB。成功原子提交消息与字节,复用 MESSAGE_CREATED 推送和游标补拉;不返回公开URL。") @PostMapping(value="/{id}/attachment-messages",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse send(@PathVariable Long id,@RequestParam String kind, @RequestParam String clientMessageId,@RequestPart MultipartFile file,HttpServletRequest request) { return ApiResponse.ok(service.send(id,current.requireUserId(),kind,clientMessageId,file),requestIds.get(request)); } @Operation(summary="鉴权下载消息附件",description="每次重新校验成员、企业认证、有效投递和屏蔽,撤回或发送者禁用后不可下载;返回 no-store/nosniff/attachment,不支持公开URL或Token查询参数。") @GetMapping("/{conversationId}/messages/{messageId}/attachment") public ResponseEntity download(@PathVariable Long conversationId,@PathVariable Long messageId) { return response(service.download(conversationId,messageId,current.requireUserId(),"download"),false); } @Operation(summary="鉴权读取图片预览",description="仅IMAGE,返回已重新编码去除元数据的图片;鉴权与下载相同。") @GetMapping("/{conversationId}/messages/{messageId}/attachment/preview") public ResponseEntity preview(@PathVariable Long conversationId,@PathVariable Long messageId) { return response(service.download(conversationId,messageId,current.requireUserId(),"preview"),true); } @Operation(summary="鉴权读取图片缩略图",description="仅IMAGE,最长边320的JPEG,鉴权与下载相同。") @GetMapping("/{conversationId}/messages/{messageId}/attachment/thumbnail") public ResponseEntity thumbnail(@PathVariable Long conversationId,@PathVariable Long messageId) { return response(service.download(conversationId,messageId,current.requireUserId(),"thumbnail"),true); } private ResponseEntity response(ChatAttachmentService.Download file,boolean inline) { return ResponseEntity.ok().contentType(MediaType.parseMediaType(file.type())) .header(HttpHeaders.CONTENT_DISPOSITION,(inline?ContentDisposition.inline():ContentDisposition.attachment()) .filename(file.name(),java.nio.charset.StandardCharsets.UTF_8).build().toString()) .header(HttpHeaders.CACHE_CONTROL,"no-store").header("X-Content-Type-Options","nosniff") .header("Content-Security-Policy","default-src 'none'; sandbox") .contentLength(file.content().length).body(file.content()); } }