From 9b791fabd11bec1fb3c1f7dccf83bbd4c383f42b Mon Sep 17 00:00:00 2001 From: cst61 Date: Sat, 2 May 2026 21:01:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=85=A7=E7=89=87=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomerManagementController.java | 82 ++++---- .../rj/dto/CustomerPhotoPreviewResult.java | 40 ++++ .../com/rj/dto/CustomerPhotoUploadResult.java | 34 +++ .../service/ICustomerManagementService.java | 18 +- .../impl/CustomerManagementServiceImpl.java | 198 +++++++++++++++++- 5 files changed, 328 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/rj/dto/CustomerPhotoPreviewResult.java create mode 100644 src/main/java/com/rj/dto/CustomerPhotoUploadResult.java diff --git a/src/main/java/com/rj/controller/CustomerManagementController.java b/src/main/java/com/rj/controller/CustomerManagementController.java index 52f050f..7100fc0 100644 --- a/src/main/java/com/rj/controller/CustomerManagementController.java +++ b/src/main/java/com/rj/controller/CustomerManagementController.java @@ -4,18 +4,22 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.rj.common.AudioManagementConstants; import com.rj.common.DataEnrichmentUtil; +import com.rj.dto.CustomerPhotoPreviewResult; +import com.rj.dto.CustomerPhotoUploadResult; import com.rj.entity.AudioManagement; import com.rj.entity.CustomerManagement; import com.rj.entity.SalesManagement; import com.rj.service.IAudioManagementService; import com.rj.service.ICustomerManagementService; import com.rj.service.ISalesManagementService; -import com.rj.service.MinIOService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -35,7 +39,6 @@ import java.util.UUID; * @author 李中华 ,spllzh * @since 2025-08-07 */ -@Slf4j @RestController @RequestMapping("/api/customerManagement") @Tag(name = "客户管理", description = "客户管理相关接口") @@ -50,9 +53,6 @@ public class CustomerManagementController { @Autowired private ISalesManagementService salesManagementService; - @Autowired - private MinIOService minIOService; - /** * 新增客户 */ @@ -395,46 +395,46 @@ public class CustomerManagementController { @PostMapping("/uploadPhoto") @Operation(summary = "上传客户照片", description = "根据客户ID上传照片到 MinIO,返回文件访问 URL") public ResponseEntity> uploadCustomerPhoto( - @Parameter(description = "客户ID", required = true) + @Parameter(description = "客户ID") @RequestParam String id, @Parameter(description = "照片文件", required = true) @RequestParam("file") MultipartFile file) { + CustomerPhotoUploadResult uploadResult = customerManagementService.uploadCustomerPhoto(id, file); Map result = new HashMap<>(); - try { - if (id == null || id.trim().isEmpty()) { - result.put("success", false); - result.put("message", "客户ID不能为空"); - return ResponseEntity.badRequest().body(result); - } - if (file == null || file.isEmpty()) { - result.put("success", false); - result.put("message", "请选择要上传的文件"); - return ResponseEntity.badRequest().body(result); - } - CustomerManagement customer = customerManagementService.getById(id.trim()); - if (customer == null) { - result.put("success", false); - result.put("message", "客户不存在"); - return ResponseEntity.badRequest().body(result); - } - String original = file.getOriginalFilename(); - String ext = ""; - if (original != null && original.contains(".")) { - ext = original.substring(original.lastIndexOf(".")); - } - String objectName = "customer-management/photo/" + id.trim() + "/" + UUID.randomUUID().toString().replace("-", "") + ext; - String url = minIOService.uploadFileWithName(file, objectName); - log.info("客户照片已上传 MinIO,customerId={}, url={}", id, url); - result.put("success", true); - result.put("message", "上传成功"); - result.put("url", url); - return ResponseEntity.ok(result); - } catch (Exception e) { - log.error("客户照片上传失败: {}", e.getMessage(), e); - result.put("success", false); - result.put("message", "上传失败:" + e.getMessage()); - return ResponseEntity.internalServerError().body(result); + result.put("success", uploadResult.isSuccess()); + result.put("message", uploadResult.getMessage()); + if (uploadResult.getUrl() != null) { + result.put("url", uploadResult.getUrl()); } + return ResponseEntity.status(uploadResult.getHttpStatus()).body(result); + } + + /** + * 客户照片预览:从 MinIO 拉流返回图片。 + * 方式一:传 {@code url}(与上传接口返回的 MinIO 访问 URL 一致)。 + * 方式二:传 {@code id} + {@code photoType}(front / side / life),读取客户表中对应照片 URL。 + */ + @GetMapping("/previewPhoto") + @Operation(summary = "预览客户照片", description = "根据 MinIO URL 或客户ID+照片类型从 MinIO 读取并返回图片流,便于 img 标签 src 引用") + public ResponseEntity previewCustomerPhoto( + @Parameter(description = "MinIO 完整访问 URL(与 uploadPhoto 返回的 url 一致)") + @RequestParam(required = false) String url, + @Parameter(description = "客户ID,与 photoType 联用") + @RequestParam(required = false) String id, + @Parameter(description = "照片类型:front 正面 / side 侧面 / life 生活照", example = "front") + @RequestParam(required = false) String photoType) { + CustomerPhotoPreviewResult preview = customerManagementService.openCustomerPhotoPreview(url, id, photoType); + if (preview.getStatus() == CustomerPhotoPreviewResult.Status.NOT_FOUND) { + return ResponseEntity.notFound().build(); + } + if (preview.getStatus() == CustomerPhotoPreviewResult.Status.BAD_REQUEST) { + return ResponseEntity.badRequest().build(); + } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(preview.getContentType())); + headers.setCacheControl("private, max-age=3600"); + Resource resource = new InputStreamResource(preview.getInputStream()); + return ResponseEntity.ok().headers(headers).body(resource); } private AudioManagement buildAudioRecordFromCustomer(CustomerManagement customer, LocalDateTime now) { diff --git a/src/main/java/com/rj/dto/CustomerPhotoPreviewResult.java b/src/main/java/com/rj/dto/CustomerPhotoPreviewResult.java new file mode 100644 index 0000000..2edc5fc --- /dev/null +++ b/src/main/java/com/rj/dto/CustomerPhotoPreviewResult.java @@ -0,0 +1,40 @@ +package com.rj.dto; + +import lombok.Getter; + +import java.io.InputStream; + +/** + * 客户照片预览:从 MinIO 打开流的结果。 + */ +@Getter +public class CustomerPhotoPreviewResult { + + public enum Status { + OK, + NOT_FOUND, + BAD_REQUEST + } + + private final Status status; + private final InputStream inputStream; + private final String contentType; + + private CustomerPhotoPreviewResult(Status status, InputStream inputStream, String contentType) { + this.status = status; + this.inputStream = inputStream; + this.contentType = contentType; + } + + public static CustomerPhotoPreviewResult ok(InputStream inputStream, String contentType) { + return new CustomerPhotoPreviewResult(Status.OK, inputStream, contentType); + } + + public static CustomerPhotoPreviewResult notFound() { + return new CustomerPhotoPreviewResult(Status.NOT_FOUND, null, null); + } + + public static CustomerPhotoPreviewResult badRequest() { + return new CustomerPhotoPreviewResult(Status.BAD_REQUEST, null, null); + } +} diff --git a/src/main/java/com/rj/dto/CustomerPhotoUploadResult.java b/src/main/java/com/rj/dto/CustomerPhotoUploadResult.java new file mode 100644 index 0000000..25e68e3 --- /dev/null +++ b/src/main/java/com/rj/dto/CustomerPhotoUploadResult.java @@ -0,0 +1,34 @@ +package com.rj.dto; + +import lombok.Getter; + +/** + * 客户照片上传至 MinIO 的结果,供控制器映射 HTTP 状态与 JSON。 + */ +@Getter +public class CustomerPhotoUploadResult { + + private final int httpStatus; + private final boolean success; + private final String message; + private final String url; + + private CustomerPhotoUploadResult(int httpStatus, boolean success, String message, String url) { + this.httpStatus = httpStatus; + this.success = success; + this.message = message; + this.url = url; + } + + public static CustomerPhotoUploadResult ok(String url) { + return new CustomerPhotoUploadResult(200, true, "上传成功", url); + } + + public static CustomerPhotoUploadResult badRequest(String message) { + return new CustomerPhotoUploadResult(400, false, message, null); + } + + public static CustomerPhotoUploadResult serverError(String message) { + return new CustomerPhotoUploadResult(500, false, message, null); + } +} diff --git a/src/main/java/com/rj/service/ICustomerManagementService.java b/src/main/java/com/rj/service/ICustomerManagementService.java index e00898a..a8faa77 100644 --- a/src/main/java/com/rj/service/ICustomerManagementService.java +++ b/src/main/java/com/rj/service/ICustomerManagementService.java @@ -1,7 +1,10 @@ package com.rj.service; -import com.rj.entity.CustomerManagement; import com.baomidou.mybatisplus.extension.service.IService; +import com.rj.dto.CustomerPhotoPreviewResult; +import com.rj.dto.CustomerPhotoUploadResult; +import com.rj.entity.CustomerManagement; +import org.springframework.web.multipart.MultipartFile; /** *

@@ -13,4 +16,17 @@ import com.baomidou.mybatisplus.extension.service.IService; */ public interface ICustomerManagementService extends IService { + /** + * 校验客户存在后,将照片上传至 MinIO,返回访问 URL。 + */ + CustomerPhotoUploadResult uploadCustomerPhoto(String id, MultipartFile file); + + /** + * 根据 MinIO URL 或客户 ID + 照片类型解析对象并打开输入流,供预览下载。 + * + * @param url 与上传接口返回一致的 MinIO 访问 URL,可为空 + * @param id 客户 ID,与 photoType 联用时可空 + * @param photoType front / side / life,与 id 联用时可空 + */ + CustomerPhotoPreviewResult openCustomerPhotoPreview(String url, String id, String photoType); } diff --git a/src/main/java/com/rj/service/impl/CustomerManagementServiceImpl.java b/src/main/java/com/rj/service/impl/CustomerManagementServiceImpl.java index 3f12a34..d626ed8 100644 --- a/src/main/java/com/rj/service/impl/CustomerManagementServiceImpl.java +++ b/src/main/java/com/rj/service/impl/CustomerManagementServiceImpl.java @@ -1,10 +1,21 @@ package com.rj.service.impl; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.rj.config.MinIOConfig; +import com.rj.dto.CustomerPhotoPreviewResult; +import com.rj.dto.CustomerPhotoUploadResult; import com.rj.entity.CustomerManagement; import com.rj.mapper.CustomerManagementMapper; import com.rj.service.ICustomerManagementService; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.rj.service.MinIOService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.UUID; /** *

@@ -14,7 +25,190 @@ import org.springframework.stereotype.Service; * @author 李中华 ,spllzh * @since 2025-08-07 */ +@Slf4j @Service -public class CustomerManagementServiceImpl extends ServiceImpl implements ICustomerManagementService { +public class CustomerManagementServiceImpl extends ServiceImpl + implements ICustomerManagementService { + @Autowired + private MinIOService minIOService; + + @Autowired + private MinIOConfig minioConfig; + + @Override + public CustomerPhotoUploadResult uploadCustomerPhoto(String id, MultipartFile file) { + try { + if (id == null || id.trim().isEmpty()) { + return CustomerPhotoUploadResult.badRequest("客户ID不能为空"); + } + if (file == null || file.isEmpty()) { + return CustomerPhotoUploadResult.badRequest("请选择要上传的文件"); + } + CustomerManagement customer = getById(id.trim()); + if (customer == null) { + return CustomerPhotoUploadResult.badRequest("客户不存在"); + } + String original = file.getOriginalFilename(); + String ext = ""; + if (original != null && original.contains(".")) { + ext = original.substring(original.lastIndexOf(".")); + } + String objectName = "customer-management/photo/" + id.trim() + "/" + + UUID.randomUUID().toString().replace("-", "") + ext; + String url = minIOService.uploadFileWithName(file, objectName); + log.info("客户照片已上传 MinIO,customerId={}, url={}", id, url); + return CustomerPhotoUploadResult.ok(url); + } catch (Exception e) { + log.error("客户照片上传失败: {}", e.getMessage(), e); + return CustomerPhotoUploadResult.serverError("上传失败:" + e.getMessage()); + } + } + + @Override + public CustomerPhotoPreviewResult openCustomerPhotoPreview(String url, String id, String photoType) { + String photoUrl = null; + if (url != null && !url.trim().isEmpty()) { + photoUrl = url.trim(); + } else if (id != null && !id.trim().isEmpty() && photoType != null && !photoType.trim().isEmpty()) { + CustomerManagement customer = getById(id.trim()); + if (customer == null) { + return CustomerPhotoPreviewResult.notFound(); + } + String type = photoType.trim().toLowerCase(); + switch (type) { + case "front": + photoUrl = customer.getFrontPhotoUrl(); + break; + case "side": + photoUrl = customer.getSidePhotoUrl(); + break; + case "life": + photoUrl = customer.getLifePhotoUrl(); + break; + default: + return CustomerPhotoPreviewResult.badRequest(); + } + } else { + return CustomerPhotoPreviewResult.badRequest(); + } + if (photoUrl == null || photoUrl.trim().isEmpty()) { + return CustomerPhotoPreviewResult.notFound(); + } + String loc = photoUrl.trim(); + String nameProbe = lastPathSegmentForPreview(loc); + if (!isImageFile(nameProbe)) { + log.warn("预览拒绝非图片扩展名: {}", nameProbe); + return CustomerPhotoPreviewResult.badRequest(); + } + String objectName = resolveMinioObjectNameForPreview(loc); + if (objectName == null || objectName.isEmpty()) { + log.warn("无法从 URL 解析 MinIO 对象名: {}", loc); + return CustomerPhotoPreviewResult.badRequest(); + } + try { + InputStream inputStream = minIOService.downloadFile(objectName); + String contentType = imageContentType(nameProbe); + return CustomerPhotoPreviewResult.ok(inputStream, contentType); + } catch (Exception e) { + log.error("从 MinIO 读取客户照片失败, objectName={}", objectName, e); + return CustomerPhotoPreviewResult.notFound(); + } + } + + private static String lastPathSegmentForPreview(String urlOrPath) { + if (urlOrPath == null) { + return ""; + } + String s = urlOrPath.trim(); + int q = s.indexOf('?'); + if (q >= 0) { + s = s.substring(0, q); + } + int slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\')); + return slash >= 0 ? s.substring(slash + 1) : s; + } + + private static boolean isImageFile(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return false; + } + String ext = getFileExtension(fileName).toLowerCase(); + switch (ext) { + case "jpg": + case "jpeg": + case "png": + case "gif": + case "webp": + case "bmp": + case "ico": + return true; + default: + return false; + } + } + + private static String getFileExtension(String fileName) { + if (fileName == null) { + return ""; + } + int i = fileName.lastIndexOf('.'); + return i >= 0 ? fileName.substring(i + 1) : ""; + } + + private static String imageContentType(String fileName) { + String ext = getFileExtension(fileName).toLowerCase(); + switch (ext) { + case "jpg": + case "jpeg": + return MediaType.IMAGE_JPEG_VALUE; + case "png": + return MediaType.IMAGE_PNG_VALUE; + case "gif": + return MediaType.IMAGE_GIF_VALUE; + case "webp": + return "image/webp"; + case "bmp": + return "image/bmp"; + case "ico": + return "image/x-icon"; + default: + return MediaType.APPLICATION_OCTET_STREAM_VALUE; + } + } + + private String resolveMinioObjectNameForPreview(String fileUrl) { + if (fileUrl == null || fileUrl.trim().isEmpty()) { + return null; + } + String trimmed = fileUrl.trim(); + String withoutQuery = trimmed; + int q = trimmed.indexOf('?'); + if (q >= 0) { + withoutQuery = trimmed.substring(0, q); + } + String prefix = minioConfig.getFileUrlPrefix(); + if (prefix != null && !prefix.isEmpty() && withoutQuery.startsWith(prefix)) { + return withoutQuery.substring(prefix.length()); + } + return extractObjectNameFromUrlFallback(trimmed); + } + + private static String extractObjectNameFromUrlFallback(String url) { + if (url == null || url.trim().isEmpty()) { + return null; + } + try { + String urlWithoutParams = url.contains("?") ? url.substring(0, url.indexOf('?')) : url; + String[] parts = urlWithoutParams.split("/"); + for (int i = parts.length - 1; i >= 0; i--) { + if (parts[i] != null && !parts[i].trim().isEmpty()) { + return parts[i]; + } + } + return null; + } catch (Exception e) { + return null; + } + } }