cos 上传针对图片保真压缩。

This commit is contained in:
Jack 2025-11-15 15:22:30 +08:00
parent c0b3b74533
commit bd8e11a663
4 changed files with 396 additions and 85 deletions

View File

@ -30,6 +30,7 @@ import com.suisung.mall.common.pojo.dto.OssCallbackResultDTO;
import com.suisung.mall.common.pojo.dto.OssPolicyResultDTO;
import com.suisung.mall.common.utils.I18nUtil;
import com.suisung.mall.common.utils.LogUtil;
import com.suisung.mall.common.utils.UploadUtil;
import com.suisung.mall.common.utils.VideoUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
@ -169,6 +170,9 @@ public class OssServiceImpl implements OssService {
* @return
*/
public Map upload(MultipartFile file, UserDto user, String dir, String uploadPath, String uploadName, String fileName) {
// 如果是图片先压缩图片再上传到 cos 对象存储
file = UploadUtil.compressImageIfNeeded(file, 2048);
// 创建临时文件
creTempFile(file, dir, uploadName);
String url = null;
@ -248,7 +252,7 @@ public class OssServiceImpl implements OssService {
tempFile.delete();
logger.info("临时文件已删除: {}", uploadPath);
}
// 同时删除可能创建的封面文件
String coverPath = uploadPath.replace("." + VideoUtil.getVideoFormat(uploadPath), ".jpg");
File coverFile = new File(coverPath);

View File

@ -6,8 +6,11 @@ import org.apache.tika.Tika;
import org.springframework.util.Base64Utils;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.*;
import java.nio.file.Files;
@ -437,4 +440,250 @@ public class UploadUtil {
}
/**
* 压缩图片文件以提高质量
* 如果文件大小超过500KB则在保持宽高比的同时调整大小
* 以提高清晰度并减小文件大小
*
* @param file 原始图片文件
* @param pixelLimit 像素限制
* @return 压缩后的图片文件如果不需要压缩则返回原始文件
*/
public static MultipartFile compressImageIfNeeded(MultipartFile file, Integer pixelLimit) {
if (file == null) {
return null;
}
try {
// 检查文件是否为图片格式
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
log.debug("文件不是图片格式,跳过压缩: {}", contentType);
return file;
}
if (pixelLimit == null) {
pixelLimit = 2048;
}
// 小文件直接返回
if (file.getSize() <= 512000) { // 500KB
log.debug("文件大小在限制范围内 ({} 字节),无需压缩", file.getSize());
return file;
}
// 将MultipartFile转换为BufferedImage
BufferedImage originalImage = ImageIO.read(file.getInputStream());
if (originalImage == null) {
log.warn("读取图片文件失败,跳过压缩");
return file;
}
int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
// 小图片直接返回
if (originalWidth <= pixelLimit && originalHeight <= pixelLimit) {
log.debug("图片尺寸在限制范围内 ({}x{}),无需调整大小", originalWidth, originalHeight);
return file;
}
// 计算保持宽高比的新尺寸
double aspectRatio = (double) originalWidth / originalHeight;
int newWidth, newHeight;
// 目标尺寸最长边最大为pixelLimit像素
if (originalWidth > originalHeight) {
newWidth = Math.min(pixelLimit, originalWidth);
newHeight = (int) (newWidth / aspectRatio);
} else {
newHeight = Math.min(pixelLimit, originalHeight);
newWidth = (int) (newHeight * aspectRatio);
}
// 确保最小尺寸
newWidth = Math.max(newWidth, 1);
newHeight = Math.max(newHeight, 1);
log.debug("调整图片尺寸从 {}x{} 到 {}x{}", originalWidth, originalHeight, newWidth, newHeight);
// 获取图片格式并执行高质量渐进式缩放
String format = getImageFormat(file.getOriginalFilename());
BufferedImage resizedImage = progressiveResize(originalImage, newWidth, newHeight, format);
// 转换回MultipartFile
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(resizedImage, format, baos);
byte[] compressedBytes = baos.toByteArray();
log.info("图片从 {} 字节压缩到 {} 字节", file.getSize(), compressedBytes.length);
return new InMemoryMultipartFile(
file.getName(),
file.getOriginalFilename(),
file.getContentType(),
compressedBytes
);
} catch (Exception e) {
log.error("压缩图片时出错,使用原始文件: {}", e.getMessage(), e);
return file;
}
}
/**
* 渐进式调整图片大小以提高质量
*
* @param originalImage 原始图片
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @param format 图片格式
* @return 调整大小后的图片质量更好
*/
private static BufferedImage progressiveResize(BufferedImage originalImage, int targetWidth, int targetHeight, String format) {
int currentWidth = originalImage.getWidth();
int currentHeight = originalImage.getHeight();
// 多步缩小以提高质量
BufferedImage resizedImage = originalImage;
while (currentWidth > targetWidth * 2 || currentHeight > targetHeight * 2) {
currentWidth = Math.max(targetWidth, currentWidth / 2);
currentHeight = Math.max(targetHeight, currentHeight / 2);
resizedImage = scaleImage(resizedImage, currentWidth, currentHeight, format);
}
// 最终调整到目标尺寸
return scaleImage(resizedImage, targetWidth, targetHeight, format);
}
/**
* 使用高质量渲染提示缩放图片
*
* @param originalImage 原始图片
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @param format 图片格式
* @return 缩放后的图片
*/
private static BufferedImage scaleImage(BufferedImage originalImage, int targetWidth, int targetHeight, String format) {
int imageType = getImageTypeForFormat(format);
BufferedImage scaledImage = new BufferedImage(targetWidth, targetHeight, imageType);
Graphics2D g2d = scaledImage.createGraphics();
// 高质量渲染设置
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
g2d.dispose();
return scaledImage;
}
/**
* 根据文件扩展名获取图片格式支持更多格式
*
* @param filename 原始文件名
* @return 图片格式 (jpg, png, gif )
*/
private static String getImageFormat(String filename) {
if (StrUtil.isBlank(filename)) {
return "jpg";
}
String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
switch (extension) {
case "png":
return "png";
case "gif":
return "gif";
case "bmp":
return "bmp";
case "wbmp":
return "wbmp";
case "jpeg":
case "jpg":
return "jpeg";
default:
return "jpeg";
}
}
/**
* 根据格式确定适当的图片类型以保留透明度
*
* @param format 图片格式
* @return BufferedImage类型
*/
private static int getImageTypeForFormat(String format) {
if ("png".equalsIgnoreCase(format) || "gif".equalsIgnoreCase(format)) {
return BufferedImage.TYPE_INT_ARGB; // 保留透明度
}
return BufferedImage.TYPE_INT_RGB; // JPEG等格式的默认值
}
/**
* Simple MultipartFile implementation for in-memory data
*/
private static class InMemoryMultipartFile implements MultipartFile {
private final String name;
private final String originalFilename;
private final String contentType;
private final byte[] content;
public InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
this.name = name;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.content = content;
}
@Override
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return content == null || content.length == 0;
}
@Override
public long getSize() {
return content.length;
}
@Override
public byte[] getBytes() throws IOException {
return content;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(content);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
new FileOutputStream(dest).write(content);
}
}
}

View File

@ -136,85 +136,124 @@ public class LklTkServiceImpl {
* 身份证反面ID_CARD_BEHIND
* 营业执照BUSINESS_LICENCE
* 银行卡BANK_CARD
* @return
* @return 上传结果及OCR识别信息
*/
public CommonResult uploadOcrImg(MultipartFile file, String imgType) {
UserDto currentUser = getCurrentUser();
if (currentUser == null) {
currentUser = new UserDto();
}
// 参数校验
if (file == null || StrUtil.isBlank(imgType)) {
logger.warn("上传文件参数缺失: imgType={}", imgType);
return CommonResult.failed("上传文件或图片类型不能为空");
}
CommonResult ossImgInfo = ossService.uploadFile(file, currentUser);
if (ossImgInfo == null) {
return CommonResult.failed("上传文件失败");
}
if (ossImgInfo.getStatus() != ResultCode.SUCCESS.getStatus() || ossImgInfo.getData() == null) {
return CommonResult.failed(ossImgInfo.getMsg());
}
String imgURL = JSONUtil.parseObj(ossImgInfo.getData()).getStr("url");
String authorization = getLklTkAuthorization();
if (StrUtil.isBlank(authorization)) {
return CommonResult.failed("获取拉卡拉token失败");
}
JSONObject header = new JSONObject();
header.put("Authorization", authorization);
String fileBase64 = UploadUtil.multipartFileToBase64(file);
if (StrUtil.isBlank(fileBase64)) {
return CommonResult.failed("解析文件转换失败");
}
// Base64Utils.encodeToString(file.getBytes());
JSONObject requestBody = new JSONObject();
requestBody.put("fileBase64", fileBase64);
requestBody.put("imgType", imgType);
requestBody.put("sourcechnl", "0"); // 来源: 0:PC,1:安卓,2:IOS
requestBody.put("isOcr", "true");
String urlPath = "/sit/htkregistration/file/base/upload";
if (isLklProd) {
// 生产环境启用
urlPath = "/registration/file/base/upload";
}
try {
ResponseEntity<JSONObject> updResponse = RestTemplateHttpUtil.sendPostBodyBackEntity(buildLklTkUrl(urlPath), header, requestBody, JSONObject.class);
if (ObjectUtil.isEmpty(updResponse)
|| updResponse.getStatusCode() != HttpStatus.OK
|| ObjectUtil.isEmpty(updResponse.getBody())) {
UserDto currentUser = getCurrentUser();
if (currentUser == null) {
currentUser = new UserDto();
}
// Compress image if needed before processing
MultipartFile processedFile = UploadUtil.compressImageIfNeeded(file, 2048);
if (processedFile == null) {
logger.warn("上传文件压缩失败: filename={}, imgType={}",
file.getOriginalFilename(), imgType);
return CommonResult.failed("服务繁忙,请重试!");
}
// 上传文件到OSS
CommonResult ossImgInfo = ossService.uploadFile(processedFile, currentUser);
if (ossImgInfo == null) {
logger.error("上传文件到OSS失败filename={}, imgType={}",
processedFile.getOriginalFilename(), imgType);
return CommonResult.failed("上传文件失败");
}
if (ossImgInfo.getStatus() != ResultCode.SUCCESS.getStatus() || ossImgInfo.getData() == null) {
logger.error("OSS上传响应异常filename={}, imgType={}, status={}",
processedFile.getOriginalFilename(), imgType, ossImgInfo.getStatus());
return CommonResult.failed(ossImgInfo.getMsg());
}
String imgURL = JSONUtil.parseObj(ossImgInfo.getData()).getStr("url");
logger.debug("文件上传OSS成功filename={}", processedFile.getOriginalFilename());
// 获取拉卡拉认证信息
String authorization = getLklTkAuthorization();
if (StrUtil.isBlank(authorization)) {
logger.error("获取拉卡拉token失败filename={}, imgType={}",
processedFile.getOriginalFilename(), imgType);
return CommonResult.failed("获取拉卡拉token失败");
}
// 构造请求头
JSONObject header = new JSONObject();
header.put("Authorization", authorization);
// 文件转Base64
String fileBase64 = UploadUtil.multipartFileToBase64(processedFile);
if (StrUtil.isBlank(fileBase64)) {
logger.error("文件转换Base64失败filename={}, imgType={}",
processedFile.getOriginalFilename(), imgType);
return CommonResult.failed("解析文件转换失败");
}
// 构造请求体
JSONObject requestBody = new JSONObject();
requestBody.put("fileBase64", fileBase64);
requestBody.put("imgType", imgType);
requestBody.put("sourcechnl", "0"); // 来源: 0:PC,1:安卓,2:IOS
requestBody.put("isOcr", "true");
// 构造请求路径
String urlPath = "/sit/htkregistration/file/base/upload";
if (isLklProd) {
urlPath = "/registration/file/base/upload";
}
// 发送请求并获取响应
ResponseEntity<JSONObject> updResponse = RestTemplateHttpUtil.sendPostBodyBackEntity(
buildLklTkUrl(urlPath), header, requestBody, JSONObject.class);
logger.info("调用拉卡拉文件上传接口filename={}, imgType={}",
processedFile.getOriginalFilename(), imgType);
// 检查响应有效性
if (ObjectUtil.isEmpty(updResponse) ||
updResponse.getStatusCode() != HttpStatus.OK ||
ObjectUtil.isEmpty(updResponse.getBody())) {
logger.error("拉卡拉文件上传响应数据异常filename={}, imgType={}",
processedFile.getOriginalFilename(), imgType);
return CommonResult.failed("上传文件返回值有误");
}
// {batchNo,status,url,showUrl,result{} }
// 提取上传结果
JSONObject updObj = updResponse.getBody();
String batchNo = updObj.getStr("batchNo");
if (StrUtil.isBlank(batchNo)) {
logger.error("拉卡拉文件上传返回批次号为空filename={}, imgType={}",
processedFile.getOriginalFilename(), imgType);
return CommonResult.failed("上传文件返回值有误");
}
updObj.put("cosURL", imgURL);
logger.info("拉卡拉文件上传成功filename={}, imgType={}, batchNo={}",
processedFile.getOriginalFilename(), imgType, batchNo);
return CommonResult.success(updObj);
} catch (Exception e) {
logger.error("上传文件失败: ", e.getMessage());
logger.error("上传文件过程发生异常filename={}, imgType={}",
file != null ? file.getOriginalFilename() : "unknown", imgType, e);
return CommonResult.failed("上传文件失败:" + e.getMessage());
}
}
/**
* 根据上传的图片的批次号获取 OCR 识别结果
*
* @param batchNo
* @param imgType * ID_CARD_FRONT 身份证正
* @param batchNo 批次号
* @param imgType 图片类型
* * ID_CARD_FRONT 身份证正
* * ID_CARD_BEHIND 身份证反
* * BUSINESS_LICENCE 营业执照照
* * BANK_CARD 银行卡企业对公不需要传
@ -227,55 +266,70 @@ public class LklTkServiceImpl {
* * SETTLE_ID_CARD_FRONT 结算人身份证人像面
* * SETTLE_ID_CARD_BEHIND 结算人身份证国徽面
* * LETTER_OF_AUTHORIZATION 法人授权涵
* @return
* @return OCR识别结果
*/
public CommonResult imgOcrResult(String batchNo, String imgType) {
// 参数校验
if (StrUtil.isBlank(batchNo) || StrUtil.isBlank(imgType)) {
logger.warn("OCR识别参数缺失: batchNo={}, imgType={}", batchNo, imgType);
return CommonResult.failed("批次号或图片类型不能为空");
}
// 调用 OCR 识别接口
String authorization = getLklTkAuthorization();
if (StrUtil.isBlank(authorization)) {
return CommonResult.failed("获取拉卡拉token失败");
}
JSONObject header = new JSONObject();
header.put("Authorization", authorization);
JSONObject ocrRequestBody = new JSONObject();
ocrRequestBody.put("batchNo", batchNo);
ocrRequestBody.put("imgType", imgType);
logger.info("ocr请求参数{}", ocrRequestBody);
String urlPath = "/sit/htkregistration/ocr/result";
if (isLklProd) {
// 生产环境启用
urlPath = "/registration/ocr/result";
}
try {
ResponseEntity<JSONObject> ocrResponse = RestTemplateHttpUtil.sendPostBodyBackEntity(buildLklTkUrl(urlPath), header, ocrRequestBody, JSONObject.class);
if (ObjectUtil.isEmpty(ocrResponse)
|| ocrResponse.getStatusCode() != HttpStatus.OK
|| ObjectUtil.isEmpty(ocrResponse.getBody())) {
// 获取认证信息
String authorization = getLklTkAuthorization();
if (StrUtil.isBlank(authorization)) {
logger.error("获取拉卡拉token失败batchNo={}, imgType={}", batchNo, imgType);
return CommonResult.failed("获取拉卡拉token失败");
}
// 构造请求头
JSONObject header = new JSONObject();
header.put("Authorization", authorization);
// 构造请求体
JSONObject ocrRequestBody = new JSONObject();
ocrRequestBody.put("batchNo", batchNo);
ocrRequestBody.put("imgType", imgType);
logger.info("调用OCR识别接口batchNo={}, imgType={}", batchNo, imgType);
// 构造请求路径
String urlPath = "/sit/htkregistration/ocr/result";
if (isLklProd) {
urlPath = "/registration/ocr/result";
}
// 发送请求并获取响应
ResponseEntity<JSONObject> ocrResponse = RestTemplateHttpUtil.sendPostBodyBackEntity(
buildLklTkUrl(urlPath), header, ocrRequestBody, JSONObject.class);
// 检查响应有效性
if (ObjectUtil.isEmpty(ocrResponse) ||
ocrResponse.getStatusCode() != HttpStatus.OK ||
ObjectUtil.isEmpty(ocrResponse.getBody())) {
logger.error("OCR识别响应数据异常batchNo={}, imgType={}", batchNo, imgType);
return CommonResult.failed("OCR响应数据有误");
}
JSONObject ocrObj = ocrResponse.getBody().get("result", JSONObject.class);
logger.info("ocr返回结果{}", ocrResponse);
// 提取OCR结果
JSONObject result = ocrResponse.getBody();
JSONObject ocrObj = result.get("result", JSONObject.class);
logger.info("OCR识别成功batchNo={}, imgType={}", batchNo, imgType);
if (ObjectUtil.isEmpty(ocrObj)) {
logger.warn("OCR识别返回结果为空batchNo={}, imgType={}", batchNo, imgType);
return CommonResult.failed("OCR返回结果有误");
}
return CommonResult.success(ocrObj);
} catch (Exception e) {
logger.error("OCR识别失败: ", e.getMessage());
logger.error("OCR识别过程发生异常batchNo={}, imgType={}", batchNo, imgType, e);
return CommonResult.failed("OCR识别失败:" + e.getMessage());
}
}
/**
* 商户进件前请求获取token
*

View File

@ -27,6 +27,7 @@ import com.suisung.mall.common.pojo.dto.OssPolicyResultDTO;
import com.suisung.mall.common.utils.CheckUtil;
import com.suisung.mall.common.utils.I18nUtil;
import com.suisung.mall.common.utils.LogUtil;
import com.suisung.mall.common.utils.UploadUtil;
import com.suisung.mall.shop.base.service.AccountBaseConfigService;
import com.suisung.mall.shop.page.service.OssService;
import com.suisung.mall.shop.page.utis.VideoUtil;
@ -144,7 +145,7 @@ public class OssServiceImpl implements OssService {
throw new ApiException(String.format(I18nUtil._("允许上传格式为:【%s】"), StringUtils.join(allowExtList, ",")));
}
Map result = upload(file, user, dir, uploadPath, uploadName, fileName);
Map result = upload(file, user, dir, uploadPath, uploadName);
String url = (String) result.get("media_url");
String thumb = (String) result.get("thumb");
@ -173,7 +174,10 @@ public class OssServiceImpl implements OssService {
* @param user 用户信息
* @return
*/
public Map upload(MultipartFile file, UserDto user, String dir, String uploadPath, String uploadName, String fileName) {
public Map upload(MultipartFile file, UserDto user, String dir, String uploadPath, String uploadName) {
// 如果是图片先压缩图片再上传到 cos 对象存储
file = UploadUtil.compressImageIfNeeded(file, 2048);
// 创建临时文件
creTempFile(file, dir, uploadName);
// String localUrl = IP + "/admin/shop/static/image/" + dir + uploadName; // 文件本地路径
@ -294,7 +298,7 @@ public class OssServiceImpl implements OssService {
throw new ApiException(String.format(I18nUtil._("允许上传格式为:【%s】"), StringUtils.join(allowExtList, ",")));
}
return upload(file, user, dir, uploadPath, uploadName, fileName);
return upload(file, user, dir, uploadPath, uploadName);
}
/**